Skip to content
Coraza reference adapter

Coraza reference adapter

Runs the conformance data in ../../tests against Coraza through its public API, as go test.

cd adapters/coraza
go test ./...            # everything
go test ./... -run TestEngine -v   # engine-tier profiles, one subtest per file
go test ./... -run 'TestUnit$' -v  # unit-tier files

The Coraza version is pinned in go.mod (v3.8.1). Change it there to grade a different release; every other input is the repository’s test data.

How results are gated

compat/known-gaps.md is the expected-failure list. For each test file:

Row for Coraza in known-gapsResultOutcome
noneall passpass
noneany failurefail (a real defect in Coraza or in the specification)
presentany failurepass, failures logged as expected (known gap: …)
presentall passfail with known-gaps row is obsolete (remove the row)

So the build is green exactly when the table is accurate. A new divergence is handled by adding a row with a source pointer; a fixed one by deleting its row.

Extended features Coraza does not implement are skipped, not failed, as spec/00-conventions.md requires: engine profiles through requires:, unit files for the operators and transformations listed in conformance_test.go, and the rx-pcre-extensions cases.

How the tiers are driven

  • Engine tier (engine.go): one WAF per profile with the profile’s files: written under a temporary root; one transaction per stage through ProcessConnection, ProcessURI, headers, WriteRequestBody, ProcessRequestBody, the synthetic response when the stage has one, and ProcessLogging. triggered_rules come from MatchedRules(), interruption from Interruption(), log_contains from the error callback. An engine panic is reported as one failure instead of aborting the run.
  • Unit tier (unit.go): each operator or transformation case is wrapped in a rule over REQUEST_BODY, the input is sent as raw bytes (unit strings are Latin-1 byte strings, tests/README.md), and the result is read from the matched rule: whether it matched for operators, the transformed value for transformations, TX:0..9 for re_groups. Coraza treats \" as the only escape inside a quoted argument, so parameters are inserted with quotes escaped and nothing else (an Apache-based adapter would also have to double backslashes; see spec/01-lexical.md#quoting-and-escapes). Engine-tier data is UTF-8 text and is sent as such; unit-tier strings are Latin-1 byte strings and are sent byte for byte.

Differential run against the Lean model

cmd/differential drives Coraza with seeded random requests against every Core profile’s ruleset and writes what the engine did, in the JSON shape formal/EvalMain.lean reads, so the Lean model can be checked on inputs no hand-written profile anticipated:

cd adapters/coraza && go run ./cmd/differential -n 30 -seed 1 > ../../formal/.lake/differential.json
cd ../../formal && lake exe seclang-eval .lake/differential.json      # exit 1 on any mismatch

Profiles with requires:, with a Coraza row in compat/known-gaps.md, or with expect_error or log assertions are skipped, and the generator never produces a value the specification leaves unspecified for the profile’s operators (digits followed by text for the numeric operators, a body of exactly SecRequestBodyLimit bytes). A mismatch is classified like any adapter disagreement: a spec or model defect is fixed, an engine divergence gets a Core profile and a known-gaps row (which then excludes the profile from the run). CI runs seed 1 with 30 requests per profile; the first run found ADR-0030, the REQBODY_PROCESSOR clarification and the body-limit boundary gap. With -profile FILE the generator runs one external profile instead: CI feeds it OWASP CRS as assembled by tools/crs_profile.py (20 requests, seed 1), so the model is checked against Coraza on some seven hundred real rules.

Adding another engine

Copy the shape: load the data (profile.go is engine-neutral), drive the engine, map observations to the same Observed/UnitResult structs, reuse CheckStage/CheckUnit and LoadGaps with your engine’s name. ADR-0023 records that reference adapters live here; an engine may equally keep its adapter in its own repository. adapters/libmodsecurity is the second reference adapter (Python, ctypes over the C API); it shares no code with this module but follows the same gating.