feat: parse WAF rule logs for rule ID/message/severity

Coraza does not write matched rules to the audit log (known upstream
bug). Rule details are logged by Caddy's http.handlers.waf logger.

Two changes:

1. caddy.ts: Always configure a dedicated Caddy log sink that writes
   http.handlers.waf logger output to /logs/waf-rules.log as JSON.

2. waf-log-parser.ts: Before parsing the audit log, read the new
   waf-rules.log to build a Map<unique_id, RuleInfo>. Each audit log
   entry joins against this map via transaction.id to populate
   ruleId, ruleMessage, and severity fields. Skips anomaly evaluation
   rules (949110/980130) to show the actual detection rule instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-03-04 08:21:49 +01:00
parent 5e7ceedfe3
commit 54a2a9ea0d
2 changed files with 112 additions and 63 deletions
+20 -20
View File
@@ -1662,26 +1662,26 @@ async function buildCaddyDocument() {
const httpApp = Object.keys(servers).length > 0 ? { http: { servers } } : {};
// Build logging configuration if enabled
const loggingApp = loggingEnabled
? {
logging: {
logs: {
http_access: {
writer: {
output: "file",
filename: "/logs/access.log",
mode: "0640"
},
encoder: {
format: loggingFormat
},
include: ["http.log.access"]
}
}
}
}
: {};
// Build logging configuration
const loggingLogs: Record<string, unknown> = {
// Always capture WAF rule match logs so the waf-log-parser can extract rule details.
// Coraza does not write matched rules to the audit log (known bug), but it does emit
// structured JSON lines via the http.handlers.waf logger for each matched rule.
waf_rules: {
writer: { output: "file", filename: "/logs/waf-rules.log", mode: "0640" },
encoder: { format: "json" },
include: ["http.handlers.waf"],
level: "ERROR"
}
};
if (loggingEnabled) {
loggingLogs.http_access = {
writer: { output: "file", filename: "/logs/access.log", mode: "0640" },
encoder: { format: loggingFormat },
include: ["http.log.access"]
};
}
const loggingApp = { logging: { logs: loggingLogs } };
return {
admin: {