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
Header
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