zig-waf

API reference

On this page 924

Every declaration below is extracted from zig-waf's source, with the doc comments as written there. A declaration listed without prose is public but undocumented in the source.

Root

version

const version = "0.0.0-dev"

compatibility

const compatibility = @import("compatibility.zig")

action_config

const action_config = @import("action_config.zig")

action_evidence

const action_evidence = @import("action_evidence.zig")

action_fuzz

const action_fuzz = @import("action_fuzz.zig")

body_fuzz

const body_fuzz = @import("body_fuzz.zig")

api

const api = @import("api.zig")

authz

const authz = @import("authz.zig")

audit_fuzz

const audit_fuzz = @import("audit_fuzz.zig")

storage_fuzz

const storage_fuzz = @import("storage_fuzz.zig")

collections

const collections = @import("collections.zig")

debug_log

const debug_log = @import("debug_log.zig")

directives

const directives = @import("directives.zig")

trace

const trace = @import("trace.zig")

engine

const engine = @import("engine.zig")

flow_evidence

const flow_evidence = @import("flow_evidence.zig")

macros

const macros = @import("macros.zig")

metrics

const metrics = @import("metrics.zig")

operators

const operators = @import("operators.zig")

rule_eval

const rule_eval = @import("rule_eval.zig")

runtime_operator

const runtime_operator = @import("runtime_operator.zig")

phrase

const phrase = @import("phrase.zig")

ip_match

const ip_match = @import("ip_match.zig")

operator_fuzz

const operator_fuzz = @import("operator_fuzz.zig")

plan

const plan = @import("plan.zig")

plugin

const plugin = @import("plugin.zig")

plan_fuzz

const plan_fuzz = @import("plan_fuzz.zig")

rule_config

const rule_config = @import("rule_config.zig")

rule_config_evidence

const rule_config_evidence = @import("rule_config_evidence.zig")

remote_rules

const remote_rules = @import("remote_rules.zig")

request

const request = @import("request.zig")

request_buffer

const request_buffer = @import("request_buffer.zig")

multipart

const multipart = @import("multipart.zig")

audit

const audit = @import("audit.zig")

audit_writer

const audit_writer = @import("audit_writer.zig")

persistent

const persistent = @import("persistent.zig")

persistent_lmdb

const persistent_lmdb = @import("persistent_lmdb.zig")

selectors

const selectors = @import("selectors.zig")

seclang

const seclang = @import("seclang/root.zig")

variables

const variables = @import("variables.zig")

transformations

const transformations = @import("transformations.zig")

transformation_fuzz

const transformation_fuzz = @import("transformation_fuzz.zig")

Waf

const Waf = engine.Waf

Runtime

const Runtime = engine.Runtime

RetiredGeneration

const RetiredGeneration = engine.RetiredGeneration

Transaction

const Transaction = engine.Transaction

Intervention

const Intervention = engine.Intervention

Phase

const Phase = engine.Phase

Feature

const Feature = engine.Feature

FeatureSet

const FeatureSet = engine.FeatureSet

ClockSample

const ClockSample = engine.ClockSample

ClockSource

const ClockSource = engine.ClockSource

MatchContext

const MatchContext = engine.MatchContext

MatchedRule

const MatchedRule = engine.MatchedRule

CaptureRange

const CaptureRange = engine.CaptureRange

RuleProjection

const RuleProjection = engine.RuleProjection

LocalEffectOutcome

const LocalEffectOutcome = engine.LocalEffectOutcome

MatchIntentId

const MatchIntentId = engine.MatchIntentId

MatchIntent

const MatchIntent = engine.MatchIntent

FlowState

const FlowState = engine.FlowState

PhaseCursor

const PhaseCursor = engine.PhaseCursor

ControlState

const ControlState = engine.ControlState

RuleExclusion

const RuleExclusion = engine.RuleExclusion

TargetExclusion

const TargetExclusion = engine.TargetExclusion

RegexTargetExclusion

const RegexTargetExclusion = engine.RegexTargetExclusion

InterventionCapabilities

const InterventionCapabilities = engine.InterventionCapabilities

compatibility

source

const source = @embedFile("compatibility/features.json")

Status

const Status = enum

Matrix

const Matrix = struct

Baselines

const Baselines = struct

Feature

const Feature = struct

action_config

ParseError

const ParseError = error

AllowScope

const AllowScope = enum

EngineMode

const EngineMode = enum

AuditEngine

const AuditEngine = enum

AuditParts

const AuditParts = struct

has

fn has(self: AuditParts, part: u8) bool

ControlKind

const ControlKind = enum

BodyProcessor

const BodyProcessor = enum

canonicalName

fn canonicalName(self: BodyProcessor) []const u8

The uppercase name published in REQBODY_PROCESSOR / RES_BODY_PROCESSOR, matching the ModSecurity/Coraza processor identifiers.

Control

const Control = struct

IdRange

const IdRange = struct

contains

fn contains(self: IdRange, value: u64) bool

TargetControl

const TargetControl = struct

RuntimeTarget

const RuntimeTarget = union(enum)

Severity

const Severity = enum(u3)

Collection

const Collection = enum

parse

fn parse(value: []const u8) ?Collection

persistent

fn persistent(self: Collection) bool

Assignment

const Assignment = struct

SetVarOperation

const SetVarOperation = enum

SetVar

const SetVar = struct

PersistentBinding

const PersistentBinding = struct

Expiration

const Expiration = struct

Deprecation

const Deprecation = struct

parseSeverity

fn parseSeverity(value: []const u8) ParseError!Severity

parseQuality

fn parseQuality(value: []const u8) ParseError!u4

parseAssignment

fn parseAssignment(value: []const u8) ParseError!Assignment

parseSetVar

fn parseSetVar(value: []const u8) ParseError!SetVar

parseInitCollection

fn parseInitCollection(value: []const u8) ParseError!PersistentBinding

parseExpiration

fn parseExpiration(value: []const u8) ParseError!Expiration

parseDeprecation

fn parseDeprecation(value: []const u8) ParseError!Deprecation

parsePositiveU32

fn parsePositiveU32(value: []const u8) ParseError!u32

parseAllowScope

fn parseAllowScope(value: ?[]const u8) ParseError!AllowScope

parseStatus

fn parseStatus(value: []const u8) ParseError!u16

parseSkip

fn parseSkip(value: []const u8) ParseError!u32

parseEngineMode

fn parseEngineMode(value: []const u8) ParseError!EngineMode

parseAuditEngine

fn parseAuditEngine(value: []const u8) ParseError!AuditEngine

parseBoolean

fn parseBoolean(value: []const u8) ParseError!bool

parseControl

fn parseControl(value: []const u8) ParseError!Control

parseBodyProcessor

fn parseBodyProcessor(value: []const u8) ParseError!BodyProcessor

parsePositiveUsize

fn parsePositiveUsize(value: []const u8) ParseError!usize

parseIdRange

fn parseIdRange(value: []const u8) ParseError!IdRange

parseTargetControl

fn parseTargetControl(value: []const u8) ParseError!TargetControl

parseRuntimeTarget

fn parseRuntimeTarget(value: []const u8) ParseError!RuntimeTarget

applyAuditParts

fn applyAuditParts(base: AuditParts, value: []const u8) ParseError!AuditParts

action_evidence

json

const json = @embedFile("compatibility/evidence/non-disruptive-actions.json")

action_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void

body_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void

api

version_prefix

const version_prefix = "/api/v1"

The API version, in the path prefix. A breaking change to any operation below takes a new prefix rather than mutating one clients already call.

Method

const Method = enum

Route

const Route = struct

One operation: how it is addressed, what it is called, and the least privilege that may invoke it.

Authorization lives here rather than in a handler because it is the property most easily forgotten in one: a handler added without a check is a hole, while a route added without a requires does not compile.

routes

const routes = [_]Route

Every operation the control plane exposes. Ordered by resource, and exhaustive: a request that matches nothing here is a 404, never a fall-through to something that happens to be close.

Match

const Match = struct

What a matched request resolved to.

parameter

fn parameter(self: Match, index: usize) ?[]const u8

max_parameters

const max_parameters = 4

No route uses more than this; a pattern needing more would not compile against it.

match

fn match(method: Method, path: []const u8) ?Match

Resolve a method and path to an operation, or null when nothing matches.

A path segment matches a {name} placeholder only if it is non-empty: a request for /nodes//heartbeat names no node, and treating an empty segment as an identifier is how a request reaches a handler with nothing to act on.

authorize

fn authorize(route: *const Route, role: ?auth.Role) bool

Whether role may invoke route. A route with no requirement is open by design — health and login — and every other route is refused for an unauthenticated caller rather than defaulting to a role.

authz

Role

const Role = enum

What a console identity is allowed to do. A fixed set, mirrored by a CHECK constraint in the schema, so an unrecognized role cannot be stored and later be interpreted as one of these.

text

fn text(self: Role) [:0]const u8

parse

fn parse(text_value: []const u8) ?Role

can

fn can(self: Role, action: Action) bool

Whether this role may perform action. Authorization is a total function over an enumerated set of actions rather than a string comparison at each call site, so a new action has to be classified here to be permitted anywhere.

Action

const Action = enum

Everything authorization is asked about. Enumerated so Role.can is exhaustive: adding an action without deciding who may do it will not compile.

audit_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void

storage_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void

collections

Name

const Name = enum

secLangName

fn secLangName(self: Name) []const u8

parse

fn parse(input: []const u8) ?Name

keyPolicy

fn keyPolicy(self: Name) KeyPolicy

minimumAvailability

fn minimumAvailability(self: Name) variables.Availability

keysEqual

fn keysEqual(self: Name, first: []const u8, second: []const u8) bool

KeyPolicy

const KeyPolicy = enum

Limits

const Limits = struct

validate

fn validate(self: Limits) error

Source

const Source = struct

View

const View = struct

Value

const Value = struct

Key

const Key = struct

SelectorError

const SelectorError = error

Matcher

const Matcher = struct

matches

fn matches(self: Matcher, key: []const u8) SelectorError!bool

Selector

const Selector = union(enum)

Target

const Target = struct

StoreError

const StoreError = std.mem.Allocator.Error || error

Store

const Store = struct

init

fn init(allocator: std.mem.Allocator, limits: Limits) Store

add

