Files
Charon/backend/internal/caddy/config_waf_test.go
GitHub Actions 3169b05156 fix: skip incomplete system log viewer tests
- Marked 12 tests as skip pending feature implementation
- Features tracked in GitHub issue #686 (system log viewer feature completion)
- Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality
- Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation
- TODO comments in code reference GitHub #686 for feature completion tracking
- Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
2026-02-09 21:55:55 +00:00

297 lines
9.8 KiB
Go

package caddy
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/Wikid82/charon/backend/internal/models"
)
// TestBuildWAFHandler_RulesetSelectionPriority verifies the priority order:
// 1. secCfg.WAFRulesSource (user's global choice)
// 2. hostRulesetName from advanced_config
// 3. host.Application
// 4. owasp-crs fallback
func TestBuildWAFHandler_RulesetSelectionPriority(t *testing.T) {
tests := []struct {
name string
host *models.ProxyHost
rulesets []models.SecurityRuleSet
rulesetPaths map[string]string
secCfg *models.SecurityConfig
wafEnabled bool
expectedInclude string // Expected substring in directives, empty if handler should be nil
}{
{
name: "WAFRulesSource takes priority over owasp-crs",
host: &models.ProxyHost{UUID: "test-host"},
rulesets: []models.SecurityRuleSet{{Name: "owasp-crs"}, {Name: "custom-xss"}},
rulesetPaths: map[string]string{
"owasp-crs": "/app/data/rulesets/owasp-crs.conf",
"custom-xss": "/app/data/rulesets/custom-xss.conf",
},
secCfg: &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "custom-xss"},
wafEnabled: true,
expectedInclude: "custom-xss.conf",
},
{
name: "hostRulesetName takes priority over owasp-crs",
host: &models.ProxyHost{
UUID: "test-host",
AdvancedConfig: `{"ruleset_name":"per-host-rules"}`,
},
rulesets: []models.SecurityRuleSet{{Name: "owasp-crs"}, {Name: "per-host-rules"}},
rulesetPaths: map[string]string{
"owasp-crs": "/app/data/rulesets/owasp-crs.conf",
"per-host-rules": "/app/data/rulesets/per-host-rules.conf",
},
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: true,
expectedInclude: "per-host-rules.conf",
},
{
name: "host.Application takes priority over owasp-crs",
host: &models.ProxyHost{
UUID: "test-host",
Application: "wordpress",
},
rulesets: []models.SecurityRuleSet{{Name: "owasp-crs"}, {Name: "wordpress"}},
rulesetPaths: map[string]string{
"owasp-crs": "/app/data/rulesets/owasp-crs.conf",
"wordpress": "/app/data/rulesets/wordpress.conf",
},
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: true,
expectedInclude: "wordpress.conf",
},
{
name: "owasp-crs used as fallback when no other match",
host: &models.ProxyHost{UUID: "test-host"},
rulesets: []models.SecurityRuleSet{{Name: "owasp-crs"}, {Name: "unrelated-rules"}},
rulesetPaths: map[string]string{
"owasp-crs": "/app/data/rulesets/owasp-crs.conf",
"unrelated-rules": "/app/data/rulesets/unrelated.conf",
},
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: true,
expectedInclude: "owasp-crs.conf",
},
{
name: "WAFRulesSource takes priority over host.Application and owasp-crs",
host: &models.ProxyHost{
UUID: "test-host",
Application: "wordpress",
},
rulesets: []models.SecurityRuleSet{{Name: "owasp-crs"}, {Name: "wordpress"}, {Name: "global-custom"}},
rulesetPaths: map[string]string{
"owasp-crs": "/app/data/rulesets/owasp-crs.conf",
"wordpress": "/app/data/rulesets/wordpress.conf",
"global-custom": "/app/data/rulesets/global-custom.conf",
},
secCfg: &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "global-custom"},
wafEnabled: true,
expectedInclude: "global-custom.conf",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
handler, err := buildWAFHandler(tc.host, tc.rulesets, tc.rulesetPaths, tc.secCfg, tc.wafEnabled)
require.NoError(t, err)
if tc.expectedInclude == "" {
require.Nil(t, handler)
return
}
require.NotNil(t, handler)
directives, ok := handler["directives"].(string)
require.True(t, ok, "directives should be a string")
require.Contains(t, directives, tc.expectedInclude)
})
}
}
// TestBuildWAFHandler_NoDirectivesReturnsNil verifies that the handler returns nil
// when no directives can be set (Bug fix #2 from the plan)
func TestBuildWAFHandler_NoDirectivesReturnsNil(t *testing.T) {
tests := []struct {
name string
host *models.ProxyHost
rulesets []models.SecurityRuleSet
rulesetPaths map[string]string
secCfg *models.SecurityConfig
wafEnabled bool
}{
{
name: "Empty rulesets returns nil",
host: &models.ProxyHost{UUID: "test-host"},
rulesets: []models.SecurityRuleSet{},
rulesetPaths: map[string]string{},
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: true,
},
{
name: "Ruleset exists but no path mapping returns nil",
host: &models.ProxyHost{UUID: "test-host"},
rulesets: []models.SecurityRuleSet{{Name: "my-rules"}},
rulesetPaths: map[string]string{
"other-rules": "/path/to/other.conf", // Path for different ruleset
},
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: true,
},
{
name: "WAFRulesSource specified but not in rulesets or paths returns nil",
host: &models.ProxyHost{UUID: "test-host"},
rulesets: []models.SecurityRuleSet{{Name: "other-rules"}},
rulesetPaths: map[string]string{},
secCfg: &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "nonexistent"},
wafEnabled: true,
},
{
name: "Empty path in rulesetPaths returns nil",
host: &models.ProxyHost{UUID: "test-host"},
rulesets: []models.SecurityRuleSet{{Name: "owasp-crs"}},
rulesetPaths: map[string]string{
"owasp-crs": "", // Empty path
},
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
handler, err := buildWAFHandler(tc.host, tc.rulesets, tc.rulesetPaths, tc.secCfg, tc.wafEnabled)
require.NoError(t, err)
require.Nil(t, handler, "Handler should be nil when no directives can be set")
})
}
}
// TestBuildWAFHandler_DisabledModes verifies WAF is disabled correctly
func TestBuildWAFHandler_DisabledModes(t *testing.T) {
rulesets := []models.SecurityRuleSet{{Name: "owasp-crs"}}
rulesetPaths := map[string]string{"owasp-crs": "/path/to/rules.conf"}
host := &models.ProxyHost{UUID: "test-host"}
tests := []struct {
name string
secCfg *models.SecurityConfig
wafEnabled bool
}{
{
name: "wafEnabled false returns nil",
secCfg: &models.SecurityConfig{WAFMode: "block"},
wafEnabled: false,
},
{
name: "WAFMode disabled returns nil",
secCfg: &models.SecurityConfig{WAFMode: "disabled"},
wafEnabled: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
handler, err := buildWAFHandler(host, rulesets, rulesetPaths, tc.secCfg, tc.wafEnabled)
require.NoError(t, err)
require.Nil(t, handler)
})
}
}
// TestBuildWAFHandler_HandlerStructure verifies the JSON structure matches the Handoff Contract
func TestBuildWAFHandler_HandlerStructure(t *testing.T) {
host := &models.ProxyHost{UUID: "test-host"}
rulesets := []models.SecurityRuleSet{{Name: "integration-xss"}}
rulesetPaths := map[string]string{
"integration-xss": "/app/data/caddy/coraza/rulesets/integration-xss-a1b2c3d4.conf",
}
secCfg := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "integration-xss"}
handler, err := buildWAFHandler(host, rulesets, rulesetPaths, secCfg, true)
require.NoError(t, err)
require.NotNil(t, handler)
// Verify handler type
require.Equal(t, "waf", handler["handler"])
// Verify directives contain Include statement
directives, ok := handler["directives"].(string)
require.True(t, ok)
require.Contains(t, directives, "Include /app/data/caddy/coraza/rulesets/integration-xss-a1b2c3d4.conf")
// Verify directives contain expected ModSecurity directives
require.Contains(t, directives, "SecRuleEngine On")
require.Contains(t, directives, "SecRequestBodyAccess On")
// Verify JSON marshaling produces expected structure
jsonBytes, err := json.Marshal(handler)
require.NoError(t, err)
require.Contains(t, string(jsonBytes), `"handler":"waf"`)
require.Contains(t, string(jsonBytes), `"directives":"SecRuleEngine`)
}
// TestBuildWAFHandler_AdvancedConfigParsing verifies advanced_config JSON parsing
func TestBuildWAFHandler_AdvancedConfigParsing(t *testing.T) {
rulesets := []models.SecurityRuleSet{
{Name: "owasp-crs"},
{Name: "custom-ruleset"},
}
rulesetPaths := map[string]string{
"owasp-crs": "/path/owasp.conf",
"custom-ruleset": "/path/custom.conf",
}
secCfg := &models.SecurityConfig{WAFMode: "block"}
tests := []struct {
name string
advancedConfig string
expectedInclude string
}{
{
name: "Valid ruleset_name in advanced_config",
advancedConfig: `{"ruleset_name":"custom-ruleset"}`,
expectedInclude: "custom.conf",
},
{
name: "Invalid JSON falls back to owasp-crs",
advancedConfig: `{invalid json`,
expectedInclude: "owasp.conf",
},
{
name: "Empty advanced_config falls back to owasp-crs",
advancedConfig: "",
expectedInclude: "owasp.conf",
},
{
name: "Empty ruleset_name string falls back to owasp-crs",
advancedConfig: `{"ruleset_name":""}`,
expectedInclude: "owasp.conf",
},
{
name: "Non-string ruleset_name falls back to owasp-crs",
advancedConfig: `{"ruleset_name":123}`,
expectedInclude: "owasp.conf",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
host := &models.ProxyHost{
UUID: "test-host",
AdvancedConfig: tc.advancedConfig,
}
handler, err := buildWAFHandler(host, rulesets, rulesetPaths, secCfg, true)
require.NoError(t, err)
require.NotNil(t, handler)
directives := handler["directives"].(string)
require.Contains(t, directives, tc.expectedInclude)
})
}
}