From b6d353c5af38b142161e54e2e23d96135f77c471 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 19:39:24 +0000
Subject: [PATCH 01/38] fix(deps): update npm minor/patch to ^19.2.1
---
frontend/package-lock.json | 55 +++++++++++++++++++++++---------------
frontend/package.json | 4 +--
2 files changed, 35 insertions(+), 24 deletions(-)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b3f744ff..93540d6d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,8 +13,8 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.555.0",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
"react-hook-form": "^7.67.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.10.0",
@@ -151,6 +151,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -506,6 +507,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -547,6 +549,7 @@
"url": "https://opencollective.com/csstools"
}
],
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2408,8 +2411,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -2497,6 +2499,7 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2507,6 +2510,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2547,6 +2551,7 @@
"integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/types": "8.48.1",
@@ -2927,6 +2932,7 @@
"integrity": "sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/utils": "4.0.15",
"fflate": "^0.8.2",
@@ -2962,6 +2968,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3179,6 +3186,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -3367,7 +3375,8 @@
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "peer": true
},
"node_modules/data-urls": {
"version": "6.0.0",
@@ -3450,8 +3459,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
@@ -3613,6 +3621,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4501,6 +4510,7 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz",
"integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==",
"dev": true,
+ "peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.23",
"@asamuzakjp/dom-selector": "^6.7.4",
@@ -4947,7 +4957,6 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
- "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -5369,6 +5378,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -5398,7 +5408,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -5413,7 +5422,6 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -5423,7 +5431,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -5467,24 +5474,26 @@
"license": "MIT"
},
"node_modules/react": {
- "version": "19.2.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
- "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
+ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.2.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
- "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.2.0"
+ "react": "^19.2.1"
}
},
"node_modules/react-hook-form": {
@@ -5524,8 +5533,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "dev": true,
- "peer": true
+ "dev": true
},
"node_modules/react-refresh": {
"version": "0.18.0",
@@ -6013,6 +6021,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6050,8 +6059,7 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
@@ -6098,6 +6106,7 @@
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6173,6 +6182,7 @@
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.0.15",
"@vitest/mocker": "4.0.15",
@@ -6410,6 +6420,7 @@
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/frontend/package.json b/frontend/package.json
index 09cfde3f..99bf4d19 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -31,8 +31,8 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.555.0",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
"react-hook-form": "^7.67.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.10.0",
From 85a15f82995908c07fcb78f6cf08a02e826e1d56 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Wed, 3 Dec 2025 20:16:42 +0000
Subject: [PATCH 02/38] fix: resolve CI failures (WAF integration, Trivy
vulnerabilities)
---
backend/go.mod | 2 +-
backend/go.sum | 4 ++--
backend/internal/api/handlers/security_handler.go | 12 ++++++++++++
backend/internal/caddy/config.go | 4 ++--
backend/internal/caddy/manager.go | 7 ++++++-
5 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/backend/go.mod b/backend/go.mod
index d0527391..53db9249 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -68,7 +68,7 @@ require (
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
- github.com/quic-go/quic-go v0.54.0 // indirect
+ github.com/quic-go/quic-go v0.54.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
diff --git a/backend/go.sum b/backend/go.sum
index dab0c891..516d18a8 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -143,8 +143,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
-github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
+github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
+github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
diff --git a/backend/internal/api/handlers/security_handler.go b/backend/internal/api/handlers/security_handler.go
index f053e2ea..2b82697a 100644
--- a/backend/internal/api/handlers/security_handler.go
+++ b/backend/internal/api/handlers/security_handler.go
@@ -329,6 +329,12 @@ func (h *SecurityHandler) Enable(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to enable Cerberus"})
return
}
+ if h.caddyManager != nil {
+ if err := h.caddyManager.ApplyConfig(c.Request.Context()); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()})
+ return
+ }
+ }
c.JSON(http.StatusOK, gin.H{"enabled": true})
}
@@ -348,6 +354,9 @@ func (h *SecurityHandler) Disable(c *gin.Context) {
cfg.Enabled = false
}
_ = h.svc.Upsert(cfg)
+ if h.caddyManager != nil {
+ _ = h.caddyManager.ApplyConfig(c.Request.Context())
+ }
c.JSON(http.StatusOK, gin.H{"enabled": false})
return
}
@@ -367,5 +376,8 @@ func (h *SecurityHandler) Disable(c *gin.Context) {
}
cfg.Enabled = false
_ = h.svc.Upsert(cfg)
+ if h.caddyManager != nil {
+ _ = h.caddyManager.ApplyConfig(c.Request.Context())
+ }
c.JSON(http.StatusOK, gin.H{"enabled": false})
}
diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go
index 8cbb6122..12421e2f 100644
--- a/backend/internal/caddy/config.go
+++ b/backend/internal/caddy/config.go
@@ -754,14 +754,14 @@ func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet,
if selected != nil {
if rulesetPaths != nil {
if p, ok := rulesetPaths[selected.Name]; ok && p != "" {
- h["include"] = []string{p}
+ h["directives"] = fmt.Sprintf("Include %s", p)
}
}
} else if secCfg != nil && secCfg.WAFRulesSource != "" {
// If there was a requested ruleset name but nothing matched, include path if known
if rulesetPaths != nil {
if p, ok := rulesetPaths[secCfg.WAFRulesSource]; ok && p != "" {
- h["include"] = []string{p}
+ h["directives"] = fmt.Sprintf("Include %s", p)
}
}
}
diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go
index c401aa23..4d9956ff 100644
--- a/backend/internal/caddy/manager.go
+++ b/backend/internal/caddy/manager.go
@@ -118,7 +118,12 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
// sanitize name to a safe filename
safeName := strings.ReplaceAll(strings.ToLower(rs.Name), " ", "-")
safeName = strings.ReplaceAll(safeName, "/", "-")
- filePath := filepath.Join(corazaDir, safeName+".conf")
+
+ // Calculate hash of the content to ensure filename changes when content changes
+ // This forces Caddy to reload the file instead of using a cached version
+ hash := sha256.Sum256([]byte(rs.Content))
+ shortHash := fmt.Sprintf("%x", hash)[:8]
+ filePath := filepath.Join(corazaDir, fmt.Sprintf("%s-%s.conf", safeName, shortHash))
// Prepend required Coraza directives if not already present.
// These are essential for the WAF to actually enforce rules:
// - SecRuleEngine On: enables blocking mode (blocks malicious requests)
From f21377c83a4cb9fbf77877d9b51eaed8d9cae597 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Wed, 3 Dec 2025 20:10:20 +0000
Subject: [PATCH 03/38] fix: resolve CI failures (WAF integration, Trivy
vulnerabilities)
---
.github/agents/DevOps.agent.md | 36 ++++++++++++++++------------------
1 file changed, 17 insertions(+), 19 deletions(-)
diff --git a/.github/agents/DevOps.agent.md b/.github/agents/DevOps.agent.md
index 55cb2cd7..4e0ca575 100644
--- a/.github/agents/DevOps.agent.md
+++ b/.github/agents/DevOps.agent.md
@@ -1,4 +1,4 @@
-name: CI_Ops
+name: Dev_Ops
description: DevOps specialist that debugs GitHub Actions, CI pipelines, and Docker builds.
argument-hint: The workflow issue (e.g., "Why did the last build fail?" or "Fix the Docker push error")
tools: ['run_terminal_command', 'read_file', 'write_file', 'search', 'list_dir']
@@ -21,12 +21,17 @@ You do not guess why a build failed. You interrogate the server to find the exac
- **Locate Artifact**: If the log mentions a specific file (e.g., `backend/handlers/proxy.go:45`), note it down.
2. **Triage Decision Matrix (CRITICAL)**:
- - **Case A: Infrastructure Failure** (YAML syntax, Docker build args, missing secrets, script permission denied).
+ - **Check File Extension**: Look at the file causing the error.
+ - Is it `.yml`, `.yaml`, `.Dockerfile`, `.sh`? -> **Case A (Infrastructure)**.
+ - Is it `.go`, `.ts`, `.tsx`, `.js`, `.json`? -> **Case B (Application)**.
+
+ - **Case A: Infrastructure Failure**:
- **Action**: YOU fix this. Edit the workflow or Dockerfile directly.
- **Verify**: Commit, push, and watch the run.
- - **Case B: Application Failure** (Compilation error, Test failure, Lint error).
- - **Action**: STOP. Do not touch the code.
- - **Output**: Generate a **Bug Report** (see format below) for the Developer Agent.
+
+ - **Case B: Application Failure**:
+ - **Action**: STOP. You are strictly forbidden from editing application code.
+ - **Output**: Generate a **Bug Report** using the format below.
3. **Remediation (If Case A)**:
- Edit the `.github/workflows/*.yml` or `Dockerfile`.
@@ -42,23 +47,16 @@ You do not guess why a build failed. You interrogate the server to find the exac
**Error Log**:
```text
{paste the specific error lines here}
-Recommendation: @{Backend_Dev or Frontend_Dev}, please fix this logic error.
```
+
+Recommendation: @{Backend_Dev or Frontend_Dev}, please fix this logic error.
+
+
+STAY IN YOUR LANE: Do not edit .go, .tsx, or .ts files to fix logic errors. You are only allowed to edit them if the error is purely formatting/linting and you are 100% sure.
+
NO ZIP DOWNLOADS: Do not try to download artifacts or log zips. Use gh run view to stream text.
LOG EFFICIENCY: Never ask to "read the whole log" if it is >50 lines. Use grep to filter.
-ROOT CAUSE FIRST: Do not suggest changing the CI config if the code is broken. Fix the code, not the messenger.
-
-
-### The Workflow in Action
-
-Now, your troubleshooting flow is perfectly circular:
-
-1. **You:** "@CI\_Ops Why did the build fail?"
-2. **CI\_Ops:** "It's a Go test failure." (Generates `## 🐛 CI Failure Report`)
-3. **You:** "@Backend\_Dev Fix the bug in the report above."
-4. **Backend\_Dev:** Reads the report, runs the specific test (Red), fixes the code (Green).
-5. **You:** "@CI\_Ops Check the build again."
-6. **CI\_Ops:** "Build is Green."
+ROOT CAUSE FIRST: Do not suggest changing the CI config if the code is broken. Generate a report so the Developer can fix the code.
From 727b02701e94dffb1a97b5ed0a8f9e98007b2c25 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 21:08:00 +0000
Subject: [PATCH 04/38] chore(deps): update alpine docker tag to v3.23
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index c9ae6e71..e9e0b780 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,7 +18,7 @@ ARG CADDY_VERSION=2.10.2
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
## upstream caddy image tags while still shipping a pinned caddy binary.
-ARG CADDY_IMAGE=alpine:3.22
+ARG CADDY_IMAGE=alpine:3.23
# ---- Cross-Compilation Helpers ----
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0 AS xx
From 58d570ee1d0ea85cabe96e45127b36ea0e366d47 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Wed, 3 Dec 2025 23:05:09 +0000
Subject: [PATCH 05/38] fix: update WAF handler tests for directives format and
fix hash calculation
- Change test assertions from checking 'include' array to 'directives' string
- Fix advanced_config array case to use 'directives' instead of 'include'
- Calculate ruleset hash from final content (after SecRuleEngine prepend)
- Update filename pattern matching in tests for hashed filenames
- Ensures WAF mode changes result in different ruleset filenames
---
.../api/handlers/certificate_handler_test.go | 8 ++--
.../api/handlers/proxy_host_handler_test.go | 44 +++++++++----------
backend/internal/caddy/config.go | 8 ++--
.../caddy/config_generate_additional_test.go | 37 +++++++++-------
backend/internal/caddy/manager.go | 12 ++---
.../internal/caddy/manager_additional_test.go | 26 +++++++----
backend/internal/caddy/manager_test.go | 18 ++++----
7 files changed, 84 insertions(+), 69 deletions(-)
diff --git a/backend/internal/api/handlers/certificate_handler_test.go b/backend/internal/api/handlers/certificate_handler_test.go
index edf1f637..4b5f6e55 100644
--- a/backend/internal/api/handlers/certificate_handler_test.go
+++ b/backend/internal/api/handlers/certificate_handler_test.go
@@ -420,10 +420,10 @@ func generateSelfSignedCertPEM() (string, string, error) {
Subject: pkix.Name{
Organization: []string{"Test Org"},
},
- NotBefore: time.Now().Add(-time.Hour),
- NotAfter: time.Now().Add(24 * time.Hour),
- KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(24 * time.Hour),
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
diff --git a/backend/internal/api/handlers/proxy_host_handler_test.go b/backend/internal/api/handlers/proxy_host_handler_test.go
index 6c047dc9..a5510d52 100644
--- a/backend/internal/api/handlers/proxy_host_handler_test.go
+++ b/backend/internal/api/handlers/proxy_host_handler_test.go
@@ -296,12 +296,12 @@ func TestProxyHostCreate_AdvancedConfig_Normalization(t *testing.T) {
// Provide an advanced_config value that will be normalized by caddy.NormalizeAdvancedConfig
adv := `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`
payload := map[string]interface{}{
- "name": "AdvHost",
- "domain_names": "adv.example.com",
- "forward_scheme": "http",
- "forward_host": "localhost",
- "forward_port": 8080,
- "enabled": true,
+ "name": "AdvHost",
+ "domain_names": "adv.example.com",
+ "forward_scheme": "http",
+ "forward_host": "localhost",
+ "forward_port": 8080,
+ "enabled": true,
"advanced_config": adv,
}
bodyBytes, _ := json.Marshal(payload)
@@ -657,14 +657,14 @@ func TestProxyHostUpdate_AdvancedConfig_ClearAndBackup(t *testing.T) {
// Create host with advanced config
host := &models.ProxyHost{
- UUID: "adv-clear-uuid",
- Name: "Advanced Host",
- DomainNames: "adv-clear.example.com",
- ForwardHost: "localhost",
- ForwardPort: 8080,
- AdvancedConfig: `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`,
+ UUID: "adv-clear-uuid",
+ Name: "Advanced Host",
+ DomainNames: "adv-clear.example.com",
+ ForwardHost: "localhost",
+ ForwardPort: 8080,
+ AdvancedConfig: `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`,
AdvancedConfigBackup: "",
- Enabled: true,
+ Enabled: true,
}
require.NoError(t, db.Create(host).Error)
@@ -854,7 +854,7 @@ func TestProxyHostUpdate_Locations_Replace(t *testing.T) {
ForwardHost: "localhost",
ForwardPort: 8080,
Enabled: true,
- Locations: []models.Location{{UUID: uuid.NewString(), Path: "/old", ForwardHost: "localhost", ForwardPort: 8080, ForwardScheme: "http"}},
+ Locations: []models.Location{{UUID: uuid.NewString(), Path: "/old", ForwardHost: "localhost", ForwardPort: 8080, ForwardScheme: "http"}},
}
require.NoError(t, db.Create(host).Error)
@@ -884,14 +884,14 @@ func TestProxyHostCreate_WithCertificateAndLocations(t *testing.T) {
adv := `{"handler":"headers","response":{"set":{"X-Test":"1"}}}`
payload := map[string]interface{}{
- "name": "Create With Cert",
- "domain_names": "cert.example.com",
- "forward_scheme": "http",
- "forward_host": "localhost",
- "forward_port": 8080,
- "enabled": true,
- "certificate_id": cert.ID,
- "locations": []map[string]interface{}{{"path": "/app", "forward_scheme": "http", "forward_host": "localhost", "forward_port": 8080}},
+ "name": "Create With Cert",
+ "domain_names": "cert.example.com",
+ "forward_scheme": "http",
+ "forward_host": "localhost",
+ "forward_port": 8080,
+ "enabled": true,
+ "certificate_id": cert.ID,
+ "locations": []map[string]interface{}{{"path": "/app", "forward_scheme": "http", "forward_host": "localhost", "forward_port": 8080}},
"advanced_config": adv,
}
body, _ := json.Marshal(payload)
diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go
index 12421e2f..3ebb0f07 100644
--- a/backend/internal/caddy/config.go
+++ b/backend/internal/caddy/config.go
@@ -328,13 +328,13 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
// Ensure it has a "handler" key
if _, ok := v["handler"]; ok {
// Capture ruleset_name if present, remove it from advanced_config,
- // and set up 'include' array for coraza-caddy plugin.
+ // and set up 'directives' with Include statement for coraza-caddy plugin.
if rn, has := v["ruleset_name"]; has {
if rnStr, ok := rn.(string); ok && rnStr != "" {
- // Set 'include' array with the ruleset file path for coraza-caddy
+ // Set 'directives' with Include statement for coraza-caddy
if rulesetPaths != nil {
if p, ok := rulesetPaths[rnStr]; ok && p != "" {
- v["include"] = []string{p}
+ v["directives"] = fmt.Sprintf("Include %s", p)
}
}
}
@@ -352,7 +352,7 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin
if rnStr, ok := rn.(string); ok && rnStr != "" {
if rulesetPaths != nil {
if p, ok := rulesetPaths[rnStr]; ok && p != "" {
- m["include"] = []string{p}
+ m["directives"] = fmt.Sprintf("Include %s", p)
}
}
}
diff --git a/backend/internal/caddy/config_generate_additional_test.go b/backend/internal/caddy/config_generate_additional_test.go
index abe337fe..b78023c2 100644
--- a/backend/internal/caddy/config_generate_additional_test.go
+++ b/backend/internal/caddy/config_generate_additional_test.go
@@ -168,17 +168,17 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
sec := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "nonexistent-rs"}
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, waf handler should include a reference but no include array
+ // Since a ruleset name was requested but none exists, waf handler should include a reference but no directives
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if _, ok := h["include"]; !ok {
+ if _, ok := h["directives"]; !ok {
found = true
}
}
}
- require.True(t, found, "expected waf handler without include array when referenced ruleset does not exist")
+ require.True(t, found, "expected waf handler without directives when referenced ruleset does not exist")
// Now test learning/monitor mode mapping
sec2 := &models.SecurityConfig{WAFMode: "block", WAFLearning: true}
@@ -218,13 +218,13 @@ func TestGenerateConfig_WAFSelectedSetsContentAndMode(t *testing.T) {
found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if incl, ok := h["include"].([]string); ok && len(incl) > 0 {
+ if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include") {
found = true
break
}
}
}
- require.True(t, found, "expected waf handler with include array to be present")
+ require.True(t, found, "expected waf handler with directives containing Include to be present")
}
func TestGenerateConfig_DecisionAdminPartsEmpty(t *testing.T) {
@@ -271,11 +271,11 @@ func TestGenerateConfig_WAFUsesRuleSet(t *testing.T) {
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, rulesetPaths, nil, nil)
require.NoError(t, err)
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
- // check waf handler present with include array
+ // check waf handler present with directives containing Include
found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if incl, ok := h["include"].([]string); ok && len(incl) > 0 {
+ if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include") {
found = true
break
}
@@ -283,7 +283,7 @@ func TestGenerateConfig_WAFUsesRuleSet(t *testing.T) {
}
if !found {
b2, _ := json.MarshalIndent(route.Handle, "", " ")
- t.Fatalf("waf handler with include array should be present; handlers: %s", string(b2))
+ t.Fatalf("waf handler with directives should be present; handlers: %s", string(b2))
}
}
@@ -295,17 +295,17 @@ func TestGenerateConfig_WAFUsesRuleSetFromAdvancedConfig(t *testing.T) {
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, rulesetPaths, nil, nil)
require.NoError(t, err)
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
- // check waf handler present with include array coming from host AdvancedConfig
+ // check waf handler present with directives containing Include from host AdvancedConfig
found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if incl, ok := h["include"].([]string); ok && len(incl) > 0 && incl[0] == "/tmp/host-rs.conf" {
+ if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include /tmp/host-rs.conf") {
found = true
break
}
}
}
- require.True(t, found, "waf handler with include array should include host advanced_config ruleset path")
+ require.True(t, found, "waf handler with directives should include host advanced_config ruleset path")
}
func TestGenerateConfig_WAFUsesRuleSetFromAdvancedConfig_Array(t *testing.T) {
@@ -316,17 +316,20 @@ func TestGenerateConfig_WAFUsesRuleSetFromAdvancedConfig_Array(t *testing.T) {
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", []models.SecurityRuleSet{rs}, rulesetPaths, nil, nil)
require.NoError(t, err)
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
- // check waf handler present with include array coming from host AdvancedConfig array
+ // check waf handler present with directives containing Include from host AdvancedConfig array
found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if incl, ok := h["include"].([]string); ok && len(incl) > 0 && incl[0] == "/tmp/host-rs-array.conf" {
+ if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include /tmp/host-rs-array.conf") {
found = true
break
}
}
}
- require.True(t, found, "waf handler with include array should include host advanced_config array ruleset path")
+ if !found {
+ b, _ := json.MarshalIndent(route.Handle, "", " ")
+ t.Fatalf("waf handler with directives should include host advanced_config array ruleset path; handlers: %s", string(b))
+ }
}
func TestGenerateConfig_WAFUsesRulesetFromSecCfgFallback(t *testing.T) {
@@ -336,18 +339,18 @@ func TestGenerateConfig_WAFUsesRulesetFromSecCfgFallback(t *testing.T) {
rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp-fallback.conf"}
cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, rulesetPaths, nil, sec)
require.NoError(t, err)
- // since secCfg requested owasp-crs and we have a path, the waf handler should include the path in include array
+ // since secCfg requested owasp-crs and we have a path, the waf handler should include the path in directives
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if incl, ok := h["include"].([]string); ok && len(incl) > 0 && incl[0] == "/tmp/owasp-fallback.conf" {
+ if dir, ok := h["directives"].(string); ok && strings.Contains(dir, "Include /tmp/owasp-fallback.conf") {
found = true
break
}
}
}
- require.True(t, found, "waf handler with include array should include fallback secCfg ruleset path")
+ require.True(t, found, "waf handler with directives should include fallback secCfg ruleset path")
}
func TestGenerateConfig_RateLimitFromSecCfg(t *testing.T) {
diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go
index 4d9956ff..cbe94672 100644
--- a/backend/internal/caddy/manager.go
+++ b/backend/internal/caddy/manager.go
@@ -119,11 +119,6 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
safeName := strings.ReplaceAll(strings.ToLower(rs.Name), " ", "-")
safeName = strings.ReplaceAll(safeName, "/", "-")
- // Calculate hash of the content to ensure filename changes when content changes
- // This forces Caddy to reload the file instead of using a cached version
- hash := sha256.Sum256([]byte(rs.Content))
- shortHash := fmt.Sprintf("%x", hash)[:8]
- filePath := filepath.Join(corazaDir, fmt.Sprintf("%s-%s.conf", safeName, shortHash))
// Prepend required Coraza directives if not already present.
// These are essential for the WAF to actually enforce rules:
// - SecRuleEngine On: enables blocking mode (blocks malicious requests)
@@ -142,6 +137,13 @@ func (m *Manager) ApplyConfig(ctx context.Context) error {
}
content = fmt.Sprintf("SecRuleEngine %s\nSecRequestBodyAccess On\n\n", engineMode) + content
}
+
+ // Calculate hash of the FINAL content (after prepending mode directives)
+ // to ensure filename changes when mode changes, forcing Caddy to reload
+ hash := sha256.Sum256([]byte(content))
+ shortHash := fmt.Sprintf("%x", hash)[:8]
+ filePath := filepath.Join(corazaDir, fmt.Sprintf("%s-%s.conf", safeName, shortHash))
+
// Write ruleset file with world-readable permissions so the Caddy
// process (which may run as an unprivileged user) can read it.
if err := writeFileFunc(filePath, []byte(content), 0644); err != nil {
diff --git a/backend/internal/caddy/manager_additional_test.go b/backend/internal/caddy/manager_additional_test.go
index b92dbc50..db0d10b8 100644
--- a/backend/internal/caddy/manager_additional_test.go
+++ b/backend/internal/caddy/manager_additional_test.go
@@ -718,9 +718,12 @@ func TestManager_ApplyConfig_IncludesWAFHandlerWithRuleset(t *testing.T) {
if h == "ruleset.example.com" {
for _, handle := range r.Handle {
if handlerName, ok := handle["handler"].(string); ok && handlerName == "waf" {
- // Validate include array (coraza-caddy schema) or inline ruleset_content presence
- if incl, ok := handle["include"].([]interface{}); ok && len(incl) > 0 {
- if rf, ok := incl[0].(string); ok && rf != "" {
+ // Validate directives field contains Include statement (coraza-caddy schema)
+ if dir, ok := handle["directives"].(string); ok && strings.Contains(dir, "Include") {
+ // Extract the file path from the Include directive
+ parts := strings.Split(dir, " ")
+ if len(parts) >= 2 {
+ rf := parts[len(parts)-1]
// Ensure file exists and contains our content
// Note: manager prepends SecRuleEngine On directives, so we check Contains
b, err := os.ReadFile(rf)
@@ -739,7 +742,7 @@ func TestManager_ApplyConfig_IncludesWAFHandlerWithRuleset(t *testing.T) {
}
}
}
- assert.True(t, found, "waf handler with inlined ruleset should be present in generated config")
+ assert.True(t, found, "waf handler with directives should be present in generated config")
}
func TestManager_ApplyConfig_RulesetWriteFileFailure(t *testing.T) {
@@ -1310,10 +1313,17 @@ func TestManager_ApplyConfig_RulesetFileCleanup(t *testing.T) {
assert.NoError(t, err, "Subdirectory should not be deleted")
assert.True(t, info.IsDir(), "Subdirectory should still be a directory")
- // Verify active ruleset file exists
- activeFile := filepath.Join(corazaDir, "active-ruleset.conf")
- _, err = os.Stat(activeFile)
- assert.NoError(t, err, "Active ruleset file should exist")
+ // Verify active ruleset file exists (with hash suffix in filename)
+ entries, err := os.ReadDir(corazaDir)
+ assert.NoError(t, err, "Should be able to read corazaDir")
+ foundActive := false
+ for _, entry := range entries {
+ if !entry.IsDir() && strings.HasPrefix(entry.Name(), "active-ruleset-") && strings.HasSuffix(entry.Name(), ".conf") {
+ foundActive = true
+ break
+ }
+ }
+ assert.True(t, foundActive, "Active ruleset file with hash suffix should exist")
}
func TestManager_ApplyConfig_RulesetCleanupReadDirError(t *testing.T) {
diff --git a/backend/internal/caddy/manager_test.go b/backend/internal/caddy/manager_test.go
index 7f765e8e..2a5ba64a 100644
--- a/backend/internal/caddy/manager_test.go
+++ b/backend/internal/caddy/manager_test.go
@@ -462,20 +462,20 @@ func TestComputeEffectiveFlags_DB_ACLTrueAndFalse(t *testing.T) {
}
func TestComputeEffectiveFlags_DB_WAFMonitor(t *testing.T) {
-dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
-db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
-require.NoError(t, err)
+ dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
+ db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
+ require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{}))
-secCfg := config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"}
-manager := NewManager(nil, db, "", "", false, secCfg)
+ secCfg := config.SecurityConfig{CerberusEnabled: true, WAFMode: "enabled"}
+ manager := NewManager(nil, db, "", "", false, secCfg)
-// Set WAF mode to monitor
+ // Set WAF mode to monitor
res := db.Create(&models.SecurityConfig{Name: "default", Enabled: true, WAFMode: "monitor"})
require.NoError(t, res.Error)
-_, _, waf, _, _ := manager.computeEffectiveFlags(context.Background())
-require.True(t, waf) // Should still be true (enabled)
+ _, _, waf, _, _ := manager.computeEffectiveFlags(context.Background())
+ require.True(t, waf) // Should still be true (enabled)
}
func TestManager_ApplyConfig_WAFMonitor(t *testing.T) {
@@ -511,7 +511,7 @@ func TestManager_ApplyConfig_WAFMonitor(t *testing.T) {
originalWriteFile := writeFileFunc
defer func() { writeFileFunc = originalWriteFile }()
writeFileFunc = func(filename string, data []byte, perm os.FileMode) error {
- if strings.Contains(filename, "owasp-crs.conf") {
+ if strings.Contains(filename, "owasp-crs") && strings.HasSuffix(filename, ".conf") {
writtenContent = string(data)
}
return originalWriteFile(filename, data, perm)
From 8f120715776d12ce81dc4c256ea434a7e0023ab7 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 23:09:41 +0000
Subject: [PATCH 06/38] fix(deps): update dependency react-hook-form to ^7.68.0
---
frontend/frontend/package-lock.json | 8 ++++----
frontend/frontend/package.json | 2 +-
frontend/package-lock.json | 8 ++++----
frontend/package.json | 2 +-
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/frontend/frontend/package-lock.json b/frontend/frontend/package-lock.json
index 483fda51..3b6c2029 100644
--- a/frontend/frontend/package-lock.json
+++ b/frontend/frontend/package-lock.json
@@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
- "react-hook-form": "^7.67.0"
+ "react-hook-form": "^7.68.0"
}
},
"node_modules/react": {
@@ -19,9 +19,9 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.67.0",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.67.0.tgz",
- "integrity": "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==",
+ "version": "7.68.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
+ "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
diff --git a/frontend/frontend/package.json b/frontend/frontend/package.json
index 6f02d091..2860157a 100644
--- a/frontend/frontend/package.json
+++ b/frontend/frontend/package.json
@@ -1,5 +1,5 @@
{
"dependencies": {
- "react-hook-form": "^7.67.0"
+ "react-hook-form": "^7.68.0"
}
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 93540d6d..1bc251c8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,7 +15,7 @@
"lucide-react": "^0.555.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
- "react-hook-form": "^7.67.0",
+ "react-hook-form": "^7.68.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.10.0",
"tailwind-merge": "^3.4.0",
@@ -5497,9 +5497,9 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.67.0",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.67.0.tgz",
- "integrity": "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ==",
+ "version": "7.68.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
+ "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
diff --git a/frontend/package.json b/frontend/package.json
index 99bf4d19..3a48ced6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -33,7 +33,7 @@
"lucide-react": "^0.555.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
- "react-hook-form": "^7.67.0",
+ "react-hook-form": "^7.68.0",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^7.10.0",
"tailwind-merge": "^3.4.0",
From 0795fcf10c0b09cc3fd159b38fef8ca1d960f795 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Wed, 3 Dec 2025 23:23:19 +0000
Subject: [PATCH 07/38] fix: update integration test to use hashed ruleset
filenames
- Use glob pattern for ruleset file inspection (integration-xss-*.conf)
- Increase wait time for monitor mode config application from 2s to 5s
- Aligns with manager.go hash-based filename generation
---
scripts/coraza_integration.sh | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/scripts/coraza_integration.sh b/scripts/coraza_integration.sh
index 921633df..9e1735bd 100644
--- a/scripts/coraza_integration.sh
+++ b/scripts/coraza_integration.sh
@@ -107,7 +107,7 @@ curl -s http://localhost:2019/config | grep -n "waf" || true
curl -s http://localhost:2019/config | grep -n "integration-xss" || true
echo "Inspecting ruleset file inside container..."
-docker exec charon-debug cat /app/data/caddy/coraza/rulesets/integration-xss.conf || true
+docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/integration-xss-*.conf' || true
echo "Recent caddy logs (may contain plugin errors):"
docker logs charon-debug | tail -n 200 || true
@@ -127,10 +127,10 @@ SEC_CFG_MONITOR='{"name":"default","enabled":true,"waf_mode":"monitor","waf_rule
curl -s -X POST -H "Content-Type: application/json" -d "${SEC_CFG_MONITOR}" -b ${TMP_COOKIE} http://localhost:8080/api/v1/security/config
echo "Wait for Caddy to apply monitor mode config..."
-sleep 2
+sleep 5
echo "Inspecting ruleset file (should now have DetectionOnly)..."
-docker exec charon-debug cat /app/data/caddy/coraza/rulesets/integration-xss.conf | head -5 || true
+docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/integration-xss-*.conf | head -5' || true
RESPONSE_MONITOR=$(curl -s -o /dev/null -w "%{http_code}" -d "" -H "Host: integration.local" http://localhost/post)
if [ "$RESPONSE_MONITOR" = "200" ]; then
From 80934670e17efd960448e01b893d67d93e2a039c Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Wed, 3 Dec 2025 23:49:58 +0000
Subject: [PATCH 08/38] fix: trigger Caddy reload when security config changes
- Add ApplyConfig call in UpdateConfig handler after saving to DB
- This ensures WAF mode changes (block/monitor) regenerate rulesets
- Add nil guard for caddyManager in tests
---
backend/internal/api/handlers/security_handler.go | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/backend/internal/api/handlers/security_handler.go b/backend/internal/api/handlers/security_handler.go
index 2b82697a..44a9adab 100644
--- a/backend/internal/api/handlers/security_handler.go
+++ b/backend/internal/api/handlers/security_handler.go
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
+ log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/caddy"
@@ -135,6 +136,12 @@ func (h *SecurityHandler) UpdateConfig(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
+ // Apply updated config to Caddy so WAF mode changes take effect
+ if h.caddyManager != nil {
+ if err := h.caddyManager.ApplyConfig(c.Request.Context()); err != nil {
+ log.WithError(err).Warn("failed to apply security config changes to Caddy")
+ }
+ }
c.JSON(http.StatusOK, gin.H{"config": payload})
}
From 2adf094f1c311f2694c5d607c8a1f1ddf0b5e183 Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Thu, 4 Dec 2025 04:04:37 +0000
Subject: [PATCH 09/38] feat: Implement comprehensive tests and fixes for
Coraza WAF integration
- Add unit tests for WAF ruleset selection priority and handler validation in config_waf_test.go.
- Enhance manager.go to sanitize ruleset names, preventing path traversal vulnerabilities.
- Introduce debug logging for WAF configuration state in manager.go to aid troubleshooting.
- Create integration tests to verify WAF handler presence and ruleset sanitization in manager_additional_test.go.
- Update coraza_integration.sh to include verification steps for WAF configuration and improved error handling.
- Document the Coraza WAF integration fix plan, detailing root cause analysis and implementation tasks.
---
.github/workflows/waf-integration.yml | 29 ++
backend/internal/caddy/config.go | 64 ++-
backend/internal/caddy/config_extra_test.go | 5 +-
.../caddy/config_generate_additional_test.go | 22 +-
.../caddy/config_waf_security_test.go | 283 ++++++++++++
backend/internal/caddy/config_waf_test.go | 292 +++++++++++++
backend/internal/caddy/manager.go | 29 +-
.../internal/caddy/manager_additional_test.go | 68 +++
docs/plans/CORAZA_WAF_FIX_PLAN.md | 405 ++++++++++++++++++
scripts/coraza_integration.sh | 118 ++++-
10 files changed, 1281 insertions(+), 34 deletions(-)
create mode 100644 backend/internal/caddy/config_waf_security_test.go
create mode 100644 backend/internal/caddy/config_waf_test.go
create mode 100644 docs/plans/CORAZA_WAF_FIX_PLAN.md
diff --git a/.github/workflows/waf-integration.yml b/.github/workflows/waf-integration.yml
index b5cd3ae3..ac325622 100644
--- a/.github/workflows/waf-integration.yml
+++ b/.github/workflows/waf-integration.yml
@@ -45,6 +45,35 @@ jobs:
scripts/coraza_integration.sh 2>&1 | tee waf-test-output.txt
exit ${PIPESTATUS[0]}
+ - name: Dump Debug Info on Failure
+ if: failure()
+ run: |
+ echo "## 🔍 Debug Information" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ echo "### Container Status" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ docker ps -a --filter "name=charon" --filter "name=coraza" >> $GITHUB_STEP_SUMMARY 2>&1 || true
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ echo "### Caddy Admin Config" >> $GITHUB_STEP_SUMMARY
+ echo '```json' >> $GITHUB_STEP_SUMMARY
+ curl -s http://localhost:2019/config 2>/dev/null | head -200 >> $GITHUB_STEP_SUMMARY || echo "Could not retrieve Caddy config" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ echo "### Charon Container Logs (last 100 lines)" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ docker logs charon-debug 2>&1 | tail -100 >> $GITHUB_STEP_SUMMARY || echo "No container logs available" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ echo "### WAF Ruleset Files" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ docker exec charon-debug sh -c 'ls -la /app/data/caddy/coraza/rulesets/ 2>/dev/null && echo "---" && cat /app/data/caddy/coraza/rulesets/*.conf 2>/dev/null' >> $GITHUB_STEP_SUMMARY || echo "No ruleset files found" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+
- name: WAF Integration Summary
if: always()
run: |
diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go
index 3ebb0f07..6cdb1775 100644
--- a/backend/internal/caddy/config.go
+++ b/backend/internal/caddy/config.go
@@ -721,10 +721,19 @@ func buildCrowdSecHandler(host *models.ProxyHost, secCfg *models.SecurityConfig,
return h, nil
}
-// 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.
+// buildWAFHandler returns a WAF handler (Coraza) configuration.
+// The coraza-caddy plugin registers as http.handlers.waf and expects:
+// - handler: "waf"
+// - directives: ModSecurity directive string including Include statements
func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet, rulesetPaths map[string]string, secCfg *models.SecurityConfig, wafEnabled bool) (Handler, error) {
+ // Early exit if WAF is disabled
+ if !wafEnabled {
+ return nil, nil
+ }
+ if secCfg != nil && secCfg.WAFMode == "disabled" {
+ return nil, nil
+ }
+
// If the host provided an advanced_config containing a 'ruleset_name', prefer that value
var hostRulesetName string
if host != nil && host.AdvancedConfig != "" {
@@ -738,23 +747,56 @@ func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet,
}
}
- // Find a ruleset to associate with WAF; prefer name match by host.Application, host.AdvancedConfig ruleset_name or default 'owasp-crs'
+ // Find a ruleset to associate with WAF
+ // Priority order:
+ // 1. Exact match to secCfg.WAFRulesSource (user's global choice)
+ // 2. Exact match to hostRulesetName (per-host advanced_config)
+ // 3. Match to host.Application (app-specific defaults)
+ // 4. Fallback to owasp-crs
var selected *models.SecurityRuleSet
+ var hostRulesetMatch, appMatch, owaspFallback *models.SecurityRuleSet
+
+ // First pass: find all potential matches
for i, r := range rulesets {
- if r.Name == "owasp-crs" || (host != nil && r.Name == host.Application) || (hostRulesetName != "" && r.Name == hostRulesetName) || (secCfg != nil && r.Name == secCfg.WAFRulesSource) {
+ // Priority 1: Global WAF rules source - highest priority, select immediately
+ if secCfg != nil && secCfg.WAFRulesSource != "" && r.Name == secCfg.WAFRulesSource {
selected = &rulesets[i]
break
}
+ // Priority 2: Per-host ruleset name from advanced_config
+ if hostRulesetName != "" && r.Name == hostRulesetName && hostRulesetMatch == nil {
+ hostRulesetMatch = &rulesets[i]
+ }
+ // Priority 3: Match by host application
+ if host != nil && r.Name == host.Application && appMatch == nil {
+ appMatch = &rulesets[i]
+ }
+ // Priority 4: Track owasp-crs as fallback
+ if r.Name == "owasp-crs" && owaspFallback == nil {
+ owaspFallback = &rulesets[i]
+ }
}
- if !wafEnabled {
- return nil, nil
+ // Second pass: select by priority if not already selected
+ if selected == nil {
+ if hostRulesetMatch != nil {
+ selected = hostRulesetMatch
+ } else if appMatch != nil {
+ selected = appMatch
+ } else if owaspFallback != nil {
+ selected = owaspFallback
+ }
}
+
+ // Build the handler with directives
h := Handler{"handler": "waf"}
+ directivesSet := false
+
if selected != nil {
if rulesetPaths != nil {
if p, ok := rulesetPaths[selected.Name]; ok && p != "" {
h["directives"] = fmt.Sprintf("Include %s", p)
+ directivesSet = true
}
}
} else if secCfg != nil && secCfg.WAFRulesSource != "" {
@@ -762,14 +804,16 @@ func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet,
if rulesetPaths != nil {
if p, ok := rulesetPaths[secCfg.WAFRulesSource]; ok && p != "" {
h["directives"] = fmt.Sprintf("Include %s", p)
+ directivesSet = true
}
}
}
- // WAF enablement is handled by the caller. Don't add a 'mode' field
- // here because the module expects a specific configuration schema.
- if secCfg != nil && secCfg.WAFMode == "disabled" {
+
+ // Bug fix: Don't return a WAF handler without directives - it creates a no-op WAF
+ if !directivesSet {
return nil, nil
}
+
return h, nil
}
diff --git a/backend/internal/caddy/config_extra_test.go b/backend/internal/caddy/config_extra_test.go
index e30b1412..62f6ef70 100644
--- a/backend/internal/caddy/config_extra_test.go
+++ b/backend/internal/caddy/config_extra_test.go
@@ -222,8 +222,11 @@ func TestGenerateConfig_SecurityPipeline_Order(t *testing.T) {
acl := models.AccessList{ID: 200, Name: "WL", Enabled: true, Type: "whitelist", IPRules: ipRules}
host := models.ProxyHost{UUID: "pipeline1", DomainNames: "pipe.example.com", Enabled: true, ForwardHost: "app", ForwardPort: 8080, AccessListID: &acl.ID, AccessList: &acl, HSTSEnabled: true, BlockExploits: true}
+ // Provide rulesets and paths so WAF handler is created with directives
+ rulesets := []models.SecurityRuleSet{{Name: "owasp-crs"}}
+ rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp.conf"}
secCfg := &models.SecurityConfig{CrowdSecMode: "local"}
- cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", nil, nil, nil, secCfg)
+ cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", rulesets, rulesetPaths, nil, secCfg)
require.NoError(t, err)
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
diff --git a/backend/internal/caddy/config_generate_additional_test.go b/backend/internal/caddy/config_generate_additional_test.go
index b78023c2..675070af 100644
--- a/backend/internal/caddy/config_generate_additional_test.go
+++ b/backend/internal/caddy/config_generate_additional_test.go
@@ -50,8 +50,11 @@ func TestGenerateConfig_SecurityPipeline_Order_Locations(t *testing.T) {
acl := models.AccessList{ID: 201, Name: "WL2", Enabled: true, Type: "whitelist", IPRules: ipRules}
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}}}
+ // Provide rulesets and paths so WAF handler is created with directives
+ rulesets := []models.SecurityRuleSet{{Name: "owasp-crs"}}
+ rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp.conf"}
sec := &models.SecurityConfig{CrowdSecMode: "local"}
- cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", nil, nil, nil, sec)
+ cfg, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, true, true, true, true, "", rulesets, rulesetPaths, nil, sec)
require.NoError(t, err)
server := cfg.Apps.HTTP.Servers["charon_server"]
@@ -168,21 +171,20 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
sec := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: "nonexistent-rs"}
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, waf handler should include a reference but no directives
+ // Since a ruleset name was requested but none exists, NO waf handler should be created
+ // (Bug fix: don't create a no-op WAF handler without directives)
route := cfg.Apps.HTTP.Servers["charon_server"].Routes[0]
- found := false
for _, h := range route.Handle {
if hn, ok := h["handler"].(string); ok && hn == "waf" {
- if _, ok := h["directives"]; !ok {
- found = true
- }
+ t.Fatalf("expected NO waf handler when referenced ruleset does not exist, but found: %v", h)
}
}
- require.True(t, found, "expected waf handler without directives when referenced ruleset does not exist")
- // Now test learning/monitor mode mapping
+ // Now test with valid ruleset - WAF handler should be created
+ rulesets := []models.SecurityRuleSet{{Name: "owasp-crs"}}
+ rulesetPaths := map[string]string{"owasp-crs": "/tmp/owasp.conf"}
sec2 := &models.SecurityConfig{WAFMode: "block", WAFLearning: true}
- cfg2, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", nil, nil, nil, sec2)
+ cfg2, err := GenerateConfig([]models.ProxyHost{host}, "/tmp/caddy-data", "", "", "", false, false, true, false, false, "", rulesets, rulesetPaths, nil, sec2)
require.NoError(t, err)
route2 := cfg2.Apps.HTTP.Servers["charon_server"].Routes[0]
monitorFound := false
@@ -191,7 +193,7 @@ func TestGenerateConfig_WAFModeAndRulesetReference(t *testing.T) {
monitorFound = true
}
}
- require.True(t, monitorFound, "expected waf handler when WAFLearning is true")
+ require.True(t, monitorFound, "expected waf handler when WAFLearning is true and ruleset exists")
}
func TestGenerateConfig_WAFModeDisabledSkipsHandler(t *testing.T) {
diff --git a/backend/internal/caddy/config_waf_security_test.go b/backend/internal/caddy/config_waf_security_test.go
new file mode 100644
index 00000000..842f7a95
--- /dev/null
+++ b/backend/internal/caddy/config_waf_security_test.go
@@ -0,0 +1,283 @@
+package caddy
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/Wikid82/charon/backend/internal/models"
+)
+
+// TestBuildWAFHandler_PathTraversalAttack tests path traversal attempts in ruleset names
+func TestBuildWAFHandler_PathTraversalAttack(t *testing.T) {
+ tests := []struct {
+ name string
+ rulesetName string
+ shouldMatch bool // Whether the ruleset should be found
+ description string
+ }{
+ {
+ name: "Path traversal in ruleset name",
+ rulesetName: "../../../etc/passwd",
+ shouldMatch: false,
+ description: "Ruleset with path traversal should not match any legitimate path",
+ },
+ {
+ name: "Null byte injection",
+ rulesetName: "rules\x00.conf",
+ shouldMatch: false,
+ description: "Ruleset with null bytes should not match",
+ },
+ {
+ name: "URL encoded traversal",
+ rulesetName: "..%2F..%2Fetc%2Fpasswd",
+ shouldMatch: false,
+ description: "URL encoded path traversal should not match",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ host := &models.ProxyHost{UUID: "test-host"}
+ rulesets := []models.SecurityRuleSet{{Name: tc.rulesetName}}
+ // Only provide paths for legitimate rulesets
+ rulesetPaths := map[string]string{
+ "owasp-crs": "/app/data/caddy/coraza/rulesets/owasp-crs.conf",
+ }
+ secCfg := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: tc.rulesetName}
+
+ handler, err := buildWAFHandler(host, rulesets, rulesetPaths, secCfg, true)
+ require.NoError(t, err)
+
+ if tc.shouldMatch {
+ require.NotNil(t, handler)
+ } else {
+ // Handler should be nil since no matching path exists
+ require.Nil(t, handler, tc.description)
+ }
+ })
+ }
+}
+
+// TestBuildWAFHandler_SQLInjectionInRulesetName tests SQL injection patterns in ruleset names
+func TestBuildWAFHandler_SQLInjectionInRulesetName(t *testing.T) {
+ sqlInjectionPatterns := []string{
+ "'; DROP TABLE rulesets; --",
+ "1' OR '1'='1",
+ "UNION SELECT * FROM users--",
+ "admin'/*",
+ }
+
+ for _, pattern := range sqlInjectionPatterns {
+ t.Run(pattern, func(t *testing.T) {
+ host := &models.ProxyHost{UUID: "test-host"}
+ // Create ruleset with malicious name but only provide path for safe ruleset
+ rulesets := []models.SecurityRuleSet{{Name: pattern}, {Name: "owasp-crs"}}
+ rulesetPaths := map[string]string{
+ "owasp-crs": "/app/data/caddy/coraza/rulesets/owasp-crs.conf",
+ }
+ secCfg := &models.SecurityConfig{WAFMode: "block", WAFRulesSource: pattern}
+
+ handler, err := buildWAFHandler(host, rulesets, rulesetPaths, secCfg, true)
+ require.NoError(t, err)
+ // Should return nil since the malicious name has no corresponding path
+ require.Nil(t, handler, "SQL injection pattern should not produce valid handler")
+ })
+ }
+}
+
+// TestBuildWAFHandler_XSSInAdvancedConfig tests XSS patterns in advanced_config JSON
+func TestBuildWAFHandler_XSSInAdvancedConfig(t *testing.T) {
+ xssPatterns := []string{
+ `{"ruleset_name":""}`,
+ `{"ruleset_name":""}`,
+ `{"ruleset_name":"javascript:alert(1)"}`,
+ `{"ruleset_name":"
+
-
Cerberus
-
-
The Guardian at the Gate.
-
-
-Ensure nothing passes without permission. Cerberus is a robust security suite featuring the Coraza WAF, deep CrowdSec integration, and granular rate-limiting. Always watching, always protecting.
-
@@ -24,89 +20,125 @@ Ensure nothing passes without permission. Cerberus is a robust security suite fe
---
-## ✨ Top Features
+## Why Charon?
-| Feature | Description |
-|---------|-------------|
-| 🔐 **Automatic HTTPS** | Free SSL certificates from Let's Encrypt, auto-renewed |
-| 🛡️ **Built-in Security** | CrowdSec integration, geo-blocking, IP access lists (optional, powered by Cerberus) |
-| ⚡ **Zero Downtime** | Hot-reload configuration without restarts |
-| 🐳 **Docker Discovery** | Auto-detect containers on local and remote Docker hosts |
-| 📊 **Uptime Monitoring** | Know when your services go down with smart notifications |
-| 🔍 **Health Checks** | Test connections before saving |
-| 📥 **Easy Import** | Bring your existing Caddy configs with one click |
-| 💾 **Backup & Restore** | Never lose your settings, export anytime |
-| 🌐 **WebSocket Support** | Perfect for real-time apps and chat services |
-| 🎨 **Beautiful Dark UI** | Modern interface that's easy on the eyes, works on any device |
+You want your apps accessible online. You don't want to become a networking expert first.
-**[See all features →](https://wikid82.github.io/charon/features)**
+**The problem:** Managing reverse proxies usually means editing config files, memorizing cryptic syntax, and hoping you didn't break everything.
+
+**Charon's answer:** A web interface where you click boxes and type domain names. That's it.
+
+- ✅ **Your blog** gets a green lock (HTTPS) automatically
+- ✅ **Your chat server** works without weird port numbers
+- ✅ **Your admin panel** blocks everyone except you
+- ✅ **Everything stays up** even when you make changes
---
-## 🚀 Quick Start
+## What Can It Do?
-```bash
+🔐 **Automatic HTTPS** — Free certificates that renew themselves
+🛡️ **Optional Security** — Block bad guys, bad countries, or bad behavior
+🐳 **Finds Docker Apps** — Sees your containers and sets them up instantly
+📥 **Imports Old Configs** — Bring your Caddy setup with you
+⚡ **No Downtime** — Changes happen instantly, no restarts needed
+🎨 **Dark Mode UI** — Easy on the eyes, works on phones
+
+**[See everything it can do →](https://wikid82.github.io/charon/features)**
+
+---
+
+## Quick Start
+
+### Docker Compose (Recommended)
+
+Save this as `docker-compose.yml`:
+
+```yaml
services:
charon:
image: ghcr.io/wikid82/charon:latest
container_name: charon
restart: unless-stopped
ports:
- - "80:80" # HTTP (Caddy proxy)
- - "443:443" # HTTPS (Caddy proxy)
- - "443:443/udp" # HTTP/3 (Caddy proxy)
- - "8080:8080" # Management UI (Charon)
- environment:
- - CHARON_ENV=production # New env var prefix (CHARON_). CPM_ values still supported.
- - TZ=UTC # Set timezone (e.g., America/New_York)
- - CHARON_HTTP_PORT=8080
- - CHARON_DB_PATH=/app/data/charon.db
- - CHARON_FRONTEND_DIR=/app/frontend/dist
- - CHARON_CADDY_ADMIN_API=http://localhost:2019
- - CHARON_CADDY_CONFIG_DIR=/app/data/caddy
- - CHARON_CADDY_BINARY=caddy
- - CHARON_IMPORT_CADDYFILE=/import/Caddyfile
- - CHARON_IMPORT_DIR=/app/data/imports
- # Security Services (Optional)
- #- CERBERUS_SECURITY_CROWDSEC_MODE=disabled # disabled, local, external
- #- CERBERUS_SECURITY_CROWDSEC_API_URL= # Required if mode is external
- #- CERBERUS_SECURITY_CROWDSEC_API_KEY= # Required if mode is external
- #- CERBERUS_SECURITY_WAF_MODE=disabled # disabled, enabled
- #- CERBERUS_SECURITY_RATELIMIT_ENABLED=false
- #- CERBERUS_SECURITY_ACL_ENABLED=false
- extra_hosts:
- - "host.docker.internal:host-gateway"
+ - "80:80"
+ - "443:443"
+ - "443:443/udp"
+ - "8080:8080"
volumes:
- - :/app/data
- - :/data
- - :/config
- - /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
- # Mount your existing Caddyfile for automatic import (optional)
- # - ./my-existing-Caddyfile:/import/Caddyfile:ro
- # - ./sites:/import/sites:ro # If your Caddyfile imports other files
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 40s
+ - ./charon-data:/app/data
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ environment:
+ - CHARON_ENV=production
```
-Open **http://localhost:8080** — that's it! 🎉
+Then run:
-**[Full documentation →](https://wikid82.github.io/charon/)**
+```bash
+docker-compose up -d
+```
+
+### Docker Run (One-Liner)
+
+```bash
+docker run -d \
+ --name charon \
+ -p 80:80 \
+ -p 443:443 \
+ -p 443:443/udp \
+ -p 8080:8080 \
+ -v ./charon-data:/app/data \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
+ -e CHARON_ENV=production \
+ ghcr.io/wikid82/charon:latest
+```
+
+### What Just Happened?
+
+1. Charon downloaded and started
+2. The web interface opened on port 8080
+3. Your websites will use ports 80 (HTTP) and 443 (HTTPS)
+
+**Open http://localhost:8080** and start adding your websites!
---
-## 💬 Community
+## Optional: Turn On Security
-- 🐛 **Found a bug?** [Open an issue](https://github.com/Wikid82/charon/issues)
-- 💡 **Have an idea?** [Start a discussion](https://github.com/Wikid82/charon/discussions)
-- 📋 **Roadmap** [View the project board](https://github.com/users/Wikid82/projects/7)
+Charon includes **Cerberus**, a security guard for your apps. It's turned off by default so it doesn't get in your way.
+
+When you're ready, add these lines to enable protection:
+
+```yaml
+environment:
+ - CERBERUS_SECURITY_WAF_MODE=monitor # Watch for attacks
+ - CERBERUS_SECURITY_CROWDSEC_MODE=local # Block bad IPs automatically
+```
+
+**Start with "monitor" mode** — it watches but doesn't block. Once you're comfortable, change `monitor` to `block`.
+
+**[Learn about security features →](https://wikid82.github.io/charon/security)**
+
+---
+
+## Getting Help
+
+**[📖 Full Documentation](https://wikid82.github.io/charon/)** — Everything explained simply
+**[🚀 5-Minute Guide](https://wikid82.github.io/charon/getting-started)** — Your first website up and running
+**[💬 Ask Questions](https://github.com/Wikid82/charon/discussions)** — Friendly community help
+**[🐛 Report Problems](https://github.com/Wikid82/charon/issues)** — Something broken? Let us know
+
+---
+
+## Contributing
+
+Want to help make Charon better? Check out [CONTRIBUTING.md](CONTRIBUTING.md)
+
+---
+
+## ✨ Top Features
-## 🤝 Contributing
-We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) to get started.
---
@@ -118,5 +150,5 @@ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) to get s