fn add(

addPair

fn addPair(self: *Store, first_value: Value, second_value: Value) StoreError!void

Atomically append two related values, such as a header value and its corresponding *_NAMES entry.

addBatch

fn addBatch(self: *Store, values: []const Value) StoreError!void

replaceCollection

fn replaceCollection(self: *Store, collection: Name, values: []const Value) StoreError!void

Replace every active entry in one collection as a single logical commit. Validation and ownership allocation complete before any prior entry is deactivated, so malformed input, capacity failures, and OOM preserve the visible collection. Arena bytes allocated by a failed attempt remain charged to the physical byte limit.

replaceKeys

fn replaceKeys(self: *Store, collection: Name, keys: []const []const u8, values: []const Value) StoreError!void

Atomically replace a bounded set of map keys without disturbing other entries in the collection. Every staged value must name one of the replacement keys; keys omitted from values are cleared.

replaceKeyGroups

fn replaceKeyGroups(self: *Store, keys: []const Key, values: []const Value) StoreError!void

Atomically replace key sets spanning multiple collections. This is the commit primitive for a preflighted rule-effect batch.

select

fn select(self: *const Store, collection: Name, selector: Selector) Iterator

selectTarget

fn selectTarget(self: *const Store, target: Target, exclusions: []const Target) TargetIterator

countTarget

fn countTarget(self: *const Store, target: Target, exclusions: []const Target) SelectorError!usize

count

fn count(self: *const Store, collection: Name, selector: Selector) SelectorError!usize

first

fn first(self: *const Store, collection: Name, key: []const u8) ?View

firstAny

fn firstAny(self: *const Store, collection: Name) ?View

set

fn set(self: *Store, collection: Name, key: []const u8, value: []const u8, source: Source) StoreError!void

Replace a map-style key or create it. Superseded arena bytes remain charged to the physical allocation limit, preventing update churn from becoming unbounded hidden memory growth.

remove

fn remove(self: *Store, collection: Name, selector: Selector) SelectorError!usize

deinit

fn deinit(self: *Store) void

Iterator

const Iterator = struct

next

fn next(self: *Iterator) SelectorError!?View

TargetIterator

const TargetIterator = struct

next

fn next(self: *TargetIterator) SelectorError!?View

debug_log

Level

const Level = enum(u4)

Verbosity, matching SecDebugLogLevel's 0-9 range so a ModSecurity configuration means here what it means there.

The names carry the intent; the numbers are what the directive sets. A record is emitted when its level is at or below the configured one, so raising the level only ever adds output.

parse

fn parse(text: []const u8) ?Level

permits

fn permits(self: Level, candidate: Level) bool

Whether a record at candidate should be emitted when the configured level is self. Level none permits nothing, including errors: a configuration that asked for silence gets it.

Settings

const Settings = struct

What SecDebugLog and SecDebugLogLevel asked for.

active

fn active(self: Settings) bool

Whether anything is actually configured. A path with level 0, or a level with no path, produces no log; saying so lets a host report the half-configuration rather than write to a file nobody asked for or compute records nobody reads.

settingsFromPlan

fn settingsFromPlan(compiled: anytype) error

Read the debug-log settings out of a compiled plan.

An unparseable level is reported as error.InvalidLevel rather than defaulting: a configuration that says SecDebugLogLevel 12 is wrong, and quietly reading it as 9 or 0 would either flood the log or silence it, both without telling anyone.

Record

const Record = struct

One thing that happened.

Limits

const Limits = struct

Stats

const Stats = struct

What was truncated, so the reader knows the log is not the whole story.

Recorder

const Recorder = struct

A bounded, non-blocking sink. Not thread-safe: one per transaction, or one guarded by the host. Sharing an unguarded recorder between threads would corrupt it, which is why this says so rather than taking a lock nobody needs.

init

fn init(allocator: std.mem.Allocator, level: Level, limits: Limits) Recorder

deinit

fn deinit(self: *Recorder) void

permits

fn permits(self: *const Recorder, level: Level) bool

Whether anything at level would be kept. Callers check this before formatting, so a disabled log costs a comparison rather than a message nobody reads.

record

fn record(self: *Recorder, level: Level, phase: ?u3, rule_id: ?u32, message: []const u8) void

Record a message. Silently does nothing when the level is not permitted — that is the point of a level — and counts a drop when a limit is reached.

Allocation failure is counted as a drop rather than returned: a diagnostic that fails the request it is describing has inverted its purpose.

print

fn print(

Record a formatted message. Formatting happens into a stack buffer, so a message longer than it is truncated exactly as record would truncate it.

items

fn items(self: *const Recorder) []const Record

clear

fn clear(self: *Recorder) void

Drop everything, keeping the counters. The host calls this after draining, and the counters survive because "how much was dropped" is a property of the transaction, not of the current buffer contents.

render

fn render(self: *const Recorder, writer: *std.Io.Writer) std.Io.Writer.Error!void

Render as text, one record per line, ending with a summary line when anything was dropped or truncated. The summary is not optional decoration: without it a reader cannot tell a complete log from a clipped one.

directives

evidence_json

const evidence_json = @embedFile("compatibility/evidence/directive-union.json")

Id

const Id = enum(u8)

Schema

const Schema = enum

Repeatability

const Repeatability = enum

Capability

const Capability = enum(u6)

CapabilitySet

const CapabilitySet = struct

full

fn full() CapabilitySet

coreOnly

fn coreOnly() CapabilitySet

has

fn has(self: CapabilitySet, capability: Capability) bool

Presence

const Presence = struct

UpstreamSupport

const UpstreamSupport = enum

ImplementationState

const ImplementationState = enum

Entry

const Entry = struct

DiagnosticCode

const DiagnosticCode = enum

id

fn id(self: DiagnosticCode) []const u8

message

fn message(self: DiagnosticCode) []const u8

Diagnostic

const Diagnostic = struct

ValidationOutcome

const ValidationOutcome = union(enum)

ValidationLimits

const ValidationLimits = struct

valid

fn valid(self: ValidationLimits) bool

RuleEngine

const RuleEngine = enum

ConnectionEngine

const ConnectionEngine = enum

LimitAction

const LimitAction = enum

AuditEngine

const AuditEngine = enum

AuditLogType

const AuditLogType = enum

AuditLogFormat

const AuditLogFormat = enum

UploadKeepFiles

const UploadKeepFiles = enum

RemoteFailAction

const RemoteFailAction = enum

Fingerprint

const Fingerprint = [32]u8

Value

const Value = union(enum)

DecodedValue

const DecodedValue = struct

ConfigurationOutcome

const ConfigurationOutcome = union(enum)

Configuration

const Configuration = struct

Immutable typed view over a plan. The plan owner must outlive this value.

init

fn init(plan: *const plan_mod.Plan, capabilities: CapabilitySet) ConfigurationOutcome

latest

fn latest(self: *const Configuration, id: Id) ?DecodedDirective

Return the last source occurrence. This is the effective occurrence for singular replacement directives; append directives retain all entries.

occurrences

fn occurrences(self: *const Configuration, id: Id) OccurrenceIterator

DecodedDirective

const DecodedDirective = struct

values

fn values(self: DecodedDirective) ValueIterator

OccurrenceIterator

const OccurrenceIterator = struct

next

fn next(self: *OccurrenceIterator) ?DecodedDirective

ValueIterator

const ValueIterator = struct

next

fn next(self: *ValueIterator) ?DecodedValue

registry

const registry = [_]Entry

lookup

fn lookup(name: []const u8) ?*const Entry

get

fn get(id: Id) *const Entry

validatePlan

fn validatePlan(compiled: *const plan_mod.Plan, capabilities: CapabilitySet) ValidationOutcome

Validate the complete immutable plan before it can be published by a WAF. Values remain borrowed from the plan; diagnostics never interpolate input.

validatePlanWithLimits

fn validatePlanWithLimits(

trace

TraceId

const TraceId = [16]u8

A 16-byte trace identifier, shared by every span in one trace.

SpanId

const SpanId = [8]u8

An 8-byte span identifier, unique within a trace.

traceparent_len

const traceparent_len = 55

The length of a rendered traceparent: 00-<32 hex>-<16 hex>-<2 hex>.

Error

const Error = error

Context

const Context = struct

A trace context: which trace, which span within it, and whether it is sampled.

sampled

fn sampled(self: Context) bool

writeTraceparent

fn writeTraceparent(self: Context, out: *[traceparent_len]u8) void

Render as a traceparent header value.

child

fn child(self: Context, io: std.Io) Error!Context

A child span in the same trace: same trace id, a fresh span id, same flags.

Sampling is inherited rather than re-decided. A WAF that sampled independently would produce spans whose parents are missing, which is worse than not tracing: a trace with holes reads as a system that lost the request.

parseTraceparent

fn parseTraceparent(header: []const u8) Error!Context

Parse a traceparent header value.

Only version 00 is accepted. The specification says a future version's extra fields may be ignored, but a WAF has no reason to guess at a format it does not know: refusing means starting a fresh trace, which is correct and traceable, while guessing means emitting identifiers that may not mean what they appear to.

Resolution

const Resolution = struct

The context to use for a request: the incoming one when it is valid, otherwise a fresh trace.

inherited reports which happened. A caller needs to know: a span whose parent was discarded is a root span, and reporting it as a child would leave a dangling parent reference in the backend.

resolve

fn resolve(header: ?[]const u8, io: std.Io) Error!Resolution

Continue the incoming trace, or start a new one when the header is absent or unusable.

This is the function a connector calls. It never fails on bad input — a malformed header is a reason to start a new trace, not to fail the request, because a WAF that rejects traffic over a tracing header has turned observability into an outage.

newTrace

fn newTrace(io: std.Io) Error!Context

A brand-new trace with a fresh trace and span id.

engine

Mode

const Mode = enum

Feature

const Feature = enum(u6)

FeatureSet

const FeatureSet = struct

allCompiled

fn allCompiled() FeatureSet

has

fn has(self: FeatureSet, feature: Feature) bool

Phase

const Phase = enum(u8)

Limits

const Limits = struct

Config

const Config = struct

InterventionCapabilities

const InterventionCapabilities = struct

ClockSample

const ClockSample = struct

ClockSource

const ClockSource = struct

Optional deterministic clock provider for embedders and tests. The callback must be nonblocking and thread-safe, and its context must outlive the WAF.

now

fn now(self: ClockSource) ClockSample

ConfigError

const ConfigError = error

DeinitError

const DeinitError = error

Intervention

const Intervention = struct

Action

const Action = enum(u8)

Builder

const Builder = struct

Builder for a validated immutable Waf.

Ruleset compilation will be added behind this type. Publishing only occurs after complete validation, so callers never observe a partially compiled ruleset.

init

fn init(allocator: std.mem.Allocator) Builder

setIo

fn setIo(self: *Builder, io: std.Io) void

Override the clock backend. The application owns the backend and must keep it alive until the WAF and all of its transactions are destroyed.

setClockSource

fn setClockSource(self: *Builder, source: ClockSource) void

setMode

fn setMode(self: *Builder, mode: Mode) void

setLimits

fn setLimits(self: *Builder, limits: Limits) void

setDebugLog

fn setDebugLog(self: *Builder, recorder: *debug_log.Recorder) void

Attach a debug-log sink (#32). Borrowed: the caller keeps it alive for as long as the WAF and its transactions, and guards it if transactions run concurrently, since a recorder is not thread-safe.

setRuleTiming

fn setRuleTiming(self: *Builder, enabled: bool) void

Measure how long each rule takes. Off by default because it reads the clock twice per rule, which is a cost worth paying only when someone is looking.

setMacroMissingPolicy

fn setMacroMissingPolicy(self: *Builder, policy: macros.MissingPolicy) void

setPersistentBackend

fn setPersistentBackend(self: *Builder, backend: persistent.Backend) void

setPluginRegistry

fn setPluginRegistry(self: *Builder, registry: *const plugin.Registry) void

Register host-provided operators (#23). The registry is borrowed, so the caller keeps it alive for as long as the WAF and its transactions.

setPersistentLimits

fn setPersistentLimits(self: *Builder, limits: persistent.Limits) void

setPersistentFailurePolicy

fn setPersistentFailurePolicy(self: *Builder, policy: persistent.FailurePolicy) void

setPersistentRequiredFeatures

fn setPersistentRequiredFeatures(self: *Builder, required: persistent.BackendFeatureSet) void

setInterventionCapabilities

fn setInterventionCapabilities(self: *Builder, capabilities: InterventionCapabilities) void

setTransformationLimits

fn setTransformationLimits(self: *Builder, limits: transformations.Limits) void

setTransformationProfile

fn setTransformationProfile(self: *Builder, profile: transformations.Profile) void

setTransformationUnicodeMap

fn setTransformationUnicodeMap(self: *Builder, unicode_map: transformations.UnicodeMap) void

The immutable Unicode map remains owned by the caller and must outlive the WAF and all child transactions.

setTransformationCacheEnabled

fn setTransformationCacheEnabled(self: *Builder, enabled: bool) void

Override SecCacheTransformations for embedders. Null configuration uses the effective ruleset directive and otherwise defaults to disabled.

setDirectiveCapabilities

fn setDirectiveCapabilities(self: *Builder, capabilities: directives.CapabilitySet) void

Select the directive capabilities compiled into this WAF build. A plan using an omitted capability is rejected before publication.

setMissingRulePolicy

fn setMissingRulePolicy(self: *Builder, policy: rule_config.MissingRulePolicy) void

setRetainedPlan

fn setRetainedPlan(self: *Builder, value: *const compiled_plan.Plan) void

Retain the plan when build succeeds. The caller keeps ownership of its handle and only needs to keep it alive until build returns.

validateCompiledPlan

fn validateCompiledPlan(self: *const Builder, value: *const compiled_plan.Plan) directives.ValidationOutcome

Return the stable source-anchored diagnostic used by build without publishing or taking ownership of the plan.

validateExecutionPlan

fn validateExecutionPlan(self: *const Builder, value: *const compiled_plan.Plan) compiled_plan.ExecutionValidation

build

fn build(self: *const Builder) (ConfigError || std.mem.Allocator.Error)!*Waf

buildTransferringPlan

fn buildTransferringPlan(self: *const Builder, value: *compiled_plan.Plan) (ConfigError || std.mem.Allocator.Error)!*Waf

Transfer value to the new WAF only on success. On error, ownership remains with the caller.

buildRuntime

fn buildRuntime(self: *const Builder) (ConfigError || RuntimeInitError)!*Runtime

Waf

const Waf = struct

Immutable, thread-safe compiled WAF state.

Keep the pointer stable until every child transaction is deinitialized. deinit rejects an early destroy instead of allowing a dangling request.

Builder

const Builder = @import("engine.zig").Builder

newTransaction

fn newTransaction(self: *const Waf) Transaction

activeTransactionCount

fn activeTransactionCount(self: *const Waf) usize

features

fn features(_: *const Waf) FeatureSet

compiledPlan

fn compiledPlan(self: *const Waf) ?*const compiled_plan.Plan

metrics

fn metrics(self: *const Waf) metrics_mod.Snapshot

A coherent snapshot of the engine's request-path counters (#32), for a Prometheus scrape. Reading is lock-free and allocation-free, so exposing metrics never interferes with request handling.

directiveConfiguration

fn directiveConfiguration(self: *const Waf) ?*const directives.Configuration

deinit

fn deinit(self: *Waf) DeinitError!void

RuntimeError

const RuntimeError = error

RuntimeInitError

const RuntimeInitError = std.mem.Allocator.Error || error

RuntimeDeinitError

const RuntimeDeinitError = error

Runtime

const Runtime = struct

Stable owner for atomically published immutable WAF generations.

The mutex protects only generation pointer acquisition and replacement. Once returned, a transaction executes without touching the runtime lock and remains pinned to the generation from which it was created.

init

fn init(allocator: std.mem.Allocator, initial: *Waf) RuntimeInitError!*Runtime

newTransaction

fn newTransaction(self: *Runtime) RuntimeError!Transaction

reload

fn reload(self: *Runtime, replacement: *Waf) RuntimeError!RetiredGeneration

Publish replacement and transfer ownership of the prior generation to the returned retirement handle. A replacement with existing requests is rejected because its ownership is not exclusive.

activeTransactionCount

fn activeTransactionCount(self: *Runtime) RuntimeError!usize

deinit

fn deinit(self: *Runtime) RuntimeDeinitError!void

Destroy the runtime and its active generation. Callers must first stop creating transactions and separately reclaim every retired generation.

RetiredGeneration

const RetiredGeneration = struct

activeTransactionCount

fn activeTransactionCount(self: *const RetiredGeneration) usize

tryReclaim

fn tryReclaim(self: *RetiredGeneration) DeinitError!void

isReclaimed

fn isReclaimed(self: *const RetiredGeneration) bool

TransactionError

const TransactionError = error

ArgumentOrigin

const ArgumentOrigin = enum

PersistentInitialization

const PersistentInitialization = enum

CaptureRange

const CaptureRange = struct

MatchContext

const MatchContext = struct

Borrowed operator-match evidence. Capture indexes correspond directly to TX.0 through TX.9; null entries represent unmatched optional groups.

TransformedValue

const TransformedValue = struct

Borrowed transformed value and staged multi-match checkpoints. Executor storage remains valid until the transaction runs another transformation pipeline or is deinitialized. Source metadata continues to describe the original collection value rather than fabricated transformed offsets.

MatchedRule

const MatchedRule = struct

RuleProjection

const RuleProjection = struct

MatchIntentId

const MatchIntentId = enum(u32)

MatchIntent

const MatchIntent = struct

LocalEffectOutcome

const LocalEffectOutcome = struct

FlowState

const FlowState = struct

ControlState

const ControlState = struct

RuleExclusion

const RuleExclusion = union(enum)

TargetExclusion

const TargetExclusion = struct

RegexTargetExclusion

const RegexTargetExclusion = struct

PersistentFailure

const PersistentFailure = enum

Transaction

const Transaction = struct

Isolated per-request mutable state.

The initial implementation records only bounded metadata and body byte counts. Connector-controlled buffering and spooling are layered on this contract; write*Body never implies unbounded in-memory retention.

compiledPlan

fn compiledPlan(self: *const Transaction) ?*const compiled_plan.Plan

transformRuleValue

fn transformRuleValue(self: *Transaction, rule_id: compiled_plan.RuleId, context: MatchContext) TransactionError!TransformedValue

Execute the selected rule member's immutable transformation pipeline. No match, capture, action, or intervention state is changed here; a failure therefore exposes no partial operator-visible state.

transformationCacheStats

fn transformationCacheStats(self: *const Transaction) transformations.CacheStats

projectRuleMetadata

fn projectRuleMetadata(self: *Transaction, rule_id: compiled_plan.RuleId) TransactionError!RuleProjection

Atomically replace the transaction-local RULE projection with typed metadata from the chain head. Macro-bearing text remains in its compiled source form here so subsequent effects can resolve RULE.* before final event expansion.

replaceCaptures

fn replaceCaptures(self: *Transaction, context: MatchContext) TransactionError!usize

Validate all borrowed ranges first, then atomically replace TX.0–TX.9. Unmatched and omitted groups clear stale capture state.

applyLocalMatchedRule

fn applyLocalMatchedRule(

Preflight and atomically commit RULE metadata, captures, ENV writes, and TX setvar effects for one matched rule/member. Persistent effects are counted for the persistence commit stage and never substituted by local storage.

applyMatchedRule

fn applyMatchedRule(

Apply local and configured persistent non-disruptive effects. Backend mutations remain staged in the request session until the lifecycle flush boundary; all transaction-local projections commit together.

applyLocalMatchedChain

fn applyLocalMatchedChain(

Apply a fully matched chain as one ordered, atomic effect batch. The first entry must be the chain head and every compiled chain_next member must appear exactly once in order.

applyMatchedChain

fn applyMatchedChain(

processConnection

fn processConnection(

processUri

fn processUri(

addRequestHeader

fn addRequestHeader(self: *Transaction, name: []const u8, value: []const u8) TransactionError!void

processRequestHeaders

fn processRequestHeaders(self: *Transaction) TransactionError!void

writeRequestBody

fn writeRequestBody(self: *Transaction, chunk: []const u8) TransactionError!void

processRequestBody

fn processRequestBody(self: *Transaction) TransactionError!void

addResponseHeader

fn addResponseHeader(self: *Transaction, name: []const u8, value: []const u8) TransactionError!void

processResponseHeaders

fn processResponseHeaders(self: *Transaction, status: u16, protocol: []const u8) TransactionError!void

writeResponseBody

fn writeResponseBody(self: *Transaction, chunk: []const u8) TransactionError!void

processResponseBody

fn processResponseBody(self: *Transaction) TransactionError!void

processLogging

fn processLogging(self: *Transaction) TransactionError!void

configuredAuditFormat

fn configuredAuditFormat(self: *const Transaction) audit.Format

The audit-log format selected by SecAuditLogFormat, or the ModSecurity default (the native serial format) when the directive is absent. Lets a connector honor the configured format instead of hard-coding one.

serializeConfiguredAuditLog

fn serializeConfiguredAuditLog(self: *Transaction, allocator: std.mem.Allocator) TransactionError![]u8

Serialize the audit record in the SecAuditLogFormat-configured format.

serializeAuditLog

fn serializeAuditLog(self: *Transaction, allocator: std.mem.Allocator, format: audit.Format) TransactionError![]u8

Serialize this transaction's audit record in format, honoring the active SecAuditLogParts selection. The returned bytes are owned by the caller (freed with allocator); all intermediate snapshot state lives in a temporary arena.

sanitiseRequestHeader

fn sanitiseRequestHeader(self: *Transaction, name: []const u8) TransactionError!void

Register a request header whose value is masked in the audit log, implementing ModSecurity's sanitiseRequestHeader.

sanitiseResponseHeader

fn sanitiseResponseHeader(self: *Transaction, name: []const u8) TransactionError!void

Register a response header whose value is masked in the audit log, implementing ModSecurity's sanitiseResponseHeader.

sanitiseArg

fn sanitiseArg(self: *Transaction, name: []const u8) TransactionError!void

Register an argument whose value is masked in the audited request body, implementing ModSecurity's sanitiseArg.

RuleTiming

const RuleTiming = struct

How long rule evaluation took, and which rule was worst (#32).

measured is false when rule_timing was off, so a caller can tell "nothing was slow" from "nobody was timing" — reporting an unmeasured zero as a measurement is how a dashboard comes to show a WAF that costs nothing.

ruleTiming

fn ruleTiming(self: *const Transaction) RuleTiming

intervention

fn intervention(self: *const Transaction) TransactionError!?Intervention

recordIntervention

fn recordIntervention(

Execution-engine hook. Detection-only mode preserves the decision while marking it non-enforcing for connector policy.

currentPhase

fn currentPhase(self: *const Transaction) ?Phase

isPhaseInterrupted

fn isPhaseInterrupted(self: *const Transaction) bool

isTerminated

fn isTerminated(self: *const Transaction) bool

isLoggingFinalized

fn isLoggingFinalized(self: *const Transaction) bool

scalar

fn scalar(self: *Transaction, name: variables.Name) TransactionError!?variables.View

scalarBySecLangName

fn scalarBySecLangName(self: *Transaction, name: []const u8) TransactionError!?variables.View

collection

fn collection(self: *const Transaction, name: collections.Name, selector: collections.Selector) TransactionError!?collections.Iterator

traceContext

fn traceContext(self: *Transaction) TransactionError!trace.Resolution

The trace this transaction belongs to (#32): the incoming W3C trace context continued with a span of the WAF's own, or a fresh trace when the request carries no usable one.

Resolved on demand rather than at transaction start, because most deployments do not trace and generating identifiers nobody reads is per-request work for nothing. The result is cached, so asking twice gives the same span — a WAF that reported two different spans for one decision would correlate to neither.

The header comes from the client, so it is parsed strictly and discarded when malformed; inherited says which happened, since a span whose parent was discarded is a root span and must not claim a parent that will never arrive.

collectionFirst

fn collectionFirst(self: *const Transaction, name: collections.Name, key: []const u8) TransactionError!?collections.View

ResolvedValue

const ResolvedValue = struct

One resolved rule-target value: the concrete variable name it came from (e.g. an ARGS key) and its current value, both borrowed from the store.

resolveTarget

fn resolveTarget(

Resolve a rule target — a SecLang variable name plus an optional key selector — to the values a rule operator runs against, allocating the result (and any count string) in arena. A scalar variable yields at most one value; a collection yields each matching entry. The count form (&VAR) yields a single decimal cardinality. Unknown variables and regex key selectors (not yet handled here) yield nothing. This is the target-resolution layer of the rule-execution engine.

evaluatePhase

fn evaluatePhase(self: *Transaction, allocator: std.mem.Allocator, phase: Phase) TransactionError!void

Evaluate every executable rule in phase against the current transaction state and apply the ones that match — the autonomous rule-execution loop a connector runs each phase. For each executable head the cursor yields, the whole chain is evaluated (all members must match); a fully-matched chain is applied as one atomic effect batch, and an enforced disruptive decision halts the phase. Per-rule scratch (compiled operators, resolved values) lives in an arena reset between rules to bound memory.

evaluateRule

fn evaluateRule(self: *Transaction, arena: std.mem.Allocator, rule_id: compiled_plan.RuleId) TransactionError!?MatchContext

Evaluate one rule against the current transaction state, returning a MatchContext for the first matching (target-value × operator) or null. Ties the execution layers together: resolve each target to values, apply the rule's transformation pipeline, compile and run the operator (with !@op negation). The integration seam for a phase-evaluation loop and a connector's inspection hook. Chains, multiMatch checkpoints, macro-expanded operator arguments, and regex key selectors are handled by later slices; a negated target (exclusion form) is skipped here.

pluginUnavailableCount

fn pluginUnavailableCount(self: *const Transaction) usize

How many times a plugin operator could not answer during this transaction.

collectionTarget

fn collectionTarget(self: *const Transaction, target: collections.Target, exclusions: []const collections.Target) TransactionError!?collections.TargetIterator

collectionCount

fn collectionCount(self: *const Transaction, target: collections.Target, exclusions: []const collections.Target) TransactionError!?usize

addCollectionValue

fn addCollectionValue(

addArgument

fn addArgument(

addRequestCookie

fn addRequestCookie(self: *Transaction, key: []const u8, value: []const u8, source: collections.Source) TransactionError!void

addFileMetadata

fn addFileMetadata(

setCollectionValue

fn setCollectionValue(

addTransactionCollectionValue

fn addTransactionCollectionValue(self: *Transaction, name: []const u8, delta: i64) TransactionError!void

initializePersistentCollection

fn initializePersistentCollection(

setSessionCollection

fn setSessionCollection(self: *Transaction, session_id: []const u8) TransactionError!PersistentInitialization

setUserCollection

fn setUserCollection(self: *Transaction, user_id: []const u8) TransactionError!PersistentInitialization

setResourceCollection

fn setResourceCollection(self: *Transaction, resource_id: []const u8) TransactionError!PersistentInitialization

setPersistentCollectionValue

fn setPersistentCollectionValue(

addPersistentCollectionValue

fn addPersistentCollectionValue(

removePersistentCollectionValue

fn removePersistentCollectionValue(self: *Transaction, namespace: persistent.Namespace, name: []const u8) TransactionError!void

expirePersistentCollectionValue

fn expirePersistentCollectionValue(

flushPersistentCollections

fn flushPersistentCollections(self: *Transaction) TransactionError!usize

lastPersistentFailure

fn lastPersistentFailure(self: *const Transaction) ?PersistentFailure

matchIntentCount

fn matchIntentCount(self: *const Transaction) usize

matchIntent

fn matchIntent(self: *const Transaction, id: MatchIntentId) ?MatchIntent

flowState

fn flowState(self: *const Transaction) FlowState

controlState

fn controlState(self: *const Transaction) ControlState

ruleExcluded

fn ruleExcluded(self: *const Transaction, rule_id: compiled_plan.RuleId) bool

targetExcluded

fn targetExcluded(self: *Transaction, rule_id: compiled_plan.RuleId, target: []const u8) TransactionError!bool

removeCollectionValues

fn removeCollectionValues(self: *Transaction, name: collections.Name, selector: collections.Selector) TransactionError!usize

expandMacro

fn expandMacro(

expandEffectText

fn expandEffectText(

Expand a compiler-owned action subexpression without reparsing macro names on the request path. The caller owns the returned bytes.

setIdentity

fn setIdentity(self: *Transaction, remote_user: []const u8, user_id: []const u8) TransactionError!void

setServerName

fn setServerName(self: *Transaction, server_name: []const u8) TransactionError!void

setCompatibilityIdentity

fn setCompatibilityIdentity(

recordRequestBodyError

fn recordRequestBodyError(self: *Transaction, processor: []const u8, message: []const u8) TransactionError!void

recordResponseBodyError

fn recordResponseBodyError(self: *Transaction, processor: []const u8, message: []const u8) TransactionError!void

recordRegexError

fn recordRegexError(self: *Transaction, limits_exceeded: bool) TransactionError!void

recordMatch

fn recordMatch(self: *Transaction, name: []const u8, value: []const u8, severity: u8) TransactionError!void

deinit

fn deinit(self: *Transaction) void

PhaseCursor

const PhaseCursor = struct

Allocation-free traversal of immutable chain heads for one active phase. Flow state is read from the transaction after each matched rule commits, so callers do not need to copy or mutate the compiled plan.

init

fn init(transaction: *Transaction, phase: Phase) TransactionError!PhaseCursor

next

fn next(self: *PhaseCursor) TransactionError!?compiled_plan.RuleId

flow_evidence

json

const json = @embedFile("compatibility/evidence/disruptive-flow-controls.json")

macros

Limits

const Limits = struct

MissingPolicy

const MissingPolicy = enum

Resolver

const Resolver = struct

scalar

fn scalar(self: Resolver, name: variables.Name) ?[]const u8

collection

fn collection(self: Resolver, name: collections.Name, key: ?[]const u8) ?[]const u8

CompileError

const CompileError = std.mem.Allocator.Error || error

ExpandError

const ExpandError = std.mem.Allocator.Error || error

Compiled

const Compiled = struct

compile

fn compile(allocator: std.mem.Allocator, input: []const u8, limits: Limits) CompileError!Compiled

expand

fn expand(

deinit

fn deinit(self: *Compiled) void

metrics

Snapshot

const Snapshot = struct

A snapshot of the engine's counters. Taken as a value so a scrape reads a coherent set rather than a mix of moments.

max_document_bytes

const max_document_bytes = 2048

The largest document render can produce, so a caller can size a buffer once. The series set is fixed, so this is a constant rather than a guess.

render

fn render(snapshot: Snapshot, out: []u8) []const u8

Render snapshot as a Prometheus exposition document into out, returning the written slice. out must be at least max_document_bytes.

operators

IpMatcher

const IpMatcher = ip_match.Matcher

SqlInjection

const SqlInjection = struct

@detectSQLi: the libinjection SQL injection detector. State is reusable per-worker scratch and holds no cross-request data.

Match

const Match = struct

fingerprintBytes

fn fingerprintBytes(self: *const Match) []const u8

evaluate

fn evaluate(self: *SqlInjection, input: []const u8) Match

Inspect one SecLang operator input without allocation.

detectXss

fn detectXss(input: []const u8) bool

@detectXSS: the libinjection cross-site-scripting detector. Stateless and allocation-free.

Profile

const Profile = enum

The observable byte semantics of an operator can differ between the two pinned engines. Numeric parsing is the clearest example: ModSecurity uses a prefix-tolerant std::stoi, while Coraza uses Go's whole-string strconv.Atoi.

Kind

const Kind = enum(u8)

Closed union of stable scalar operators owned by WAF-17. Regex operators (rx, rxGlobal, rsub) and injection detectors (detectSQLi, detectXSS) are compiled and evaluated through dedicated paths and are not part of this allocation-free comparison dispatch.

canonicalName

fn canonicalName(self: Kind) []const u8

Spec

const Spec = struct

specs

const specs = [_]Spec

resolve

fn resolve(name: []const u8) ?Kind

Case-insensitive resolution of a canonical scalar operator name.

evaluate

fn evaluate(kind: Kind, profile: Profile, parameter: []const u8, input: []const u8) bool

Evaluate one scalar operator against a borrowed input value. parameter is the already macro-expanded operator argument and input is the transformed variable value. Both are arbitrary byte strings; neither is retained. The result is the raw operator outcome before rule negation.

evaluateNegated

fn evaluateNegated(kind: Kind, profile: Profile, parameter: []const u8, input: []const u8, negated: bool) bool

Apply SecLang operator negation (!@op) to a raw operator outcome.

RegexCompileError

const RegexCompileError = error

Compile-time errors for a regex operator. These are distinguishable so a ruleset with an invalid or over-complex pattern is rejected before publication instead of failing silently at request time.

max_capture_fields

const max_capture_fields = 10

The largest capture field index ModSecurity and Coraza expose. Field 0 is the full match; fields 1..9 are the first nine parenthesized groups.

RegexOutcome

const RegexOutcome = struct

The outcome of one regex evaluation. A bounded runtime error (match-limit, step-limit, oversized input) is not a match; it is reported through runtime_error/limit_exceeded exactly as ModSecurity sets TX.MSC_PCRE_ERROR and TX.MSC_PCRE_LIMITS_EXCEEDED and does not match.

RegexOperator

const RegexOperator = struct

A ruleset-owned compiled regex operator (@rx / @rxGlobal). The compiled program is immutable and may be shared by request workers; create one reusable Worker per worker thread so mutable matcher caches are never shared. An empty pattern always matches, mirroring the pinned engines.

compile

fn compile(allocator: std.mem.Allocator, pattern: []const u8) RegexCompileError!RegexOperator

compileWithFlags

fn compileWithFlags(allocator: std.mem.Allocator, pattern: []const u8, flags: regex.common.CompileFlags) RegexCompileError!RegexOperator

deinit

fn deinit(self: *RegexOperator) void

worker

fn worker(self: *const RegexOperator) Worker

workerWithMemo

fn workerWithMemo(self: *const RegexOperator, allocator: std.mem.Allocator, limits: MemoLimits) Worker

A memoizing worker keeps a bounded per-worker LRU of recent @rx outcomes keyed by exact input bytes. It is optional; the plain worker still amortizes the lazy DFA/NFA construction across evaluations.

MemoLimits

const MemoLimits = struct

MemoStats

const MemoStats = struct

Worker

const Worker = struct

deinit

fn deinit(self: *Worker) void

memoStats

fn memoStats(self: *const Worker) MemoStats

evaluate

fn evaluate(self: *Worker, input: []const u8) RegexOutcome

@rx: the leftmost match with capture fields 0..9. When a memo is enabled, an exact repeat input is served from the cache and its capture fields are rebuilt against the live input.

evaluateGlobal

fn evaluateGlobal(self: *Worker, allocator: std.mem.Allocator, input: []const u8) RegexOutcome

@rxGlobal: every non-overlapping match. Capture fields keep the first value per field index, matching ModSecurity storeOrUpdateFirst. Global evaluation is not memoized.

rsub

fn rsub(_: []const u8, _: []const u8) bool

ModSecurity 3.0.16 leaves @rsub unimplemented: Rsub::evaluate is a documented stub that returns true. zig-waf preserves that exact pinned behavior rather than inventing substitution semantics the baseline lacks. Actual regex substitution is out of scope until upstream implements it.

PhraseOperator

const PhraseOperator = struct

A ruleset-owned compiled @pm phrase-set operator. Space-separated patterns are compiled into an immutable, shareable Aho-Corasick automaton; matching is ASCII case-insensitive. @pmFromFile and @pmFromDataset share this type by supplying their patterns from a file or dataset instead of the argument.

compile

fn compile(allocator: std.mem.Allocator, argument: []const u8, limits: phrase.Limits) phrase.BuildError!PhraseOperator

Compile the pinned Coraza @pm argument form: the space-separated keyword list. Empty tokens (from repeated spaces) become always-match patterns, matching the pinned split semantics.

compilePatterns

fn compilePatterns(allocator: std.mem.Allocator, patterns: []const []const u8, limits: phrase.Limits) phrase.BuildError!PhraseOperator

Compile from an explicit pattern list (@pmFromDataset, or any dataset the engine supplies).

compileFromFileBytes

fn compileFromFileBytes(allocator: std.mem.Allocator, bytes: []const u8, limits: phrase.Limits) phrase.BuildError!PhraseOperator

Compile from @pmFromFile bytes: one keyword per line, trimmed, with blank lines and # comments ignored.

deinit

fn deinit(self: *PhraseOperator) void

matches

fn matches(self: *const PhraseOperator, input: []const u8) bool

Whether any phrase occurs in the input (case-insensitively).

iterator

fn iterator(self: *const PhraseOperator, input: []const u8) phrase.AhoCorasick.Iterator

Non-overlapping match iterator for @pm capture extraction.

compileIpMatch

fn compileIpMatch(allocator: std.mem.Allocator, argument: []const u8, limits: ip_match.Limits) ip_match.BuildError!IpMatcher

Compile the pinned Coraza @ipMatch argument: a comma-separated list of IPv4/IPv6 addresses and CIDR subnets. Unparseable tokens are skipped.

compileIpMatchFromFileBytes

fn compileIpMatchFromFileBytes(allocator: std.mem.Allocator, bytes: []const u8, limits: ip_match.Limits) ip_match.BuildError!IpMatcher

Compile @ipMatchFromFile bytes into an IP subnet matcher.

validateUtf8Encoding

fn validateUtf8Encoding(input: []const u8) bool

@validateUtf8Encoding: matches when the input is not valid UTF-8, mirroring Coraza !utf8.ValidString.

validateUrlEncoding

fn validateUrlEncoding(input: []const u8) bool

@validateUrlEncoding: matches when the input contains a % that is not followed by two hexadecimal digits, or is truncated. An empty input never matches. Mirrors the pinned Coraza validateURLEncodingInternal.

ByteRangeOperator

const ByteRangeOperator = struct

A ruleset-owned compiled @validateByteRange operator. The comma-separated argument lists allowed bytes as start-end ranges or single values; the operator matches when the input contains any byte outside the allowed set. An empty argument matches unconditionally, mirroring Coraza.

CompileError

const CompileError = error

compile

fn compile(argument: []const u8) CompileError!ByteRangeOperator

matches

fn matches(self: *const ByteRangeOperator, input: []const u8) bool

Whether the input contains a byte outside the allowed set.

MatcherKind

const MatcherKind = enum

The compiled multi-pattern and IP-set operators. Unlike the scalar Kind union these carry a compiled automaton or subnet set, so they resolve to a constructor rather than the allocation-free evaluate dispatch.

matcher_specs

const matcher_specs = [_]MatcherSpec

resolveMatcher

fn resolveMatcher(name: []const u8) ?MatcherKind

Case-insensitive resolution of a matcher operator name, including the pinned pmf/ipMatchF shorthand aliases.

ValidationKind

const ValidationKind = enum

The stable validation operators. validate_byte_range compiles an allowed byte set; the other two are stateless byte-string checks.

validation_specs

const validation_specs = [_]ValidationSpec

IdentityKind

const IdentityKind = enum

The national-identifier and card operators (@verifyCC, @verifyCPF, @verifySSN) share one shape: a regex parameter finds candidate digit runs, and each candidate is accepted only if it also passes a checksum or structural rule. The regex alone would match any digit string of the right shape, which is why upstream pairs it with a verifier — a rule that only ran the regex would report every 16-digit order number as a credit card.

Upstream (ModSecurity 3.0.16 verify_cc.cc) restarts the match at every byte offset and stops at the first candidate that verifies, so overlapping candidates are all considered. findAll supplies the same set of match start positions. Coraza 3.7.0 dropped these operators, so ModSecurity is the only baseline.

verify

fn verify(self: IdentityKind, candidate: []const u8) bool

resolveIdentity

fn resolveIdentity(name: []const u8) ?IdentityKind

Resolve an identity operator name, or null if it is not one.

IdentityOutcome

const IdentityOutcome = struct

The result of an identity check: whether any candidate verified, and which one, so a capturing rule can store it in TX.0 as upstream does.

luhnValid

fn luhnValid(value: []const u8) bool

Luhn mod-10 over the digits of value, ignoring any non-digit bytes (a card number written with spaces or dashes still verifies, matching upstream, which weights only digits). A candidate with no digits never verifies.

cpfValid

fn cpfValid(value: []const u8) bool

Brazilian CPF: eleven digits whose last two are mod-11 check digits over the preceding nine and ten. A run of one repeated digit (111.111.111-11) satisfies the arithmetic but is not a real CPF, and every issuer rejects it, so it is rejected here too.

ssnValid

fn ssnValid(value: []const u8) bool

US Social Security number: nine digits, excluding the ranges the Social Security Administration has never issued — area 000, 666, and 900-999; group 00; serial 0000. There is no checksum, so these structural rules are the whole test, and omitting them would make the operator match any nine digits.

IdentityOperator

const IdentityOperator = struct

A ruleset-owned @verifyCC / @verifyCPF / @verifySSN operator: the compiled candidate-finding regex plus the verifier that decides which candidates count.

compile

fn compile(allocator: std.mem.Allocator, kind: IdentityKind, pattern: []const u8) RegexCompileError!IdentityOperator

deinit

fn deinit(self: *IdentityOperator) void

evaluate

fn evaluate(self: *const IdentityOperator, allocator: std.mem.Allocator, input: []const u8) IdentityOutcome

The first candidate in input that both matches the pattern and verifies. Every candidate is considered, not just the first match: a benign digit run earlier in the value must not shadow a real card number after it.

resolveValidation

fn resolveValidation(name: []const u8) ?ValidationKind

Case-insensitive resolution of a validation operator name.

rule_eval

Outcome

const Outcome = union(enum)

evaluate

fn evaluate(name: []const u8, parameter: []const u8, input: []const u8, profile: operators.Profile) Outcome

Evaluate a compile-free operator by name. name may carry a leading @; parameter is the already macro-expanded operator argument; input is the transformed variable value. Nothing is retained.

negate

fn negate(outcome: Outcome, negated: bool) Outcome

Apply !@op negation to a decided outcome; runtime/unknown pass through.

runtime_operator

CompileError

const CompileError = anyerror

RuntimeOperator

const RuntimeOperator = union(enum)

compile

fn compile(allocator: std.mem.Allocator, name: []const u8, parameter: []const u8) CompileError!RuntimeOperator

Compile an operator by name. @rx/@pm/@ipMatch build reusable state; every other operator is stored as a borrowed name/parameter pair.

deinit

fn deinit(self: *RuntimeOperator) void

evaluate

fn evaluate(self: *const RuntimeOperator, input: []const u8, profile: operators.Profile) bool

Evaluate the operator against input (before rule negation). For @rx this spawns a lightweight per-call worker over the shared compiled program; the execution loop may instead hold a per-transaction worker for hot paths. An unrecognized compile-free operator evaluates to false.

Match

const Match = struct

One operator match: whether it matched, plus whatever the operator produced that a capturing rule can stage into TX.0..TX.N — regex captures for @rx, or libinjection's fingerprint for @detectSQLi. Regex capture strings borrow input and are valid only while it is; the fingerprint is held by value, since it is derived rather than a slice of the input.

match

fn match(self: *const RuntimeOperator, input: []const u8, profile: operators.Profile) Match

phrase

BuildError

const BuildError = std.mem.Allocator.Error || error

Limits

const Limits = struct

Match

const Match = struct

AhoCorasick

const AhoCorasick = struct

deinit

fn deinit(self: *AhoCorasick) void

contains

fn contains(self: *const AhoCorasick, input: []const u8) bool

True when any compiled pattern occurs in input (case-insensitively).

Iterator

const Iterator = struct

A non-overlapping match iterator for capture extraction. It reports, at each successive position where a pattern ends, the longest pattern ending there, then resumes scanning after that match. The @pm boolean result uses contains; capture position semantics are not corpus-pinned.

next

fn next(self: *Iterator) ?Match

iterator

fn iterator(self: *const AhoCorasick, input: []const u8) Iterator

build

fn build(allocator: std.mem.Allocator, patterns: []const []const u8, limits: Limits) BuildError!AhoCorasick

Compile the space- or list-separated patterns into an immutable automaton. Patterns are lowercased for ASCII case-insensitive matching; empty patterns make the automaton always match, mirroring the pinned engines.

ip_match

Address

const Address = union(enum)

Cidr

const Cidr = union(enum)

BuildError

const BuildError = std.mem.Allocator.Error || error

Limits

const Limits = struct

parseIpv4

fn parseIpv4(text: []const u8) ?u32

Parse a single dotted-quad IPv4 address into host-order bits.

parseIpv6

fn parseIpv6(text: []const u8) ?u128

Parse an IPv6 address, including :: zero-compression and a trailing embedded IPv4 group, into 128 host-order bits.

parseAddress

fn parseAddress(text: []const u8) ?Address

Parse an address that is either IPv4 or IPv6. An IPv4-mapped IPv6 address (::ffff:a.b.c.d) is normalized to IPv4, matching Go net.IP.To4, so it compares as IPv4 and never matches a genuine IPv6 subnet.

parseCidr

fn parseCidr(text_raw: []const u8) ?Cidr

Parse one @ipMatch subnet token with the pinned bare-address defaults.

contains

fn contains(cidr: Cidr, address: Address) bool

Whether address falls inside cidr. Cross-family comparisons never match.

Matcher

const Matcher = struct

A ruleset-owned compiled @ipMatch argument: a set of subnets. Immutable and shareable across request workers.

build

fn build(allocator: std.mem.Allocator, argument: []const u8, limits: Limits) BuildError!Matcher

Build from a comma-separated subnet list. Unparseable tokens are skipped, matching Coraza net.ParseCIDR error tolerance.

buildFromFileBytes

fn buildFromFileBytes(allocator: std.mem.Allocator, bytes: []const u8, limits: Limits) BuildError!Matcher

Build from @ipMatchFromFile bytes: one subnet per line, trimmed, with blank lines and # comments ignored and unparseable tokens skipped.

deinit

fn deinit(self: *Matcher) void

matches

fn matches(self: *const Matcher, input: []const u8) bool

Whether the parsed input address is inside any configured subnet.

operator_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void

fuzzDeterministic

fn fuzzDeterministic(allocator: std.mem.Allocator, iterations: usize, seed: u64) !void

plan

StringId

const StringId = enum(u32)

DirectiveId

const DirectiveId = enum(u32)

RuleId

const RuleId = enum(u32)

DefaultId

const DefaultId = enum(u32)

MacroProgramId

const MacroProgramId = enum(u32)

MarkerId

const MarkerId = enum(u32)

compiler_abi_version

const compiler_abi_version: u32 = 7

Fingerprint

const Fingerprint = [32]u8

evidence_json

const evidence_json = @embedFile("compatibility/evidence/structural-plan.json")

Limits

const Limits = struct

validate

fn validate(self: Limits) error

CompileError

const CompileError = std.mem.Allocator.Error || error

DataProvider

const DataProvider = struct

Supplies the bytes of an operator data file (@pmFromFile, @ipMatchFromFile) referenced from a rule file. The compiler resolves the file once, at compile time, so the runtime never touches the filesystem. base_dir is the directory of the rule file that named the data file; the provider is responsible for confining the read (see seclang.include.readDataFileAlloc). A null provider (the default) leaves file-backed operators unresolved.

read

fn read(self: DataProvider, allocator: std.mem.Allocator, base_dir: []const u8, filename: []const u8) anyerror![]u8

Read the data file's bytes relative to base_dir, owned by allocator.

DiagnosticCode

const DiagnosticCode = enum

id

fn id(self: DiagnosticCode) []const u8

message

fn message(self: DiagnosticCode) []const u8

Diagnostic

const Diagnostic = struct

CompileOutcome

const CompileOutcome = union(enum)

deinit

fn deinit(self: *CompileOutcome) void

ExecutionValidation

const ExecutionValidation = union(enum)

StringRange

const StringRange = struct

SourceRecord

const SourceRecord = struct

Argument

const Argument = struct

Directive

const Directive = struct

Target

const Target = struct

Operator

const Operator = struct

PrefilterKind

const PrefilterKind = enum

Prefilter

const Prefilter = struct

Action

const Action = struct

ActionClass

const ActionClass = enum

Marker

const Marker = struct

SkipAfterTarget

const SkipAfterTarget = struct

MarkerResolution

const MarkerResolution = struct

MacroTokenKind

const MacroTokenKind = enum

MacroToken

const MacroToken = struct

MacroProgram

const MacroProgram = struct

Transformation

const Transformation = struct

DefaultSnapshot

const DefaultSnapshot = struct

MetadataText

const MetadataText = struct

RuleMetadata

const RuleMetadata = struct

EffectText

const EffectText = struct

EffectKind

const EffectKind = enum

NondisruptiveEffect

const NondisruptiveEffect = struct

Typed request-path descriptor. Field meanings are selected by kind: setenv uses name/value; setvar uses collection/name/value/operation; init/bind uses collection/value; expiry uses collection/name/value; and deprecation additionally uses auxiliary for the period.

DisruptiveKind

const DisruptiveKind = enum

DisruptiveDecision

const DisruptiveDecision = struct

Effective decision for a matching rule after its phase default and explicit actions have been reconciled. block is compiled to the referenced phase default and never requires action-list scanning on the request path.

FlowDecision

const FlowDecision = struct

RuntimeControl

const RuntimeControl = struct

Rule

const Rule = struct

RuleRemoval

const RuleRemoval = struct

MissingRuleReferenceKind

const MissingRuleReferenceKind = enum

MissingRuleReference

const MissingRuleReference = struct

RemoteSource

const RemoteSource = seclang.include.RemoteSource

RemoteWarning

const RemoteWarning = seclang.include.RemoteWarning

Plan

const Plan = struct

deinit

fn deinit(self: *Plan) void

retain

fn retain(self: *const Plan, allocator: std.mem.Allocator) std.mem.Allocator.Error!*Plan

Create another owned handle to the same immutable payload.

sharedReferenceCount

fn sharedReferenceCount(self: *const Plan) usize

string

fn string(self: *const Plan, id: StringId) ?[]const u8

settingText

fn settingText(self: *const Plan, name: []const u8) ?[]const u8

The single argument of the last-declared name directive, or null when the configuration never set it.

"Last-declared" is the semantics of a singular-replace directive: a later SecDebugLogLevel overrides an earlier one, including one from an include, and returning the first would make the answer depend on file order in the opposite direction from how the parser resolved it.

This exists so a setting the parser validates is a setting a host can act on. A directive that is checked and then unreadable is one that silently does nothing — the operator sets it, sees no effect, and has no way to tell that nothing is listening.

sourceSlice

fn sourceSlice(self: *const Plan, span: seclang.source.Span) CompileError![]const u8

sourceLocation

fn sourceLocation(self: *const Plan, source_id: seclang.source.SourceId, offset: u32) CompileError!seclang.source.Location

phaseRules

fn phaseRules(self: *const Plan, phase: u8) []const RuleId

resolveMarkerAfter

fn resolveMarkerAfter(self: *const Plan, rule_id: RuleId, name: []const u8) ?MarkerResolution

Resolve a dynamic skipAfter value without allocation. Duplicate marker names select the first matching marker after the executing rule.

firstUnresolvedStaticMarker

fn firstUnresolvedStaticMarker(self: *const Plan) ?SkipAfterTarget

validateExecutionPlan

fn validateExecutionPlan(self: *const Plan) ExecutionValidation

prefilterMayMatch

fn prefilterMayMatch(self: *const Plan, value: Prefilter, input: []const u8) bool

Returns false only when the advisory prefilter proves the operator cannot match. Callers must still execute the operator when this returns true.

compile

fn compile(

compileWithProvider

fn compileWithProvider(

Like compile, but with a DataProvider so @pmFromFile / @ipMatchFromFile operators load their referenced data files (resolved relative to each rule file's directory). Callers that read config from disk supply a provider; callers with no filesystem context use compile.

PluginNames

const PluginNames = struct

The extension names a host provides, so a configuration naming one compiles — and one naming an extension nothing provides still does not.

compileWithPlugins

fn compileWithPlugins(

Compile with the extension names a host's plugins provide (#23), so a rule may name @geoLookup, or a ctl may select a host body processor, when the host answers it — and still be refused when nothing does, rather than compiling into a rule that never matches or a body nothing reads.

compileTree

fn compileTree(

Compile an include/remote source tree in textual directive order. Child sources are visited immediately after their owning include/remote directive, rather than after the remainder of the parent document.

compileTreeWithProvider

fn compileTreeWithProvider(

Include-aware compileTree with a DataProvider for file-backed operators.

compileWithPrevious

fn compileWithPrevious(

Compile transactionally, then reuse the prior immutable payload only when every owned field is exactly equivalent. The public fingerprint is a fast reject, never the sole equality decision.

compileOutcome

fn compileOutcome(

plugin

Outcome

const Outcome = union(enum)

What a plugin operator decided.

Operator

const Operator = struct

One host-provided operator.

evaluateFn is called on the request path, so it must not block: a plugin that needs a network round trip is expected to answer from a cache it maintains elsewhere and report unavailable on a miss. Neither parameter nor input is retained after the call returns.

evaluate

fn evaluate(self: Operator, parameter: []const u8, input: []const u8) Outcome

BodyProcessor

const BodyProcessor = struct

A host-provided body processor: given the buffered body, it publishes arguments the WAF's built-in processors do not know how to extract — protobuf, msgpack, a bespoke form encoding.

The processor is selected by name through ctl:requestBodyProcessor, exactly like the built-in ones, and reports each extracted argument through publish. It runs on the request path with the body already in memory, so it must not do I/O of its own.

process

fn process(self: BodyProcessor, body: []const u8, sink: ArgumentSink) ProcessResult

ArgumentSink

const ArgumentSink = struct

Where a body processor puts what it extracts. Names and values are copied by the WAF, so the processor may reuse its own buffers.

add

fn add(self: ArgumentSink, name: []const u8, value: []const u8) error

ProcessResult

const ProcessResult = enum

How a body processor finished. A processor that gives up must say so: a body it silently declined to read is a body no rule inspected.

Registry

const Registry = struct

The set of extensions a host provides. Borrowed by the WAF for its lifetime, so the host owns the storage and must outlive it — the same ownership rule as the persistent-collection backend.

findOperator

fn findOperator(self: *const Registry, name: []const u8) ?*const Operator

The plugin answering to name (with or without a leading @), or null.

providesOperator

fn providesOperator(self: *const Registry, name: []const u8) bool

Whether name is provided, for the compile-time check that refuses a rule whose operator nothing can evaluate.

operatorNames

fn operatorNames(self: *const Registry, out: [][]const u8) [][]const u8

The registered operator names, for passing to plan compilation.

bodyProcessorNames

fn bodyProcessorNames(self: *const Registry, out: [][]const u8) [][]const u8

The registered body-processor names, for passing to plan compilation.

findBodyProcessor

fn findBodyProcessor(self: *const Registry, name: []const u8) ?*const BodyProcessor

The body processor answering to name, or null.

validate

fn validate(self: *const Registry) error

A registry is only usable if its names are distinct and non-empty: two plugins answering to one name would make which of them runs depend on registration order.

plan_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void

rule_config

MissingRulePolicy

const MissingRulePolicy = enum

IdInterval

const IdInterval = struct

contains

fn contains(self: IdInterval, value: u64) bool

IdSelectorLimits

const IdSelectorLimits = struct

validate

fn validate(self: IdSelectorLimits) error

IdSelectorError

const IdSelectorError = std.mem.Allocator.Error || error

IdSelectorFailure

const IdSelectorFailure = struct

IdSelector

const IdSelector = struct

parse

fn parse(

deinit

fn deinit(self: *IdSelector) void

matches

fn matches(self: IdSelector, value: u64) bool

rule_config_evidence

json

const json = @embedFile("compatibility/evidence/rule-configuration.json")

remote_rules

FailAction

const FailAction = enum

Limits

const Limits = struct

validate

fn validate(self: Limits) error

DestinationPolicy

const DestinationPolicy = struct

authorize

fn authorize(self: DestinationPolicy, url: []const u8, address: ?[]const u8) bool

Request

const Request = struct

Response

const Response = struct

deinit

fn deinit(self: *Response) void

FetchError

const FetchError = std.mem.Allocator.Error || error

Fetcher

const Fetcher = struct

fetch

fn fetch(self: Fetcher, allocator: std.mem.Allocator, request: Request) FetchError!Response

WarningCode

const WarningCode = enum

Warning

const Warning = struct

Deliberately contains neither the authentication key nor response bytes.

Source

const Source = struct

bytes

fn bytes(self: *const Source) []const u8

finalUrl

fn finalUrl(self: *const Source) []const u8

deinit

fn deinit(self: *Source) void

Outcome

const Outcome = union(enum)

deinit

fn deinit(self: *Outcome) void

LoadError

const LoadError = std.mem.Allocator.Error || error

load

fn load(

request

default_separator

const default_separator: u8 = '&'

The default query-string argument separator. ModSecurity exposes it through SecArgumentSeparator; Coraza defaults to &.

Pair

const Pair = struct

QueryIterator

const QueryIterator = struct

Iterates raw (undecoded) query-string arguments exactly like Coraza doParseQuery: split on the separator, skip empty segments, and split each segment on its first = (a segment with no = has an empty value).

next

fn next(self: *QueryIterator) ?Pair

parseQuery

fn parseQuery(query: []const u8, separator: u8) QueryIterator

Decoded

const Decoded = struct

A decoded token and whether the input contained a malformed percent-escape. The two are separate because decoding never fails — a bad escape is preserved literally, as both baselines do — while URLENCODED_ERROR still has to report that it happened, which is how a rule detects an encoding trick aimed at whatever is behind the WAF.

queryUnescape

fn queryUnescape(dest: []u8, input: []const u8) []u8

Decode one x-www-form-urlencoded token into dest, which must be at least input.len bytes; the decoded slice is never longer than the input. + becomes a space, %XX decodes a byte, and a malformed or truncated % is preserved literally, matching the pinned Coraza queryUnescape.

queryUnescapeCounting

fn queryUnescapeCounting(dest: []u8, input: []const u8) Decoded

queryUnescape, additionally reporting whether any escape was malformed — what ModSecurity's urldecode_nonstrict_inplace counts to set URLENCODED_ERROR.

CookieIterator

const CookieIterator = struct

Iterates raw cookies from a Cookie: header value in the Netscape format: name=value pairs separated by ;, with optional surrounding spaces. A pair with no = yields an empty value; a leading $-prefixed attribute is preserved as an ordinary cookie, matching ModSecurity's byte-oriented split.

next

fn next(self: *CookieIterator) ?Pair

parseCookies

fn parseCookies(header: []const u8) CookieIterator

fuzzOne

fn fuzzOne(input: []const u8) !void

Fuzz invariants for the request parsers: parsing never crashes, decoding never exceeds the input length and is deterministic, and iterators yield only borrowed slices of the input.

request_buffer

Policy

const Policy = enum

Limits

const Limits = struct

validate

fn validate(self: Limits) error

SinkError

const SinkError = error

Sink

const Sink = struct

Overflow destination for spilled bytes (a temp file in production).

Status

const Status = enum

WriteError

const WriteError = std.mem.Allocator.Error || SinkError || error

FileSink

const FileSink = struct

A Sink backed by a file the caller opened, for spooling an oversized body or an uploaded file to disk (#25).

The engine never opens this itself. Spooling is deliberately the connector's job: the request path must not block on a filesystem, and where uploads may be written (SecUploadDir/SecTmpDir), with what permissions, and whether they survive the transaction (SecUploadKeepFiles) are deployment decisions the WAF core has no business making. This type exists so a connector does not have to reimplement the boundary — it owns the handle, counts what it wrote, and reports disk exhaustion as such rather than as a generic failure.

init

fn init(io: std.Io, file: std.Io.File, max_bytes: u64) FileSink

sink

fn sink(self: *FileSink) Sink

Buffer

const Buffer = struct

init

fn init(allocator: std.mem.Allocator, limits: Limits, policy: Policy, sink: ?Sink) error

deinit

fn deinit(self: *Buffer) void

write

fn write(self: *Buffer, chunk: []const u8) WriteError!void

Append one body chunk. Returns error.BodyLimitRejected under the reject policy once the total limit would be exceeded; under process-partial it truncates silently and reports truncated via status.

inMemory

fn inMemory(self: *const Buffer) []const u8

The in-memory portion of the body. When isSpooled() is false this is the complete body; otherwise it is the leading in_memory_limit bytes.

isSpooled

fn isSpooled(self: *const Buffer) bool

wasTruncated

fn wasTruncated(self: *const Buffer) bool

multipart

Error

const Error = std.mem.Allocator.Error || error
const Header = struct

Part

const Part = struct

One parsed part: the raw header block and the body, both borrowed from the input. incomplete marks a part whose body was not closed by a delimiter.

HeaderIterator

const HeaderIterator = struct

next

fn next(self: *HeaderIterator) ?Header

headers

fn headers(self: Part) HeaderIterator

RawLineIterator

const RawLineIterator = struct

Iterates the raw header lines (CR stripped, blank lines skipped), which is what ModSecurity records verbatim in MULTIPART_PART_HEADERS.

next

fn next(self: *RawLineIterator) ?[]const u8

rawHeaderLines

fn rawHeaderLines(self: Part) RawLineIterator

headerValue

fn headerValue(self: Part, wanted: []const u8) ?[]const u8

The first header value matching name case-insensitively, or null.

filename

fn filename(self: Part) ?[]const u8

The filename parameter of Content-Disposition (empty/absent → null), which is how Coraza distinguishes a file part from a field.

name

fn name(self: Part) ?[]const u8

The name parameter of Content-Disposition regardless of the disposition type. ModSecurity keys ARGS_POST and the MULTIPART_* fields off this raw name (unlike Go's FormName, which requires form-data).

formName

fn formName(self: Part) ?[]const u8

Go's Part.FormName: the name parameter, but only when the disposition type is form-data.

Anomalies

const Anomalies = struct

What was irregular about a multipart body, mirroring ModSecurity's MULTIPART_* flags (#26).

None of these is malformed enough to stop parsing — every one of them is a body some client really sends and some server really accepts. They matter because a WAF and the application behind it may disagree about what such a body means, and that disagreement is the bypass: a part the WAF skips and the app reads is an unfiltered input. So each is reported as a flag a rule can act on, rather than being silently normalized away.

any

fn any(self: Anomalies) bool

Whether any anomaly was seen, which is what MULTIPART_STRICT_ERROR reports.

boundaryWithAnomalies

fn boundaryWithAnomalies(content_type: []const u8, anomalies: *Anomalies) ?[]const u8

The boundary parameter together with what was irregular about how it was written. A quoted boundary is legal per RFC 2046 but is the classic way to make two parsers disagree, so it is reported rather than merely accepted.

scanAnomalies

fn scanAnomalies(body: []const u8, boundary: []const u8, anomalies: *Anomalies) void

Scan a body for the line-ending and boundary anomalies that do not depend on how parts are split: bare LF endings, a mix of CRLF and LF, an unterminated final part, and boundary-looking text inside part data.

max_safe_filename

const max_safe_filename = 128

The longest safe filename safeFileName will produce, leaving room for a connector's own prefix within a typical 255-byte filesystem limit.

safeFileName

fn safeFileName(filename: []const u8, out: []u8) []const u8

Reduce an uploaded part's filename to something safe to use as a name (#26). The result contains only ASCII letters, digits, dot, dash, and underscore; never a path separator, never a leading dot, and never empty.

A multipart filename is attacker-controlled and is not a path, however much it looks like one. ../../etc/crontab, C:\windows\system32\x, and a name containing a NUL are all things clients send, and a connector that joins one to an upload directory writes wherever the client chose. This exists so a connector never has to decide how to make one safe, and so the answer is the same everywhere. The original filename is still published verbatim in FILES_NAMES, because rules must see what was actually sent.

Returns the safe name written into out, which must be at least max_safe_filename bytes.

headersUseFolding

fn headersUseFolding(raw_headers: []const u8) bool

Whether a part's raw header block uses obsolete line folding — a continuation line starting with space or tab. Parsers differ on whether the folded value belongs to the previous header, which is exactly the ambiguity worth flagging.

boundaryFromContentType

fn boundaryFromContentType(content_type: []const u8) ?[]const u8

The boundary parameter of a multipart/* Content-Type value, unquoting a quoted-string form. Returns null when absent or empty — Coraza treats a missing boundary as a strict multipart error.

Reader

const Reader = struct

init

fn init(allocator: std.mem.Allocator, input: []const u8, boundary: []const u8) Error!Reader

deinit

fn deinit(self: *Reader) void

next

fn next(self: *Reader) ?Part

audit

Header

const Header = struct

Parts

const Parts = struct

The audit parts to include, keyed 'A'..'K'. A and Z are always emitted (the header and terminator); B–K are selectable. Mirrors SecAuditLogParts.

has

fn has(self: Parts, part: u8) bool

fromLetters

fn fromLetters(letters: []const u8) Parts

Build from a letter set such as "ABCFHZ".

AuditRecord

const AuditRecord = struct

Format

const Format = enum

The audit-log output formats.

write

fn write(out: *std.ArrayList(u8), allocator: std.mem.Allocator, record: AuditRecord, parts: Parts, format: Format) !void

Serialize record in format, appending to out.

Civil

const Civil = struct

The UTC calendar breakdown of an epoch timestamp.

civil

fn civil(epoch_seconds: i64) Civil

Break epoch_seconds into UTC calendar components. UTC keeps every derived timestamp and path deterministic and free of the host time-zone database.

writeTimestamp

fn writeTimestamp(out: *std.ArrayList(u8), allocator: std.mem.Allocator, epoch_seconds: i64) !void

Append epoch_seconds (UTC) as ModSecurity's audit timestamp, [dd/Mmm/yyyy:hh:mm:ss +0000].

writeSerial

fn writeSerial(out: *std.ArrayList(u8), allocator: std.mem.Allocator, record: AuditRecord, parts: Parts) !void

Serialize record in the native serial (ModSecurity legacy) audit format, appending to out.

writeJson

fn writeJson(out: *std.ArrayList(u8), allocator: std.mem.Allocator, record: AuditRecord, parts: Parts) !void

Serialize record as a single JSON object matching Coraza's structured audit-log schema: {"transaction":{...},"messages":[...]}. Field values are gated by the same A–K part selection (headers/body only when their part is selected). Go marshals header maps in nondeterministic key order, so this is schema-compatible, not byte-identical.

writeLegacyJson

fn writeLegacyJson(out: *std.ArrayList(u8), allocator: std.mem.Allocator, record: AuditRecord, parts: Parts) !void

Serialize record in Coraza's legacy JSON audit format: a flatter object with remote_address/local_address naming and header maps whose repeated values are joined with ", " into a single string.

writeOcsf

fn writeOcsf(out: *std.ArrayList(u8), allocator: std.mem.Allocator, record: AuditRecord, parts: Parts) !void

Serialize record as an OCSF (Open Cybersecurity Schema Framework) v1.2.0 "Web Resources Activity" event, matching Coraza's OCSF formatter. Emits the class/category/type identifiers, allowed/denied action, HTTP request and response objects, endpoints, enrichments per rule message, and observables.

audit_writer

WriteError

const WriteError = error

Sink

const Sink = struct

A non-blocking, embedder-owned destination for record bytes.

write

fn write(self: Sink, bytes: []const u8) WriteError!void

SerialWriter

const SerialWriter = struct

Appends each formatted record to a single destination, one record per line (Coraza's serialWriter: the formatted bytes followed by a newline). An empty record is skipped, matching Coraza.

write

fn write(self: SerialWriter, record: []const u8) WriteError!void

CallbackWriter

const CallbackWriter = struct

Delivers each formatted record to a caller-supplied callback, for connectors that own their transport. The callback runs synchronously and must not block the request path; a connector that needs durability should enqueue.

write

fn write(self: CallbackWriter, record: []const u8) WriteError!void

DeliveryQueue

const DeliveryQueue = struct

A bounded queue between record production and delivery (#31).

The request path must not wait on a log destination — a slow disk or an unreachable collector cannot be allowed to slow down request handling, and a destination that is down must not consume memory without limit either. So records are enqueued (a copy, bounded in count and bytes) and delivered later by whatever the host runs the drain on.

The queue is explicit about loss. A record that cannot be enqueued is counted, and dropped() is a number an operator can alert on, because an audit log with gaps nobody knows about is worse than one that is visibly incomplete.

Limits

const Limits = struct

Overflow

const Overflow = enum

What a full queue does with a new record.

Stats

const Stats = struct

Delivery accounting. Every record is in exactly one of these outcomes, so the numbers add up and a gap cannot hide.

init

fn init(allocator: std.mem.Allocator, limits: Limits, overflow: Overflow) DeliveryQueue

deinit

fn deinit(self: *DeliveryQueue) void

len

fn len(self: *const DeliveryQueue) usize

accounting

fn accounting(self: *const DeliveryQueue) Stats

enqueue

fn enqueue(self: *DeliveryQueue, record: []const u8) error

Copy a record into the queue. Returns false when the record could not be held — which is counted, never silent.

drain

fn drain(self: *DeliveryQueue, writer: SerialWriter) usize

Deliver queued records through writer, stopping at the first failure and keeping everything from that record onward for the next attempt. Returns how many were delivered.

Stopping rather than skipping is deliberate: an audit log that reorders or silently omits records is not an audit log, so a failing destination delays delivery instead of scattering it.

backoffMilliseconds

fn backoffMilliseconds(self: *const DeliveryQueue, base_ms: u64, max_delay_ms: u64) u64

How long to wait before retrying, doubling per consecutive failure up to max_delay_ms. A destination that is down should be retried, not hammered.

shutdown

fn shutdown(self: *DeliveryQueue, writer: SerialWriter) usize

Deliver everything still queued at shutdown, reporting what could not be delivered. A caller that ignores the return value loses records knowingly; one that logs it can say exactly how many.

ConcurrentEntry

const ConcurrentEntry = struct

The per-record file path and audit-index line for the ModSecurity "concurrent" layout. The core produces this metadata; the connector writes the formatted record to <storage_dir>/<path> and appends index_line to the audit index file. Both slices are owned by the caller.

deinit

fn deinit(self: ConcurrentEntry, allocator: std.mem.Allocator) void

concurrentEntry

fn concurrentEntry(

Build the concurrent-layout path and index line for record. The path is the time-bucketed YYYYMMDD/YYYYMMDD-HHMM/YYYYMMDD-HHMMSS-<id> (UTC), and the index line is ModSecurity's combined-log-style audit index entry; file_size and md5_hex describe the written record and are the caller's.

persistent

backend_abi_version

const backend_abi_version: u32 = 3

BackendFeature

const BackendFeature = enum(u8)

BackendFeatureSet

const BackendFeatureSet = struct

core

fn core() BackendFeatureSet

insert

fn insert(self: *BackendFeatureSet, feature: BackendFeature) void

has

fn has(self: BackendFeatureSet, feature: BackendFeature) bool

containsAll

fn containsAll(self: BackendFeatureSet, required: BackendFeatureSet) bool

Namespace

const Namespace = enum(u8)

collectionName

fn collectionName(self: Namespace) collections.Name

parse

fn parse(input: []const u8) ?Namespace

FailurePolicy

const FailurePolicy = enum

Limits

const Limits = struct

validate

fn validate(self: Limits) error

BackendError

const BackendError = std.mem.Allocator.Error || error

Value

const Value = struct

expired

fn expired(self: Value, now_ns: i64) bool

Snapshot

const Snapshot = struct

Allocator-owned result returned by a backend load. The caller must deinit it.

init

fn init(allocator: std.mem.Allocator, revision: u64) Snapshot

append

fn append(self: *Snapshot, value: Value) std.mem.Allocator.Error!void

deinit

fn deinit(self: *Snapshot) void

SetMutation

const SetMutation = struct

Mutation

const Mutation = union(enum)

CommitRequest

const CommitRequest = struct

CleanupBudget

const CleanupBudget = struct

CleanupResult

const CleanupResult = struct

Backend

const Backend = struct

Versioned callback table. Context lifetime is owned by the application and must exceed the WAF and every transaction that can call this backend.

load

fn load(

commit

fn commit(self: Backend, request: CommitRequest) BackendError!u64

cleanup

fn cleanup(self: Backend, now_ns: i64, budget: CleanupBudget) BackendError!CleanupResult

SessionError

const SessionError = BackendError || error

DeprecationResult

const DeprecationResult = struct

Checkpoint

const Checkpoint = struct

Session

const Session = struct

Per-request persistence state. Creating a session performs no backend I/O; only explicit collection initialization and flushing invoke callbacks.

init

fn init(allocator: std.mem.Allocator, backend: Backend, limits: Limits) error

initialize

fn initialize(self: *Session, namespace: Namespace, collection_key: []const u8, now_ns: i64) SessionError!?Snapshot

Returns a loaded snapshot on first initialization and null when the same namespace/key pair was already initialized by this transaction.

set

fn set(self: *Session, namespace: Namespace, name: []const u8, value: []const u8, expires_at_ns: ?i64) SessionError!void

remove

fn remove(self: *Session, namespace: Namespace, name: []const u8) SessionError!void

add

fn add(self: *Session, namespace: Namespace, name: []const u8, delta: i64) SessionError!void

subtract

fn subtract(self: *Session, namespace: Namespace, name: []const u8, delta: i64) SessionError!void

deprecate

fn deprecate(

Stage elapsed-time decay while retaining an append-only clock history that can be truncated by rollback. The backend recomputes the same operation against its authoritative revision during commit/retry.

expire

fn expire(self: *Session, namespace: Namespace, name: []const u8, expires_at_ns: i64) SessionError!void

flush

fn flush(self: *Session, now_ns: i64) SessionError!usize

Flush dirty bindings independently. A revision conflict reloads only the current revision and reapplies the ordered mutation log. Numeric additions therefore compose instead of becoming last-writer-wins.

hasDirtyCollections

fn hasDirtyCollections(self: *const Session) bool

checkpoint

fn checkpoint(self: *const Session) Checkpoint

Capture the logical session state before staging a multi-effect rule. Arena allocations remain charged after rollback, while bindings and mutation logs return to the exact checkpoint boundary.

rollback

fn rollback(self: *Session, checkpoint_value: Checkpoint) void

cancelInitialization

fn cancelInitialization(self: *Session, namespace: Namespace) void

Roll back a just-created binding when the caller cannot publish its loaded snapshot into transaction-local collection storage.

discardLastMutation

fn discardLastMutation(self: *Session, namespace: Namespace) void

deinit

fn deinit(self: *Session) void

Record

const Record = struct

Owned mutable record used by persistence backend implementations.

init

fn init(allocator: std.mem.Allocator, namespace: Namespace, collection_key: []const u8, revision: u64) std.mem.Allocator.Error!Record

clone

fn clone(allocator: std.mem.Allocator, source: *const Record) std.mem.Allocator.Error!Record

append

fn append(self: *Record, value: Value) std.mem.Allocator.Error!void

deinit

fn deinit(self: *Record) void

InMemoryBackend

const InMemoryBackend = struct

Thread-safe optimistic backend used by tests and embedded deployments. Commits build a replacement record before taking publication ownership, so allocation or validation failure cannot expose a partial mutation batch.

init

fn init(allocator: std.mem.Allocator) InMemoryBackend

backend

fn backend(self: *InMemoryBackend) Backend

deinit

fn deinit(self: *InMemoryBackend) void

applyMutations

fn applyMutations(record: *Record, mutations: []const Mutation, limits: Limits) BackendError!void

parseNumericOrZero

fn parseNumericOrZero(input: []const u8) i64

Compatibility parser for ModSecurity's std::stoi-based setvar arithmetic: leading ASCII whitespace and a sign are accepted, parsing stops after the decimal prefix, and missing/invalid/out-of-range input becomes zero.

persistent_lmdb

Options

const Options = struct

validate

fn validate(self: Options) error

InitError

const InitError = std.mem.Allocator.Error || error

LmdbBackend

const LmdbBackend = struct

init

fn init(allocator: std.mem.Allocator, path: [:0]const u8, options: Options) InitError!LmdbBackend

backend

fn backend(self: *LmdbBackend) persistent.Backend

deinit

fn deinit(self: *LmdbBackend) void

selectors

RegexSelector

const RegexSelector = struct

Ruleset-owned compiled selector. Create one reusable Worker per request worker so mutable DFA caches are never shared across threads.

compile

fn compile(allocator: std.mem.Allocator, pattern: []const u8) !RegexSelector

worker

fn worker(self: *const RegexSelector) Worker

deinit

fn deinit(self: *RegexSelector) void

Worker

const Worker = struct

matcher

fn matcher(self: *Worker) collections.Matcher

deinit

fn deinit(self: *Worker) void

seclang

source

const source = @import("source.zig")

diagnostic

const diagnostic = @import("diagnostic.zig")

evidence

const evidence = @import("evidence.zig")

fuzz

const fuzz = @import("fuzz.zig")

include

const include = @import("include.zig")

lexer

const lexer = @import("lexer.zig")

parser

const parser = @import("parser.zig")

assembly

const assembly = @import("assembly.zig")

syntax

const syntax = @import("syntax.zig")

variables

inline_value_capacity

const inline_value_capacity = 64

Common scalar values stay allocation-free without multiplying a large buffer by every entry in the stable registry.

Name

const Name = enum

secLangName

fn secLangName(self: Name) []const u8

parse

fn parse(input: []const u8) ?Name

minimumAvailability

fn minimumAvailability(self: Name) Availability

defaultOrigin

fn defaultOrigin(self: Name) Origin

Origin

const Origin = enum

Availability

const Availability = enum(u8)

View

const View = struct

StoreError

const StoreError = std.mem.Allocator.Error || error

SetValue

const SetValue = struct

PreparedBatch

const PreparedBatch = struct

deinit

fn deinit(self: *PreparedBatch) void

Store

const Store = struct

init

fn init(allocator: std.mem.Allocator) Store

set

fn set(

setUnsigned

fn setUnsigned(

prepareBatch

fn prepareBatch(

Allocate and validate a unique scalar update set without changing the visible store. commitPreparedBatch cannot fail after this succeeds.

commitPreparedBatch

fn commitPreparedBatch(self: *Store, prepared: *PreparedBatch) void

get

fn get(self: *const Store, name: Name, current: Availability) ?View

getBySecLangName

fn getBySecLangName(self: *const Store, name: []const u8, current: Availability) ?View

deinit

fn deinit(self: *Store) void

transformations

Kind

const Kind = enum(u8)

canonicalName

fn canonicalName(self: Kind) []const u8

Resolution

const Resolution = union(enum)

canonicalName

fn canonicalName(self: Resolution) []const u8

Spec

const Spec = struct

Alias

const Alias = struct

specs

const specs = [_]Spec

aliases

const aliases = [_]Alias

resolve

fn resolve(name: []const u8) ?Resolution

Limits

const Limits = struct

validate

fn validate(self: Limits) error

Storage

const Storage = enum

Profile

const Profile = enum

UnicodeMap

const UnicodeMap = struct

Immutable ModSecurity Unicode-map values indexed by a 16-bit code point. Negative and out-of-range entries are unmapped. The backing table must outlive the executor and may be shared by request workers.

lookup

fn lookup(self: UnicodeMap, code_point: u16) ?u8

Options

const Options = struct

CacheStatus

const CacheStatus = enum

CacheStats

const CacheStats = struct

Result

const Result = struct

Checkpoint

const Checkpoint = struct

PipelineResult

const PipelineResult = struct

ApplyError

const ApplyError = std.mem.Allocator.Error || error

FailureKind

const FailureKind = enum

failureKind

fn failureKind(err: ApplyError) FailureKind

Executor

const Executor = struct

Reusable bounded scratch for one request worker/transaction. Executor-backed result bytes remain valid until that same scratch slot is reused (at least one subsequent executor-backed result); borrowed results retain input life.

init

fn init(allocator: std.mem.Allocator, limits: Limits) error

initWithProfile

fn initWithProfile(allocator: std.mem.Allocator, limits: Limits, profile: Profile) error

initWithOptions

fn initWithOptions(allocator: std.mem.Allocator, limits: Limits, options: Options) error

deinit

fn deinit(self: *Executor) void

cacheStats

fn cacheStats(self: *const Executor) CacheStats

apply

fn apply(self: *Executor, kind: Kind, input: []const u8) ApplyError!Result

applyPipeline

fn applyPipeline(self: *Executor, pipeline: anytype, input: []const u8, multi_match: bool) ApplyError!PipelineResult

transformation_fuzz

fuzzOne

fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) !void