Skip to main content

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.

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.

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.
HTTP request


┌─────────────┐    anomaly scores
│  WAF engine │ ─────────────────────────────────────┐
│ (ModSec /   │                                      │
│  Coraza)    │ HTTP payload                         │
└─────────────┘     │                                │
                    ▼                                ▼
             ┌─────────────────────────────────────────────┐
             │                  WACElib                     │
             │                                             │
             │  model plugins ──► decision plugin ──► bool │
             └─────────────────────────────────────────────┘
The WAF engine runs its rule evaluation independently. WACElib receives the raw HTTP payload over the transaction lifecycle API (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 a ModelPluginType 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.
ValueScope
RequestHeadersURI, method, HTTP version, and request header list
RequestBodyRequest body only
AllRequestFull request: headers and body together
ResponseHeadersResponse protocol, status code, and response header list
ResponseBodyResponse body only
AllResponseFull response: headers and body together
EverythingEntire transaction: both request and response
You can make multiple 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.
1

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.
2

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, localplugins.Process is 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 CheckTransaction may 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.
3

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.
4

CloseTransaction

Call wace.CloseTransaction(transactionID) to release all per-transaction resources: the result store, the sync model channels, and the coordination channel. This step must always run — even when an error occurs — to prevent goroutine and memory leaks.

Concurrent model execution

All model dispatch happens in goroutines. You can safely call Analyze 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: true in 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 signalling CheckTransaction.
  • Async plugins (mode: async in config) — the payload is published to NATS and the function returns immediately. Results arrive later via ModelResultsHandler, which listens on <modelID>/results and stores them in a separate async channel that is not awaited by CheckTransaction.
The NATS server URL defaults to 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.
If you use remote or async model plugins and the NATS connection is unavailable, those plugins will silently fail to deliver results. Monitor the wace.model.duration.nanoseconds OpenTelemetry histogram and the WACElib log output to detect NATS connectivity issues in production.

OpenTelemetry instrumentation

WACElib emits two metrics through the metric.Meter you provide at Init:
Metric nameTypeDescription
wace.model.duration.nanosecondsHistogramLatency 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.totalCounterIncremented each time CheckTransaction returns true. Labeled with the decision plugin ID.

Build docs developers (and LLMs) love