WACElib sits between your WAF engine and your block/allow decision logic. After a WAF like ModSecurity or Coraza evaluates a transaction against the OWASP Core Rule Set, you forward the HTTP payload and the resulting anomaly scores into WACElib. WACElib fans the payload out to one or more machine learning model plugins, waits for their probability-of-attack scores, and then hands everything to a decision plugin that produces the final verdict. This page explains each layer of that pipeline, how plugin types are selected, and where NATS fits into the picture.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tilsor/ModSecIntl_wace_lib/llms.txt
Use this file to discover all available pages before exploring further.
How WACE fits alongside a WAF
A conventional WAF evaluates each HTTP transaction against a ruleset and accumulates an anomaly score. When the score crosses a threshold the request is blocked. WACElib does not replace this pipeline — it extends it.InitTransaction / Analyze / CheckTransaction / CloseTransaction) and receives the WAF anomaly scores as a map[string]string passed into CheckTransaction. The decision plugin sees both the ML model results and the WAF scores and returns a single bool.
The two plugin types
WACElib uses two distinct categories of plugin, each compiled to a Go shared object (.so) file.
Model plugins
A model plugin performs ML inference on a portion of the HTTP transaction. It exports an
InitPlugin or InitPluginAsync function and a Process function that accepts a ModelInput and returns ModelResults — a ProbAttack float64 and an optional data map. Each model plugin is scoped to a single ModelPluginType, which determines which phase of the transaction it receives. Multiple model plugins can run concurrently for the same transaction.Decision plugins
A decision plugin aggregates model results and WAF signals into a block/allow verdict. It exports an
InitPlugin function and a CheckResults function that accepts a DecisionInput — containing the transaction ID, a map of ModelResults keyed by model ID, per-model weights, and the WAF anomaly score map — and returns a bool. A single decision plugin is invoked per transaction, after all model plugins have finished.ModelPluginType: scoping models to transaction phases
Each model plugin is registered with aModelPluginType that declares which portion of the HTTP transaction it can analyze. When you call Analyze, you pass a type string and a list of model IDs. WACElib validates that each model’s registered type matches the string you supplied and rejects mismatches with an error.
| Value | Scope |
|---|---|
RequestHeaders | URI, method, HTTP version, and request header list |
RequestBody | Request body only |
AllRequest | Full request: headers and body together |
ResponseHeaders | Response protocol, status code, and response header list |
ResponseBody | Response body only |
AllResponse | Full response: headers and body together |
Everything | Entire transaction: both request and response |
Analyze calls per transaction — for example, one for RequestHeaders after the WAF processes request headers, and a second for RequestBody after the WAF processes the request body. Each call dispatches a separate group of model plugins concurrently.
Transaction analysis flow
The following steps describe what happens inside WACElib from the moment a new request arrives until the block/allow decision is returned.InitTransaction
Call
wace.InitTransaction(transactionID) to allocate per-transaction state: a result store (sync.Map) and a coordination channel. The transaction ID is typically a unique string generated by your WAF middleware for each HTTP request.Analyze (one call per transaction phase)
Call
wace.Analyze(modelsTypeAsString, transactionID, payload, models) for each WAF processing phase you want ML models to analyze. Internally, Analyze increments a counter on the transaction’s transactionSync struct and launches callPlugins in a new goroutine.Inside callPlugins, each model plugin in the models list is dispatched based on its execution mode:- Sync, local —
plugins.Processis called in a goroutine; the result is written directly to the transaction result store and signalled on the sync status channel. - Sync, remote — the payload is published to NATS via
plugins.AddToQueue; the result arrives back over NATS and is written to the result store. - Async — the payload is published to NATS; the result arrives independently, after
CheckTransactionmay have already been called, and is not factored into the current decision.
callPlugins waits for all synchronous models to report a result, then sends a "done" signal on the transaction’s coordination channel.CheckTransaction
Call
wace.CheckTransaction(transactionID, decisionPlugin, wafParams) to wait for every pending Analyze call to complete. Internally, CheckTransaction reads from the coordination channel once for each outstanding Analyze invocation (tracked by the atomic counter on transactionSync). Only after all "done" signals are received does it call plugins.CheckResult, which invokes the decision plugin’s CheckResults function with the accumulated model results and WAF parameters.CheckTransaction returns the decision plugin’s bool verdict and any error.Concurrent model execution
All model dispatch happens in goroutines. You can safely callAnalyze multiple times before calling CheckTransaction, and each call dispatches its model group concurrently. CheckTransaction uses an atomic counter to know exactly how many callPlugins goroutines are outstanding, and drains the coordination channel before invoking the decision plugin. This means the order in which you call Analyze does not matter — CheckTransaction always waits for all of them.
Async model plugins run on a separate NATS-based pipeline. Their results arrive after
CheckTransaction returns and are not included in the current transaction decision. Use async plugins for use cases like logging, model retraining data collection, or secondary alerting where the result is not needed synchronously.NATS for async and remote model execution
WACElib uses NATS as a message bus for two scenarios:- Remote sync plugins (
remote: truein config) — the payload is JSON-serialized and published to a NATS subject named after the model ID. A remote process subscribes, runs inference, and publishes results to<modelID>/results. WACElib waits for the result before signallingCheckTransaction. - Async plugins (
mode: asyncin config) — the payload is published to NATS and the function returns immediately. Results arrive later viaModelResultsHandler, which listens on<modelID>/resultsand stores them in a separate async channel that is not awaited byCheckTransaction.
localhost:4222 and can be overridden with the NatsURL field in ConfigFileData. WACElib attempts the NATS connection at Init time; a failed connection is logged as an error but does not prevent the process from starting.
OpenTelemetry instrumentation
WACElib emits two metrics through themetric.Meter you provide at Init:
| Metric name | Type | Description |
|---|---|---|
wace.model.duration.nanoseconds | Histogram | Latency from Analyze call to result receipt, per model ID and execution mode (sync / async). Also records the attack_probability as an attribute. |
wace.client.request.blocked.total | Counter | Incremented each time CheckTransaction returns true. Labeled with the decision plugin ID. |