Enhance GenerateConfig function to accept ruleset paths and update related tests
- Modified the GenerateConfig function to include an additional parameter for ruleset paths. - Updated multiple test cases across various files to accommodate the new parameter. - Enhanced the manager's ApplyConfig method to handle ruleset file creation and error handling. - Added integration tests for Coraza WAF to validate runtime behavior and ruleset application. - Updated documentation to include instructions for testing Coraza WAF integration locally.
This commit is contained in:
2
.github/agents/Frontend_Dev.agent.md
vendored
2
.github/agents/Frontend_Dev.agent.md
vendored
@@ -35,7 +35,7 @@ You do not just "make it work"; you make it **feel** professional, responsive, a
|
||||
3. **Verification (Definition of Done)**:
|
||||
- Run `npm run lint`.
|
||||
- Run `npm run test` (Ensure no regressions).
|
||||
- **MANDATORY**: Run `pre-commit run --all-files`.
|
||||
- **MANDATORY**: Run `pre-commit run --all-files` and fix any issues immediately..
|
||||
</workflow>
|
||||
|
||||
<constraints>
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"strconv"
|
||||
"time"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,13 +18,16 @@ import (
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/config"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/caddy"
|
||||
)
|
||||
|
||||
func setupSecurityTestRouterWithExtras(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
|
||||
// Use a file-backed sqlite DB to avoid shared memory connection issues in tests
|
||||
dsn := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityDecision{}, &models.SecurityAudit{}, &models.SecurityRuleSet{}))
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}, &models.AccessList{}, &models.SecurityConfig{}, &models.SecurityDecision{}, &models.SecurityAudit{}, &models.SecurityRuleSet{}))
|
||||
|
||||
r := gin.New()
|
||||
api := r.Group("/api/v1")
|
||||
@@ -97,3 +102,70 @@ func TestSecurityHandler_CreateAndListDecisionAndRulesets(t *testing.T) {
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &delResp))
|
||||
require.Equal(t, true, delResp["deleted"].(bool))
|
||||
}
|
||||
|
||||
func TestSecurityHandler_UpsertDeleteTriggersApplyConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
// Setup DB
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityDecision{}, &models.SecurityAudit{}, &models.SecurityRuleSet{}))
|
||||
|
||||
// Ensure DB has expected tables (migrations executed above)
|
||||
|
||||
// Ensure proxy_hosts table exists in case AutoMigrate didn't create it
|
||||
db.Exec("CREATE TABLE IF NOT EXISTS proxy_hosts (id INTEGER PRIMARY KEY AUTOINCREMENT, domain_names TEXT, forward_host TEXT, forward_port INTEGER, enabled BOOLEAN)")
|
||||
// Create minimal settings and caddy_configs tables to satisfy Manager.ApplyConfig queries
|
||||
db.Exec("CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, value TEXT, type TEXT, category TEXT, updated_at datetime)")
|
||||
db.Exec("CREATE TABLE IF NOT EXISTS caddy_configs (id INTEGER PRIMARY KEY AUTOINCREMENT, config_hash TEXT, applied_at datetime, success BOOLEAN, error_msg TEXT)")
|
||||
// debug: tables exist
|
||||
|
||||
// Caddy admin server to capture /load calls
|
||||
loadCh := make(chan struct{}, 2)
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/load" && r.Method == http.MethodPost {
|
||||
loadCh <- struct{}{}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := caddy.NewClient(caddyServer.URL)
|
||||
tmp := t.TempDir()
|
||||
m := caddy.NewManager(client, db, tmp, "", false, config.SecurityConfig{CerberusEnabled: true, WAFMode: "block"})
|
||||
|
||||
r := gin.New()
|
||||
api := r.Group("/api/v1")
|
||||
cfg := config.SecurityConfig{}
|
||||
h := NewSecurityHandler(cfg, db, m)
|
||||
api.POST("/security/rulesets", h.UpsertRuleSet)
|
||||
api.DELETE("/security/rulesets/:id", h.DeleteRuleSet)
|
||||
|
||||
// Upsert ruleset should trigger manager.ApplyConfig -> POST /load
|
||||
rpayload := `{"name":"owasp-crs","source_url":"https://example.com/owasp","mode":"owasp","content":"test"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/security/rulesets", strings.NewReader(rpayload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
select {
|
||||
case <-loadCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for manager ApplyConfig /load post on upsert")
|
||||
}
|
||||
|
||||
// Now delete the ruleset and ensure /load is triggered again
|
||||
// Read ID from DB
|
||||
var rs models.SecurityRuleSet
|
||||
assert.NoError(t, db.First(&rs).Error)
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/security/rulesets/"+strconv.Itoa(int(rs.ID)), nil)
|
||||
resp = httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
select {
|
||||
case <-loadCh:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for manager ApplyConfig /load post on delete")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestClient_Load_Success(t *testing.T) {
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
}, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
|
||||
err := client.Load(context.Background(), config)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// GenerateConfig creates a Caddy JSON configuration from proxy hosts.
|
||||
// This is the core transformation layer from our database model to Caddy config.
|
||||
func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
// Define log file paths
|
||||
// We assume storageDir is like ".../data/caddy/data", so we go up to ".../data/logs"
|
||||
// storageDir is .../data/caddy/data
|
||||
@@ -257,7 +257,7 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
|
||||
}
|
||||
|
||||
// WAF handler (placeholder) — add according to runtime flag
|
||||
if wafH, err := buildWAFHandler(&host, rulesets, secCfg, wafEnabled); err == nil && wafH != nil {
|
||||
if wafH, err := buildWAFHandler(&host, rulesets, rulesetPaths, secCfg, wafEnabled); err == nil && wafH != nil {
|
||||
securityHandlers = append(securityHandlers, wafH)
|
||||
}
|
||||
|
||||
@@ -701,7 +701,7 @@ func buildCrowdSecHandler(host *models.ProxyHost, secCfg *models.SecurityConfig,
|
||||
// buildWAFHandler returns a placeholder WAF handler (Coraza) configuration.
|
||||
// This is a stub; integration with a Coraza caddy plugin would be required
|
||||
// for real runtime enforcement.
|
||||
func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet, secCfg *models.SecurityConfig, wafEnabled bool) (Handler, error) {
|
||||
func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, secCfg *models.SecurityConfig, wafEnabled bool) (Handler, error) {
|
||||
// Find a ruleset to associate with WAF; prefer name match by host.Application or default 'owasp-crs'
|
||||
var selected *models.SecurityRuleSet
|
||||
for i, r := range rulesets {
|
||||
@@ -718,6 +718,11 @@ func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet,
|
||||
if selected != nil {
|
||||
h["ruleset_name"] = selected.Name
|
||||
h["ruleset_content"] = selected.Content
|
||||
if rulesetPaths != nil {
|
||||
if p, ok := rulesetPaths[selected.Name]; ok && p != "" {
|
||||
h["ruleset_path"] = p
|
||||
}
|
||||
}
|
||||
} else if secCfg != nil && secCfg.WAFRulesSource != "" {
|
||||
// If there was a requested ruleset name but nothing matched, include it as a reference
|
||||
h["ruleset_name"] = secCfg.WAFRulesSource
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateConfig_CatchAllFrontend(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -32,7 +32,7 @@ func TestGenerateConfig_AdvancedInvalidJSON(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -63,7 +63,7 @@ func TestGenerateConfig_AdvancedArrayHandler(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -77,7 +77,7 @@ func TestGenerateConfig_LowercaseDomains(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{UUID: "d1", DomainNames: "UPPER.EXAMPLE.COM", ForwardHost: "a", ForwardPort: 80, Enabled: true},
|
||||
}
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Debug prints removed
|
||||
@@ -93,7 +93,7 @@ func TestGenerateConfig_AdvancedObjectHandler(t *testing.T) {
|
||||
Enabled: true,
|
||||
AdvancedConfig: `{"handler":"headers","response":{"set":{"X-Obj":["1"]}}}`,
|
||||
}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// First handler should be headers
|
||||
@@ -110,7 +110,7 @@ func TestGenerateConfig_AdvancedHeadersStringToArray(t *testing.T) {
|
||||
Enabled: true,
|
||||
AdvancedConfig: `{"handler":"headers","request":{"set":{"Upgrade":"websocket"}},"response":{"set":{"X-Obj":"1"}}}`,
|
||||
}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Debug prints removed
|
||||
@@ -170,7 +170,7 @@ func TestGenerateConfig_ACLWhitelistIncluded(t *testing.T) {
|
||||
aclH, err := buildACLHandler(&acl, "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, aclH)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Accept either a subroute (ACL) or reverse_proxy as first handler
|
||||
@@ -182,7 +182,7 @@ func TestGenerateConfig_ACLWhitelistIncluded(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_SkipsEmptyDomainEntries(t *testing.T) {
|
||||
hosts := []models.ProxyHost{{UUID: "u1", DomainNames: ", test.example.com", ForwardHost: "a", ForwardPort: 80, Enabled: true}}
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig(hosts, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
require.Equal(t, []string{"test.example.com"}, route.Match[0].Host)
|
||||
@@ -190,7 +190,7 @@ func TestGenerateConfig_SkipsEmptyDomainEntries(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_AdvancedNoHandlerKey(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "adv3", DomainNames: "nohandler.example.com", ForwardHost: "app", ForwardPort: 8080, Enabled: true, AdvancedConfig: `{"foo":"bar"}`}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// No headers handler appended; last handler is reverse_proxy
|
||||
@@ -200,7 +200,7 @@ func TestGenerateConfig_AdvancedNoHandlerKey(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_AdvancedUnexpectedJSONStructure(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "adv4", DomainNames: "struct.example.com", ForwardHost: "app", ForwardPort: 8080, Enabled: true, AdvancedConfig: `42`}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Expect main reverse proxy handler exists but no appended advanced handler
|
||||
@@ -223,7 +223,7 @@ func TestGenerateConfig_SecurityPipeline_Order(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "pipeline1", DomainNames: "pipe.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl, HSTSEnabled: true, BlockExploits: true}
|
||||
|
||||
secCfg := &models.SecurityConfig{CrowdSecMode: "local"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", nil, nil, secCfg)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", nil, nil, nil, secCfg)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
|
||||
@@ -246,7 +246,7 @@ func TestGenerateConfig_SecurityPipeline_Order(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_SecurityPipeline_OmitWhenDisabled(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "pipe2", DomainNames: "pipe2.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestGenerateConfig_ZerosslAndBothProviders(t *testing.T) {
|
||||
}
|
||||
|
||||
// Zerossl provider
|
||||
cfgZ, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "zerossl", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfgZ, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "zerossl", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfgZ.Apps.TLS)
|
||||
// Expect only zerossl issuer present
|
||||
@@ -37,7 +37,7 @@ func TestGenerateConfig_ZerosslAndBothProviders(t *testing.T) {
|
||||
require.True(t, foundZerossl)
|
||||
|
||||
// Default/both provider
|
||||
cfgBoth, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfgBoth, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
issuersBoth := cfgBoth.Apps.TLS.Automation.Policies[0].IssuersRaw
|
||||
// We should have at least 2 issuers (acme + zerossl)
|
||||
@@ -51,7 +51,7 @@ func TestGenerateConfig_SecurityPipeline_Order_Locations(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "pipeline2", DomainNames: "pipe-loc.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl, HSTSEnabled: true, BlockExploits: true, Locations: []models.Location{{Path: "/loc", ForwardHost: "app", ForwardPort: 9000}}}
|
||||
|
||||
sec := &models.SecurityConfig{CrowdSecMode: "local"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", nil, nil, sec)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", nil, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
@@ -96,7 +96,7 @@ func TestGenerateConfig_ACLLogWarning(t *testing.T) {
|
||||
acl := models.AccessList{ID: 300, Name: "BadACL", Enabled: true, Type: "blacklist", IPRules: "invalid-json"}
|
||||
host := models.ProxyHost{UUID: "acl-log", DomainNames: "acl-err.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestGenerateConfig_ACLHandlerIncluded(t *testing.T) {
|
||||
ipRules := `[ { "cidr": "10.0.0.0/8" } ]`
|
||||
acl := models.AccessList{ID: 301, Name: "WL3", Enabled: true, Type: "whitelist", IPRules: ipRules}
|
||||
host := models.ProxyHost{UUID: "acl-incl", DomainNames: "acl-incl.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
require.NotNil(t, server)
|
||||
@@ -136,7 +136,7 @@ func TestGenerateConfig_DecisionsBlockWithAdminExclusion(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "dec1", DomainNames: "dec.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
// create a security decision to block 1.2.3.4
|
||||
dec := models.SecurityDecision{Action: "block", IP: "1.2.3.4"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "10.0.0.1/32", nil, []models.SecurityDecision{dec}, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, "10.0.0.1/32", nil, nil, []models.SecurityDecision{dec}, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// Expect first security handler is a subroute that includes both remote_ip and a 'not' exclusion for adminWhitelist
|
||||
@@ -164,7 +164,7 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "wafref", DomainNames: "wafref.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
// No rulesets provided but secCfg references a rulesource
|
||||
sec := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "nonexistent-rs"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, sec)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
// Since a ruleset name was requested but none exists, coraza handler should include ruleset_name but no ruleset_content
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
@@ -182,7 +182,7 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
|
||||
|
||||
// Now test learning/monitor mode mapping
|
||||
sec2 := &models.SecurityConfig{WAFMode: "block", WAFLearning: true}
|
||||
cfg2, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, sec2)
|
||||
cfg2, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, nil, sec2)
|
||||
require.NoError(t, err)
|
||||
route2 := cfg2.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
monitorFound := false
|
||||
@@ -199,7 +199,7 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
|
||||
func TestGenerateConfig_WAFModeDisabledSkipsHandler(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "waf-disabled", DomainNames: "wafd.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
sec := &models.SecurityConfig{WAFMode: "disabled", WAFRulesSource: "owasp-crs"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, sec)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
for _, h := range route.Handle {
|
||||
@@ -213,7 +213,7 @@ func TestGenerateConfig_WAFSelectedSetsContentAndMode(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "waf-2", DomainNames: "waf2.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
rs := models.SecurityRuleSet{Name: "owasp-crs", SourceURL: "http://example.com/owasp", Content: "rule 1"}
|
||||
sec := &models.SecurityConfig{WAFMode: "block"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, nil, sec)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
found := false
|
||||
@@ -235,7 +235,7 @@ func TestGenerateConfig_DecisionAdminPartsEmpty(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "dec2", DomainNames: "dec2.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
dec := models.SecurityDecision{Action: "block", IP: "2.3.4.5"}
|
||||
// Provide an adminWhitelist with an empty segment to trigger p == ""
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, ", 10.0.0.1/32", nil, []models.SecurityDecision{dec}, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, false, false, ", 10.0.0.1/32", nil, nil, []models.SecurityDecision{dec}, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
found := false
|
||||
@@ -271,7 +271,7 @@ func TestGenerateConfig_WAFUsesRuleSet(t *testing.T) {
|
||||
// host + ruleset configured
|
||||
host := models.ProxyHost{UUID: "waf-1", DomainNames: "waf.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
rs := models.SecurityRuleSet{Name: "owasp-crs", SourceURL: "http://example.com/owasp", Content: "rule 1"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
// check coraza handler present with ruleset_name
|
||||
@@ -290,7 +290,7 @@ func TestGenerateConfig_WAFUsesRuleSet(t *testing.T) {
|
||||
func TestGenerateConfig_RateLimitFromSecCfg(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "rl-1", DomainNames: "rl.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
sec := &models.SecurityConfig{RateLimitRequests: 10, RateLimitWindowSec: 60, RateLimitBurst: 5}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, true, false, "", nil, nil, sec)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, false, true, false, "", nil, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
found := false
|
||||
@@ -310,7 +310,7 @@ func TestGenerateConfig_RateLimitFromSecCfg(t *testing.T) {
|
||||
func TestGenerateConfig_CrowdSecHandlerFromSecCfg(t *testing.T) {
|
||||
host := models.ProxyHost{UUID: "cs-1", DomainNames: "cs.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080}
|
||||
sec := &models.SecurityConfig{CrowdSecMode: "local", CrowdSecAPIURL: "http://cs.local"}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, sec)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, false, false, false, "", nil, nil, nil, sec)
|
||||
require.NoError(t, err)
|
||||
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
|
||||
found := false
|
||||
@@ -326,7 +326,7 @@ func TestGenerateConfig_CrowdSecHandlerFromSecCfg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateConfig_EmptyHostsAndNoFrontend(t *testing.T) {
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/data/caddy/data", "", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{}, "/data/caddy/data", "", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
// Should return base config without server routes
|
||||
_, found := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
@@ -338,7 +338,7 @@ func TestGenerateConfig_SkipsInvalidCustomCert(t *testing.T) {
|
||||
cert := models.SSLCertificate{ID: 1, UUID: "c1", Name: "CustomCert", Provider: "custom", Certificate: "cert", PrivateKey: ""}
|
||||
host := models.ProxyHost{UUID: "h1", DomainNames: "a.example.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080, Certificate: &cert, CertificateID: ptrUint(1)}
|
||||
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
// Custom cert missing key should not be in LoadPEM
|
||||
if cfg.Apps.TLS != nil && cfg.Apps.TLS.Certificates != nil {
|
||||
@@ -351,7 +351,7 @@ func TestGenerateConfig_SkipsDuplicateDomains(t *testing.T) {
|
||||
// Two hosts with same domain - one newer than other should be kept only once
|
||||
h1 := models.ProxyHost{UUID: "h1", DomainNames: "dup.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080}
|
||||
h2 := models.ProxyHost{UUID: "h2", DomainNames: "dup.com", Enabled: true, ForwardHost: "127.0.0.2", ForwardPort: 8081}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{h1, h2}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{h1, h2}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
// Expect that only one route exists for dup.com (one for the domain)
|
||||
@@ -361,7 +361,7 @@ func TestGenerateConfig_SkipsDuplicateDomains(t *testing.T) {
|
||||
func TestGenerateConfig_LoadPEMSetsTLSWhenNoACME(t *testing.T) {
|
||||
cert := models.SSLCertificate{ID: 1, UUID: "c1", Name: "LoadPEM", Provider: "custom", Certificate: "cert", PrivateKey: "key"}
|
||||
host := models.ProxyHost{UUID: "h1", DomainNames: "pem.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080, Certificate: &cert, CertificateID: &cert.ID}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg.Apps.TLS)
|
||||
require.NotNil(t, cfg.Apps.TLS.Certificates)
|
||||
@@ -369,7 +369,7 @@ func TestGenerateConfig_LoadPEMSetsTLSWhenNoACME(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_DefaultAcmeStaging(t *testing.T) {
|
||||
hosts := []models.ProxyHost{{UUID: "h1", DomainNames: "a.example.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080}}
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", true, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "", true, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
// Should include acme issuer with CA staging URL
|
||||
issuers := cfg.Apps.TLS.Automation.Policies[0].IssuersRaw
|
||||
@@ -390,7 +390,7 @@ func TestGenerateConfig_ACLHandlerBuildError(t *testing.T) {
|
||||
// create host with an ACL with invalid JSON to force buildACLHandler to error
|
||||
acl := models.AccessList{ID: 10, Name: "BadACL", Enabled: true, Type: "blacklist", IPRules: "invalid"}
|
||||
host := models.ProxyHost{UUID: "h1", DomainNames: "a.example.com", Enabled: true, ForwardHost: "127.0.0.1", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
// Even if ACL handler error occurs, config should still be returned with routes
|
||||
@@ -401,7 +401,7 @@ func TestGenerateConfig_ACLHandlerBuildError(t *testing.T) {
|
||||
func TestGenerateConfig_SkipHostDomainEmptyAndDisabled(t *testing.T) {
|
||||
disabled := models.ProxyHost{UUID: "h1", Enabled: false, DomainNames: "skip.com", ForwardHost: "127.0.0.1", ForwardPort: 8080}
|
||||
emptyDomain := models.ProxyHost{UUID: "h2", Enabled: true, DomainNames: "", ForwardHost: "127.0.0.1", ForwardPort: 8080}
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{disabled, emptyDomain}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig([]models.ProxyHost{disabled, emptyDomain}, "/data/caddy/data", "", "/frontend/dist", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
server := cfg.Apps.HTTP.Servers["charon_server"]
|
||||
// Both hosts should be skipped; only routes from no hosts should be only catch-all if frontend provided
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestGenerateConfig_CustomCertsAndTLS(t *testing.T) {
|
||||
Locations: []models.Location{{Path: "/app", ForwardHost: "127.0.0.1", ForwardPort: 8081}},
|
||||
},
|
||||
}
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "letsencrypt", true, false, false, false, false, "", nil, nil, nil)
|
||||
cfg, err := GenerateConfig(hosts, "/data/caddy/data", "admin@example.com", "/frontend/dist", "letsencrypt", true, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
// TLS should be configured
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateConfig_Empty(t *testing.T) {
|
||||
config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
require.Empty(t, config.Apps.HTTP.Servers)
|
||||
@@ -34,7 +34,7 @@ func TestGenerateConfig_SingleHost(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
require.Len(t, config.Apps.HTTP.Servers, 1)
|
||||
@@ -76,7 +76,7 @@ func TestGenerateConfig_MultipleHosts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, config.Apps.HTTP.Servers["charon_server"].Routes, 2)
|
||||
require.Len(t, config.Apps.HTTP.Servers["charon_server"].Routes, 2)
|
||||
@@ -93,7 +93,7 @@ func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.HTTP)
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, config.Apps.HTTP.Servers["charon_server"].Routes)
|
||||
// Should produce empty routes (or just catch-all if frontendDir was set, but it's empty here)
|
||||
@@ -124,7 +124,7 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) {
|
||||
|
||||
func TestGenerateConfig_Logging(t *testing.T) {
|
||||
hosts := []models.ProxyHost{}
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Logging)
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestGenerateConfig_Advanced(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config)
|
||||
@@ -211,7 +211,7 @@ func TestGenerateConfig_ACMEStaging(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test with staging enabled
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", true, false, false, false, true, "", nil, nil, nil)
|
||||
config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", true, false, false, false, true, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.TLS)
|
||||
require.NotNil(t, config.Apps.TLS)
|
||||
@@ -227,7 +227,7 @@ func TestGenerateConfig_ACMEStaging(t *testing.T) {
|
||||
require.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", acmeIssuer["ca"])
|
||||
|
||||
// Test with staging disabled (production)
|
||||
config, err = GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", false, false, false, false, false, "", nil, nil, nil)
|
||||
config, err = GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "letsencrypt", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config.Apps.TLS)
|
||||
require.NotNil(t, config.Apps.TLS.Automation)
|
||||
|
||||
@@ -106,7 +106,27 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
|
||||
if secCfg.AdminWhitelist != "" {
|
||||
adminWhitelist = secCfg.AdminWhitelist
|
||||
}
|
||||
config, err := generateConfigFunc(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir, sslProvider, m.acmeStaging, crowdsecEnabled, wafEnabled, rateLimitEnabled, aclEnabled, adminWhitelist, rulesets, decisions, &secCfg)
|
||||
// Ensure ruleset files exist on disk and build a map of their paths for GenerateConfig
|
||||
rulesetPaths := make(map[string]string)
|
||||
if len(rulesets) > 0 {
|
||||
corazaDir := filepath.Join(m.configDir, "coraza", "rulesets")
|
||||
if err := os.MkdirAll(corazaDir, 0755); err != nil {
|
||||
logger.Log().WithError(err).Warn("failed to create coraza rulesets dir")
|
||||
}
|
||||
for _, rs := range rulesets {
|
||||
// sanitize name to a safe filename
|
||||
safeName := strings.ReplaceAll(strings.ToLower(rs.Name), " ", "-")
|
||||
safeName = strings.ReplaceAll(safeName, "/", "-")
|
||||
filePath := filepath.Join(corazaDir, safeName+".conf")
|
||||
if err := writeFileFunc(filePath, []byte(rs.Content), 0600); err != nil {
|
||||
logger.Log().WithError(err).WithField("ruleset", rs.Name).Warn("failed to write coraza ruleset file")
|
||||
} else {
|
||||
rulesetPaths[rs.Name] = filePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config, err := generateConfigFunc(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir, sslProvider, m.acmeStaging, crowdsecEnabled, wafEnabled, rateLimitEnabled, aclEnabled, adminWhitelist, rulesets, rulesetPaths, decisions, &secCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %w", err)
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ func TestManager_ApplyConfig_GenerateConfigFails(t *testing.T) {
|
||||
|
||||
// stub generateConfigFunc to always return error
|
||||
orig := generateConfigFunc
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
return nil, fmt.Errorf("generate fail")
|
||||
}
|
||||
defer func() { generateConfigFunc = orig }()
|
||||
@@ -582,7 +582,7 @@ func TestManager_ApplyConfig_PassesAdminWhitelistToGenerateConfig(t *testing.T)
|
||||
// Stub generateConfigFunc to capture adminWhitelist
|
||||
var capturedAdmin string
|
||||
orig := generateConfigFunc
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
capturedAdmin = adminWhitelist
|
||||
// return minimal config
|
||||
return &Config{Apps: Apps{HTTP: &HTTPApp{Servers: map[string]*Server{}}}}, nil
|
||||
@@ -633,7 +633,7 @@ func TestManager_ApplyConfig_PassesRuleSetsToGenerateConfig(t *testing.T) {
|
||||
|
||||
var capturedRules []models.SecurityRuleSet
|
||||
orig := generateConfigFunc
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
capturedRules = rulesets
|
||||
return &Config{Apps: Apps{HTTP: &HTTPApp{Servers: map[string]*Server{}}}}, nil
|
||||
}
|
||||
@@ -688,10 +688,10 @@ func TestManager_ApplyConfig_IncludesCorazaHandlerWithRuleset(t *testing.T) {
|
||||
var capturedWafEnabled bool
|
||||
var capturedRulesets []models.SecurityRuleSet
|
||||
origGen := generateConfigFunc
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
capturedWafEnabled = wafEnabled
|
||||
capturedRulesets = rulesets
|
||||
return origGen(hosts, storageDir, acmeEmail, frontendDir, sslProvider, acmeStaging, crowdsecEnabled, wafEnabled, rateLimitEnabled, aclEnabled, adminWhitelist, rulesets, decisions, secCfg)
|
||||
return origGen(hosts, storageDir, acmeEmail, frontendDir, sslProvider, acmeStaging, crowdsecEnabled, wafEnabled, rateLimitEnabled, aclEnabled, adminWhitelist, rulesets, rulesetPaths, decisions, secCfg)
|
||||
}
|
||||
defer func() { generateConfigFunc = origGen }()
|
||||
|
||||
@@ -720,11 +720,20 @@ func TestManager_ApplyConfig_IncludesCorazaHandlerWithRuleset(t *testing.T) {
|
||||
if handlerName, ok := handle["handler"].(string); ok && handlerName == "coraza" {
|
||||
// Validate ruleset fields
|
||||
if rsName, ok := handle["ruleset_name"].(string); ok && rsName == "owasp-crs" {
|
||||
// check for inlined content
|
||||
if rsContent, ok := handle["ruleset_content"].(string); ok && rsContent == "test-rule-content" {
|
||||
if mode, ok := handle["mode"].(string); ok && mode == "block" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
// check for written ruleset_path file, if present validate file content
|
||||
if rsPath, ok := handle["ruleset_path"].(string); ok && rsPath != "" {
|
||||
// Ensure file exists and contains our content
|
||||
b, err := os.ReadFile(rsPath)
|
||||
if err == nil && string(b) == "test-rule-content" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,6 +744,102 @@ func TestManager_ApplyConfig_IncludesCorazaHandlerWithRuleset(t *testing.T) {
|
||||
assert.True(t, found, "coraza handler with inlined ruleset should be present in generated config")
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_RulesetWriteFileFailure(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()+"rulesets-failwrite")
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}, &models.SecurityConfig{}, &models.SecurityRuleSet{}))
|
||||
|
||||
// Create host and ruleset
|
||||
h := models.ProxyHost{DomainNames: "rulesetw.example.com", ForwardHost: "127.0.0.1", ForwardPort: 8080, Enabled: true}
|
||||
db.Create(&h)
|
||||
rs := models.SecurityRuleSet{Name: "owasp-crs", Content: "test-rule-content"}
|
||||
assert.NoError(t, db.Create(&rs).Error)
|
||||
sec := models.SecurityConfig{Name: "default", Enabled: true, AdminWhitelist: "10.0.0.1/32", WAFMode: "block", WAFRulesSource: "owasp-crs"}
|
||||
assert.NoError(t, db.Create(&sec).Error)
|
||||
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/load" && r.Method == http.MethodPost {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/config/" && r.Method == http.MethodGet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{" + "\"apps\":{\"http\":{}}}"))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
|
||||
// Stub writeFileFunc to return an error for coraza ruleset files only to exercise the warn branch
|
||||
origWrite := writeFileFunc
|
||||
writeFileFunc = func(path string, b []byte, perm os.FileMode) error {
|
||||
if strings.Contains(path, string(filepath.Separator)+"coraza"+string(filepath.Separator)+"rulesets") {
|
||||
return fmt.Errorf("cannot write")
|
||||
}
|
||||
return origWrite(path, b, perm)
|
||||
}
|
||||
defer func() { writeFileFunc = origWrite }()
|
||||
|
||||
// Capture rulesetPaths from GenerateConfig
|
||||
var capturedPaths map[string]string
|
||||
origGen := generateConfigFunc
|
||||
generateConfigFunc = func(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string, acmeStaging bool, crowdsecEnabled bool, wafEnabled bool, rateLimitEnabled bool, aclEnabled bool, adminWhitelist string, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, decisions []models.SecurityDecision, secCfg *models.SecurityConfig) (*Config, error) {
|
||||
capturedPaths = rulesetPaths
|
||||
return origGen(hosts, storageDir, acmeEmail, frontendDir, sslProvider, acmeStaging, crowdsecEnabled, wafEnabled, rateLimitEnabled, aclEnabled, adminWhitelist, rulesets, rulesetPaths, decisions, secCfg)
|
||||
}
|
||||
defer func() { generateConfigFunc = origGen }()
|
||||
|
||||
manager := NewManager(client, db, tmp, "", false, config.SecurityConfig{CerberusEnabled: true, WAFMode: "block"})
|
||||
assert.NoError(t, manager.ApplyConfig(context.Background()))
|
||||
// writeFile failed, capturedPaths should not contain our ruleset entry
|
||||
assert.NotContains(t, capturedPaths, "owasp-crs")
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_RulesetDirMkdirFailure(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
// Create a file at tmp/coraza to cause MkdirAll on tmp/coraza/rulesets to fail
|
||||
corazaFile := filepath.Join(tmp, "coraza")
|
||||
os.WriteFile(corazaFile, []byte("not a dir"), 0644)
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()+"rulesets-mkdirfail")
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.Setting{}, &models.CaddyConfig{}, &models.SSLCertificate{}, &models.SecurityConfig{}, &models.SecurityRuleSet{}))
|
||||
|
||||
// Create host and ruleset
|
||||
h := models.ProxyHost{DomainNames: "rulesetm.example.com", ForwardHost: "127.0.0.1", ForwardPort: 8080, Enabled: true}
|
||||
db.Create(&h)
|
||||
rs := models.SecurityRuleSet{Name: "owasp-crs", Content: "test-rule-content"}
|
||||
assert.NoError(t, db.Create(&rs).Error)
|
||||
sec := models.SecurityConfig{Name: "default", Enabled: true, AdminWhitelist: "10.0.0.1/32", WAFMode: "block", WAFRulesSource: "owasp-crs"}
|
||||
assert.NoError(t, db.Create(&sec).Error)
|
||||
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/load" && r.Method == http.MethodPost {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/config/" && r.Method == http.MethodGet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{" + "\"apps\":{\"http\":{}}}"))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
// Use tmp as configDir and we already have a file at tmp/coraza which should make MkdirAll to create rulesets fail
|
||||
manager := NewManager(client, db, tmp, "", false, config.SecurityConfig{CerberusEnabled: true, WAFMode: "block"})
|
||||
// This should not error (failures to create coraza dir are warned only)
|
||||
assert.NoError(t, manager.ApplyConfig(context.Background()))
|
||||
}
|
||||
|
||||
func TestManager_ApplyConfig_ReappliesOnFlagChange(t *testing.T) {
|
||||
// Capture /load payloads
|
||||
loadCh := make(chan []byte, 10)
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestValidate_ValidConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil)
|
||||
config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, false, false, false, false, "", nil, nil, nil, nil)
|
||||
err := Validate(config)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -155,6 +155,16 @@ Charon follows a multi-layered security approach. The recommendation below shows
|
||||
|
||||
- **CrowdSec**: Best for dynamic, behavior-driven blocking — bots, scanners, credential stuffing, IP reputation. CrowdSec integrates with local or external agents and should be used for most bot and scanner detection/remediation.
|
||||
- **WAF (Coraza)**: Best for payload and application-level attacks (XSS, SQLi, file inclusion). Protects against malicious payloads regardless of source IP.
|
||||
|
||||
### Coraza runtime integration test
|
||||
|
||||
To validate runtime Coraza WAF integration locally using Docker Compose:
|
||||
|
||||
1. Build the local Docker image and start services: `docker build -t charon:local . && docker compose -f docker-compose.local.yml up -d`.
|
||||
2. Configure a ruleset via the API: POST to `/api/v1/security/rulesets` with a rule that would match an XSS payload.
|
||||
3. Send a request that triggers the rule (e.g., POST with `<script>` payload) and verify `403` or similar WAF-blocking response.
|
||||
|
||||
There is a lightweight helper script `scripts/coraza_integration.sh` which performs these steps and can be used as a starting point for CI integration tests.
|
||||
- **Rate Limiting**: Best for high-volume scanners and brute-force attempts; helps prevent abuse from cloud providers and scrapers.
|
||||
- **ACLs (Geo/Page-Level)**: Best for static location-based or private network restrictions, e.g., geo-blocking or restricting access to RFC1918 ranges for internal services.
|
||||
|
||||
|
||||
47
scripts/coraza_integration.sh
Normal file
47
scripts/coraza_integration.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Brief: Integration test for Coraza WAF using Docker Compose and built image
|
||||
# Steps:
|
||||
# 1. Build the local image: docker build -t charon:local .
|
||||
# 2. Start docker-compose.local.yml: docker compose -f docker-compose.local.yml up -d
|
||||
# 3. Wait for API to be ready and then configure a ruleset that blocks a simple signature
|
||||
# 4. Request a path containing the signature and verify 403 (or WAF block response)
|
||||
|
||||
echo "Starting Coraza integration test..."
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker is not available; aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker build -t charon:local .
|
||||
docker compose -f docker-compose.local.yml up -d
|
||||
|
||||
echo "Waiting for Charon API to be ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -s -f http://localhost:8080/api/v1/ >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
echo -n '.'
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Creating simple WAF ruleset (XSS block)..."
|
||||
RULESET='{"name":"integration-xss","content":"SecRule REQUEST_BODY \"<script>\" \"id:12345,phase:2,deny,status:403,msg:\'XSS blocked\'\""}'
|
||||
curl -s -X POST -H "Content-Type: application/json" -d "${RULESET}" http://localhost:8080/api/v1/security/rulesets
|
||||
|
||||
echo "Apply rules and test payload..."
|
||||
# create minimal proxy host if needed; omitted here for brevity; test will target local Caddy root
|
||||
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -d "<script>alert(1)</script>" http://localhost/)
|
||||
if [ "$RESPONSE" = "403" ]; then
|
||||
echo "Coraza WAF blocked payload as expected (HTTP 403)"
|
||||
else
|
||||
echo "Unexpected response code: $RESPONSE (expected 403)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Coraza integration test complete. Cleaning up..."
|
||||
docker compose -f docker-compose.local.yml down
|
||||
echo "Done"
|
||||
Reference in New Issue
Block a user