Detection engines
Engines live
Detection is where Heimdall decides what it is looking at. Rather than one giant rule set, it runs a registry of specialized engines, each an expert in one class of attack, each reading the canonical Params.
The contract
Per ADR-0002, every engine implements the same fixed interface —
Analyze(request, out) — and obeys one rule: an engine never blocks and never
decides. It only appends findings:
Finding {
Engine // which engine
Category // attack class
Risk // 0..1000, how dangerous
Confidence // 0..100, how sure
Evidence { Param, Offset, Length, Rule } // the exact canonical span + rule id
}
This separation is deliberate. Engines are pure, fast and independently testable; aggregation and enforcement are the job of the risk and decision engines. It is also what makes the Attack Explorer possible — every finding points at the precise span that triggered it and the rule that fired.
Engines are zero-allocation and fixed-point; the whole registry runs on every request without touching the heap.
The engines
| Engine | Detects (rule ids) |
|---|---|
| SQL | UNION SELECT (1001), tautologies (1002), stacked queries (1003), dangerous functions (1004), comment truncation (1005), context density (1006) — semantic tokenizer + comment stripping |
| XSS | script tags (2001), event handlers (2002), javascript: scheme (2003), data: URI (2004), dangerous tags (2005), script-context density (2006) — context-aware HTML/JS tokenizer |
| Path traversal | ../ sequences (3001), sensitive paths like /etc/passwd (3002), null byte (3003) — depth-weighted |
| Command injection | command chaining ; \| && (4001), substitution $(…) / backticks (4002), shell invocation (4003), language-level launchers system() / exec() (4004) |
| SSRF | cloud metadata 169.254.169.254 (5001), internal/RFC1918/loopback hosts (5002), dangerous schemes file:// gopher:// (5003) |
| Header | CRLF injection (6001), null byte in header (6002), Host injection (6003) |
| Protocol | missing Host (7001), unknown method (7002) — complements the HTTP-engine anomaly flags without double-counting |
| API | GraphQL introspection (8001), deep nesting (8002), mass assignment of privilege fields (8003), XXE (8004) |
| Rate | rate exceeded (9001), flood (9002) — stateful leaky bucket keyed per client |
| Auth | brute force (10001), credential stuffing (10002), JWT alg:none (10003) |
| SSI | <!--#exec (12001), <!--#include (12002), other SSI directives (12003), edge-side includes <esi:…> (12004) |
| Upload | executable extension (13001), double extension shell.php.jpg (13002), script or binary content regardless of the declared name (13003), path in filename (13004) — multipart parts only |
| Overflow | padding run (14001), format-string chain (14002), binary payload in a text field (14003, 14004) — sampled, cost independent of value size |
| SSTI | template expression (15001), runtime gadget inside one (15002), Freemarker directive (15003) |
| Deserialize | Java stream or rO0AB (16001), PHP O:len:"Class" (16002), .NET AAEAAAD///// (16003), pickle and unsafe YAML (16004) |
| NoSQL | query operators $ne $gt $regex (17001), server-side code $where $expr (17002) — read from parameter names as well as values |
Every engine emits into the same findings list, in registry order.
Families, not payload shapes
Two of the engines carry a density rule (SQL 1006, XSS 2006) that scores how much of a language a value is written in, rather than matching one shape at a time. Error-based, blind and enumeration SQL injection, or injection into an attribute where no tag is present, have no single decisive token — but they carry vocabulary in call form, enumeration clauses and tautology fragments that ordinary input does not. No signal fires alone: prose says "select", "order by phone" and "having trouble"; an injection says several at once, in syntax prose does not use.
The same idea shapes the others. A traversal sequence is suspicion, a traversal
target (/etc/passwd) is proof. Command injection is appended to a value
(127.0.0.1;id) while prose spaces its separators (search & find). The point is
to recognize a family by its structure, not to enumerate its members.
Scan windows
Each value gets a guaranteed head window, and long values are followed up from the end — where a payload pushed past the window by padding necessarily lands — under a per-request budget shared across all parameters. The budget is what keeps this bounded: scanning proportional to input would hand an attacker a cheap way to spend CPU. A payload buried in the middle of a very large value, padded on both sides, remains out of reach; covering it means scanning everything, and scanning everything is the denial of service.
Custom rules and access control
Two more sources feed the same findings pipeline:
- Custom-rule engine — operator-defined and signed-feed rules
(contains / equals / prefix / RE2 regex over path, query, param, header or body),
each adding its
scoreon a hit. This is how you extend detection without writing Go. See Policy & sites. - Access control — allow/deny by IP, CIDR or country plus per-client rate and adaptive auto-ban. It runs before detection and can short-circuit a request to a block, or allowlist it past the WAF entirely.
Plugin findings
Out-of-process plugins contribute findings too, and they flow through the identical risk pipeline. To keep the boundary honest, engine IDs ≥ 128 are reserved for plugins, and the plugin host drops any plugin finding that claims a core engine ID — a plugin can never impersonate the SQL engine. See Intelligence plugins.
Status
Sixteen engines plus the custom-rule engine and access control are registered, zero-allocation and live. A dedicated Bot detection engine is still planned; bot handling today runs through the challenge tier and the Browser Identity plugin's UA/fingerprint findings.
Known gaps are recorded rather than papered over: remote file inclusion over
http:// or ftp:// is indistinguishable from a legitimate redirect target
without knowing what an endpoint normally receives, and repeated-unit padding
cannot be separated from a bulk data export by size or character diversity. Both
belong to endpoint intelligence, which knows what a given parameter normally
looks like.