Audit logging
On this page 6
A finished transaction serializes into an audit-log record via
Transaction.serializeAuditLog(allocator, format). The call snapshots the
audit-relevant facts into an audit.AuditRecord and renders them in the
requested format, honoring the active SecAuditLogParts selection. All
intermediate state lives in a temporary arena; the returned bytes are owned by
the caller.
The snapshot
buildAuditRecord gathers, borrowing bytes that are stable for the
transaction's lifetime:
- Identity and connection:
unique_id, client/server address and port. - Request line:
REQUEST_METHOD,REQUEST_URI, and the HTTP version (theREQUEST_PROTOCOLvalue without itsHTTP/prefix). - Request and response headers, iterated from
REQUEST_HEADERS/RESPONSE_HEADERSwith the casing the connector supplied. - Request and response bodies (
REQUEST_BODY/RESPONSE_BODY) and the response status. - The wall-clock timestamp from the configured clock source.
is_interrupted— whether an intervention is pending.- Part-H messages: one per matched rule flagged
auditlog, formatted from the accumulated match intents in ModSecurity'sRuleMessage::loglayout (ModSecurity: Warning./Access denied with code N.followed by[id "…"] [msg "…"] [data "…"] [severity "…"] [tag "…"] … [unique_id "…"]).
Parts
Sections are gated by SecAuditLogParts (ctl:auditLogParts at runtime). A
(header) and Z (terminator) are always present; B request headers, C
request body, E response body, F response headers, and H trailer are
selectable, and the reserved D/G/I/J/K markers emit empty when chosen.
Formats
- Native serial — ModSecurity's legacy dashed-boundary format
(
--<boundary>-A--…--<boundary>-Z--), pinned totoOldAuditLogFormat, including the blank-line record terminator afterZ. - JSON — Coraza's structured schema (
{"transaction":{…},"messages":[…]}) with header objects whose repeated names group into arrays. - Legacy JSON — Coraza's flatter schema with
remote_address/local_addressnaming and header maps whose repeated values join with,into a single string. - OCSF — an OCSF v1.2.0 "Web Resources Activity" event (class 6004), allowed/denied by interruption, with HTTP request/response objects, endpoints, per-message enrichments, and observables, matching Coraza's OCSF formatter.
Header-map key order is nondeterministic in Go's encoding/json, so the JSON
family is schema-compatible with Coraza rather than byte-identical.
Delivery
Formatted records are delivered across an I/O-free Sink boundary the
connector owns (src/audit_writer.zig): SerialWriter appends each record and
a newline (serial file, stdout/stderr, or a syslog/HTTPS sink), CallbackWriter
hands each record to a callback, and concurrentEntry produces the ModSecurity
concurrent-layout per-record path (YYYYMMDD/YYYYMMDD-HHMM/YYYYMMDD-HHMMSS-<id>)
and combined-log index line. The core never blocks on I/O.
Redaction
The ModSecurity sanitise* actions mask sensitive data before serialization:
sanitizeRequestHeader:<name>/sanitizeResponseHeader:<name>replace the named header's value with a same-length run of*.sanitizeArg:<name>masks the value of a request-body argument in place (the bytes after the first=in the argument's rawkey=valuesegment).sanitizeMatchedmasks whichever variable the rule matched, dispatching to header or argument masking by the matched variable's collection.
Masking is fail-safe — it only ever masks more, never leaks — so it applies
directly rather than through the staged rollback batch. sanitizeMatchedBytes
(masking only the operator-matched byte sub-range) is not yet implemented.
Qualification evidence
- Unit tests in
src/audit.zigcover each format (serial byte-exactly; the JSON family by round-tripping throughstd.json), the UTC timestamp layout, part gating, and body suppression. - An engine test in
src/engine.zigdrives a disruptiveauditlogrule and asserts the serial and JSON serializations of the live transaction.
Implementation is tracked by WAF-29. Audit writers (serial/concurrent/syslog/HTTPS) and redaction, debug logs, and telemetry land in later issues.