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 score on 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.