297 lines
9.8 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|