feat(waf): log why Coraza acted (wrule/wrules/wscore); VERSION 2026.10.4
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 3m14s

coraza-spoa v0.7.1 already returns the interrupting rule id
(txn.coraza.ruleid) and, when the request message sends
exportRuleIDs=bool(true), the comma list of every matched CRS attack
rule (txn.coraza.rule_ids) plus the inbound anomaly score. Send the arg
and append all three to the access log so a block's cause (e.g.
920440,949110 score 5) is visible in haproxy.log without opening the
Coraza audit log. Fields are appended last; existing ones unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-16 08:20:46 -07:00
1 parent 519f2d8ade
commit cbbd15b787
4 files changed
+33 -4

No files matched your search

+15 -1
View File
@@ -160,7 +160,16 @@ class ListenerWithCoraza(unittest.TestCase):
def test_access_log_carries_coverage_fields(self):
lf = [l for l in self.lines if l.startswith('log-format ')]
self.assertEqual(len(lf), 1)
self.assertTrue(lf[0].endswith('wafb=%[var(txn.waf_body)] wafe=%[var(txn.coraza.error)]"'))
self.assertIn('wafb=%[var(txn.waf_body)] wafe=%[var(txn.coraza.error)]', lf[0])
def test_access_log_carries_waf_verdict_fields(self):
"""wrule/wrules/wscore are appended LAST so any consumer anchored on
the earlier fields keeps working."""
lf = [l for l in self.lines if l.startswith('log-format ')]
self.assertEqual(len(lf), 1)
self.assertTrue(lf[0].endswith(
'wrule=%[var(txn.coraza.ruleid)] wrules=%[var(txn.coraza.rule_ids)] '
'wscore=%[var(txn.coraza.anomaly_score)]"'))
def test_fail_open_preserved(self):
"""Nothing may deny on var(txn.coraza.error): an SPOA outage must not
@@ -247,6 +256,11 @@ class CorazaTransactionIdIsTheRequestReference(unittest.TestCase):
self.assertIn('coraza-req', self.messages)
self.assertEqual(self.args_of('coraza-req').get('id'), 'unique-id')
def test_request_message_exports_rule_ids(self):
"""Without exportRuleIDs coraza-spoa never sets txn.coraza.rule_ids
and the access log's wrules= is always '-'."""
self.assertEqual(self.args_of('coraza-req').get('exportRuleIDs'), 'bool(true)')
def test_app_is_still_the_first_arg(self):
"""agent.go HandleSPOE Panics unless the FIRST kv entry is `app`."""
for name, args in self.messages.items():