From d7e20b10b59d0075dc381dba0e94fed6f82fbd16 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:49:22 +0100 Subject: [PATCH] fix WAF: use directives array and Include for OWASP CRS, fix log parser field paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildWafHandler: directives must be string[] not a joined string (coraza-caddy JSON API requirement); load_owasp_crs is Caddyfile-only and silently ignored in JSON config — replaced with Include @owasp_crs/... directives - waf-log-parser: use unix_timestamp (nanoseconds) for precise ts; host header is headers.host[] (lowercase array); messages[].data.{id,msg,severity} not rule.* Co-Authored-By: Claude Sonnet 4.6 --- src/lib/caddy.ts | 25 ++++++++++++++---------- src/lib/waf-log-parser.ts | 40 +++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 32f78dd3..6ada8967 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -827,25 +827,30 @@ function resolveEffectiveWaf( } function buildWafHandler(waf: WafSettings): Record { - const directives = [ + // directives must be string[] — coraza-caddy JSON API does not accept a joined string. + // load_owasp_crs is a Caddyfile-only directive and is silently ignored in JSON config; + // CRS must be loaded via Include directives instead. + const directives: string[] = [ `SecRuleEngine ${waf.mode}`, 'SecAuditEngine On', 'SecAuditLog /logs/waf-audit.log', 'SecAuditLogFormat JSON', 'SecAuditLogParts ABIJDEFHZ', - waf.custom_directives, - ].filter(Boolean).join('\n'); - - const handler: Record = { - handler: 'waf', - directives, - }; + 'SecResponseBodyAccess Off', + ]; if (waf.load_owasp_crs) { - handler.load_owasp_crs = true; + directives.push( + 'Include @owasp_crs/crs-setup.conf.example', + 'Include @owasp_crs/rules/*.conf', + ); } - return handler; + if (waf.custom_directives?.trim()) { + directives.push(waf.custom_directives.trim()); + } + + return { handler: 'waf', directives }; } function buildBlockerHandler(config: GeoBlockSettings): Record { diff --git a/src/lib/waf-log-parser.ts b/src/lib/waf-log-parser.ts index d5ecdaf8..10bd60fa 100644 --- a/src/lib/waf-log-parser.ts +++ b/src/lib/waf-log-parser.ts @@ -57,21 +57,24 @@ function lookupCountry(ip: string): string | null { interface CorazaAuditEntry { transaction?: { client_ip?: string; + // unix_timestamp is nanoseconds since epoch + unix_timestamp?: number; + timestamp?: string; request?: { method?: string; uri?: string; - host?: string; + // headers values are arrays of strings (lowercase keys) + headers?: Record; }; - timestamp?: string; }; + // messages is absent/null when no rules match messages?: Array<{ - rule?: { - id?: number; + data?: { + id?: string | number; msg?: string; + severity?: string; }; - severity?: string; - data?: string; - }>; + }> | null; } function parseLine(line: string): typeof wafEvents.$inferInsert | null { @@ -89,16 +92,29 @@ function parseLine(line: string): typeof wafEvents.$inferInsert | null { if (!clientIp) return null; const req = tx.request ?? {}; - const ts = tx.timestamp ? Math.floor(new Date(tx.timestamp).getTime() / 1000) : Math.floor(Date.now() / 1000); + + // unix_timestamp is nanoseconds; fall back to parsing timestamp string + let ts: number; + if (tx.unix_timestamp) { + ts = Math.floor(tx.unix_timestamp / 1e9); + } else if (tx.timestamp) { + ts = Math.floor(new Date(tx.timestamp).getTime() / 1000); + } else { + ts = Math.floor(Date.now() / 1000); + } + + // Host header is an array under lowercase key + const hostArr = req.headers?.['host'] ?? req.headers?.['Host']; + const host = Array.isArray(hostArr) ? (hostArr[0] ?? '') : (hostArr ?? ''); const firstMsg = entry.messages?.[0]; - const ruleId = firstMsg?.rule?.id ?? null; - const ruleMessage = firstMsg?.rule?.msg ?? null; - const severity = firstMsg?.severity ?? null; + const ruleId = firstMsg?.data?.id != null ? Number(firstMsg.data.id) : null; + const ruleMessage = firstMsg?.data?.msg ?? null; + const severity = firstMsg?.data?.severity ?? null; return { ts, - host: req.host ?? '', + host, clientIp, countryCode: lookupCountry(clientIp), method: req.method ?? '',