# Detection engines

Telemetry is what the sensor sees. The engines on this page are what it decides.
Twelve behavioral engines and two real, industry-standard rule engines, a
SigmaHQ-format evaluator and VirusTotal's YARA-X, run on the host, observe-only,
and turn a raw event stream into named technique detections and per-process
verdicts.

The collectors (see /docs/collectors) give us visibility. On top of that stream
sits the analytic layer: hash reputation, a per-process trust tier, the Sigma
evaluator, the YARA-X scanner, a process-injection scanner, an event-chain
correlator, a critical-config watch, a ransomware behavior detector, and the
internet-scale graph reputation that scores every destination and hash the fleet
touches. Together they name 91 on-host ATT&CK techniques through 12 behavioral
engines, 79 bundled Sigma rules and a curated critical-config watch. Turn the
sensor on and every one of them ships on: the engines below are opt-out, not
opt-in, and the next section shows the exact key for each. YARA-X sits on a
different axis entirely, a build-time switch rather than a runtime one, so the
default release build still ships zero YARA rules regardless of any key. None
of this is tuned by vibes: the package carries 2,112 test functions and 93 fuzz
targets, the pure-logic deciders are mutation-tested, and the ransomware engine
is false-positive-safe by construction. The full per-technique ledger, with the
four graph-side infrastructure columns beside it, is on /docs/attack-coverage.

## Telemetry is not a verdict

This distinction is load-bearing, so we state it before anything else. Telemetry
is visibility: the exec, file, socket and DNS events the collectors capture. We
measured that at 100% across a driven Atomic Red Team run (37 of 37 techniques).
A verdict is an engine deciding that a specific pattern is worth an alert. That
analytic layer is narrower than the telemetry: on the same run, the signature
lane fired on 32 of 37 techniques (86%), and we publish both numbers rather than
let the 100% stand in for detection. We never claim an alert we cannot
demonstrate.

There is a second line that matters just as much. Every engine on this page is
observe-only: it emits a detection event onto the telemetry stream, and it never
blocks, kills or quarantines on its own. A verdict, by default, is held:
surfaced, ranked and recorded, but not acted on. On-host blocking is opt-in, and
the adversary-proof lever, the off-host kill-switch, lives on the network, not
in the sensor. So on this page you will read detected, held and observe-first,
never prevented or contained. The response levers are documented in
/docs/kill-switch.

## Opt-out, not opt-in

The default posture flipped: every engine below ships on the moment the sensor
itself is on. Turning the sensor on is the one opt-in; each engine underneath
it is its own key, and the key is read the way everything here is, liberally:
absent or `true` means on, and only an explicit `false` turns that one engine
off. Drop this into the sensor's config (`/etc/whisper/sensor.json` for a
system service, `.whisper/config` for a project) and reinstall or restart:

```
# /etc/whisper/sensor.json - every key below is ON unless you say otherwise.
{
  "sensor": {
    "enabled": true,
    "sigma": false,            // the Sigma rule evaluator
    "injection": false,        // the memory-map sweep (Linux, Windows, macOS ES)
    "eventchain": false,       // the write -> exec -> connect correlator
    "lateralmovement": false,  // T1021 remote-service correlator
    "defenseevasion": false,   // T1070 / T1562 correlator
    "collection": false,       // T1056 / T1113 / T1115 correlator
    "credaccess": false,       // T1003.001 / T1552.001 / T1555.003 correlator
    "dga": false,              // DNS qname burst classifier
    "jatap": false,            // the JA3/JA4 capture tap
    "javerdict": false,        // the JA3/JA4 graph verdict
    "yara": false              // YARA-X, where a -tags yara build carries it
  }
}
```

An existing config is honored verbatim, so none of this is retroactive: add
only the lines for what you want off, and everything else keeps running.

Two engines sit outside this switch entirely, for opposite reasons. The driver
blocklist (T1068) is wired unconditionally onto the image-load feed, so there
is no key to flip. YARA-X is gated a level up, at build time: the default
release binary never compiles the engine in, so the `yara` key above only
means anything once you build with `-tags yara` yourself, at which point it,
too, defaults on. Both are covered in the engine table below.

## The engines

Every engine below ships and runs on the shipped Linux sensor today, with the
build-tag and platform caveats noted in the status column and the exact
opt-out key from the block above.

| Engine | What it decides | Where it runs | Status |
|---|---|---|---|
| Hash verdicts | Every observed binary gets a hash verdict: known-bad from the threat graph (MalwareBazaar / ThreatFox) becomes a confirmed-bad signal, and known-good OS binaries are affirmatively cleared. | On-host clamp plus the graph | on |
| Process trust tier (T0-T4) | Grades every process from T4 (verified-signed or known-good) down to T0 (known-bad). Only a confirmed graph verdict reaches T0, never a fail-open guess. | On-host, folded with graph reputation | on |
| Sigma evaluator | Matches all four event vocabularies against Sigma rules: 79 bundled rules (34 SigmaHQ under DRL 1.1, 45 first-party under MIT), operator rules alongside, capped at 512, no cloud round-trip. | On-host, inline | on · `sigma:false` |
| YARA-X scanner | Scans settled unknown executables and injection-flagged memory regions: four rule files in the tree, zero in the default build, operator rules alongside. | On-host, build-gated (`-tags yara`), calm-scheduled | on in a `-tags yara` build · `yara:false`; absent from the default build entirely |
| Process-injection scanner (T1055 / T1620) | Classifies four injected-memory shapes from a process's own memory map, no disassembler, JIT-screened. | On-host: /proc/\<pid\>/maps sweep (Linux), the same sweep (Windows), a real-time correlator off the macOS Endpoint Security feed | on · `injection:false`; the macOS arm is also feed-gated on Apple's ES entitlement |
| Event-chain correlator (T1105) | Stitches write, then exec, then outbound connect into one narrative. | On-host, crossed with the graph | on · `eventchain:false` |
| Critical-config watch | Flags writes to 16 hard-coded persistence and priv-esc paths, each pre-tagged with its technique. | On-host | on · `configWatch:false` |
| Ransomware behavior (T1486 / T1490) | An 8-heuristic sliding-window correlator: canaries, gated entropy, mass write / rename / delete, tree rollup, low-and-slow, recovery destruction. | On-host | detection on · `ransomware:false`; the freeze responder that can act on a detection stays a separate opt-in, see below |
| Driver blocklist (T1068) | Matches the vulnerable-driver blocklist on the live image-load feed. | On-host (Windows) | on, unconditionally; no config key exists to turn it off |
| Lateral-movement correlator (T1021.001-006) | Remote-service session shapes folded over the socket and exec streams. | On-host | on · `lateralmovement:false` |
| Defense-evasion correlator (T1070 / T1562) | Two or more distinct evasion classes inside one process tree within a window. | On-host | on · `defenseevasion:false` |
| Collection correlator (T1056 / T1113 / T1115) | Screen-capture and clipboard shapes correlated with the file they staged. | On-host | on · `collection:false` |
| Credential-access correlator (T1003.001 / T1552.001 / T1555.003) | LSASS dump shape and browser credential-store reads, file-corroborated. | On-host (Windows) | on · `credaccess:false` |
| DGA classifier (T1568.002) | Burst classification over the DNS qname stream. | On-host | on · `dga:false` |
| JA3/JA4 verdict (T1573) | The TLS ClientHello fingerprint judged against the graph. | On-host (Linux) | on · `javerdict:false`; the capture tap feeding it is its own switch, `jatap:false` |
| Graph reputation | Scores every destination, hostname, IP, ASN, certificate and file hash the fleet touches, off the host, at resolve time. | Off-host (the graph) | live |

On the trust tier and egress: the T0-T4 tier is computed live for every process,
and mapping each tier to an egress freedom (none to unrestricted) is
live-enforced in the egress plane: the endpoint's worst observed process tier
drives the decision. The wider auto-contain ladder beyond that mapping stays
gated and observe-first, and the policy read-back labels every namespace as
enforcement=live or enforcement=stored.

## Two real rule engines, running natively

Most lightweight agents bundle neither YARA nor Sigma; heavyweight ones bolt on
a separate YARA integration and convert Sigma somewhere in a SIEM. Whisper runs
both, in-process, on the host: the Sigma evaluator matches single events across
all four stream vocabularies (exec as process_creation, file as file_event,
conn as network_connection, dns as dns_query), and the YARA-X engine scans files
and memory. Both carry the rule's own name, author, license and ATT&CK technique
onto every finding: the technique is read from the rule's own tag, never
invented, and a rule without one is reported as unmapped.

The bundled Sigma set is 79 rules, attribution preserved on every one: 34
upstream SigmaHQ rules bundled verbatim under the Detection Rule License 1.1,
each keeping its title, id, author, date, references and tags, and 45
first-party Whisper rules under the MIT License, written where SigmaHQ carries
nothing for a lane we ship. One
bundle carries every platform's layer side by side and load-time admission
selects the subset the running binary can observe, so the platform columns
overlap. No GPL rule packs, ever.

| The 79, by | Split |
|---|---|
| Stream | 61 process creation, 10 file event, 7 network connection, 1 DNS query |
| Platform admission | 21 Windows, 17 Linux, 59 macOS |
| Severity | 22 high, 38 medium, 18 low, 1 informational |

Named examples, so the shape is concrete: Linux Shell Pipe to Shell (T1140,
medium), Process Execution From Shared Memory Directory (T1027.011, high),
Netcat Reverse Shell Execution (T1059, high), Communication To Ngrok Tunneling
Service (T1572, high), Linux Crypto Mining Pool Connections (T1496, high), New
Cron File Created (T1053.003, low) and DNS Query to External Service Interaction
Domains (T1190 / T1595.002, high).

The YARA rules in the tree (4 files, 5 rules, every one under a permissive
license with the author's attribution kept; the default release build ships
none of them):

| Rule | Author | License | Targets |
|---|---|---|---|
| MAL_LNX_PLAGUE_BACKDOOR_Jul25 | Pezier Pierre-Henri | DRL 1.1 | the Plague Linux backdoor |
| SUSP_LNX_Linux_Malware_Indicators_Aug20_1 | Florian Roth / Nextron | DRL 1.1 | generic Linux malware indicators |
| APT_MAL_LNX_Kobalos | ESET research | BSD-2 | the Kobalos backdoor |
| APT_MAL_LNX_Kobalos_SSH_Credential_Stealer | ESET research | BSD-2 | its SSH credential stealer (T1204.002) |
| Whisper_EICAR_Test_File | Whisper Security | MIT | the EICAR self-test, so you can prove the engine end to end |

Bring your own rules. Both engines are two-layer: the embedded default set plus
an operator override directory (drop .yml Sigma rules or .yar files into the
sensor's .whisper/ rule directories; an operator rule with the same id replaces
the bundled one). Sigma is capped at 512 rules and matching stays pure in-memory
string work, so it runs inline with no scheduler involvement. YARA full-file
scans are the expensive half, so they are calm-scheduled: each scan is a
deferrable task with a 60-second starvation floor (a fresh unknown executable
must not wait long, even on a busy host), file reads are capped at 32 MiB, and
binaries already cleared as known-good OS packages skip the deep scan entirely.
The boundaries: the default public build ships the YARA engine behind the
`-tags yara` build gate, so nothing above about the `yara` key applies until
you build that way yourself; once you do, YARA-X defaults on like every other
engine on this page, and `{"sensor":{"yara":false}}` opts back out. Sigma
aggregation (multi-event) rules are skipped at load rather than
half-evaluated.

## Ransomware: eight heuristics, false-positive-safe by construction

The complaint every operator has about behavioral detection is the false alarm.
The ransomware engine (the most-tested engine in the sensor, 156 test functions)
is an 8-heuristic per-process sliding-window correlator over the file and exec
streams, and it is built so that no single ambiguous signal convicts. It fires
only when several heuristics corroborate and a destructive action is confirmed:

| Heuristic | What it takes | Note |
|---|---|---|
| Mass write | a high volume of distinct paths written by one process inside a short sliding window | volume, not content |
| Rename churn | a sustained run of moves converging on one uniform new extension | benign suffixes (.tmp, .partial, package-manager renames) screened out |
| Canary tripwires | any touch of a planted decoy file | near-proof; the canary names are derived per host with an HMAC, so an author cannot ship a grep list |
| Mass delete + backup tamper | a burst of deletes alongside execs of shred / snapshot / backup tooling | the destruction pattern, not one delete |
| Gated entropy | repeated high-entropy reads AND a destructive corroborator | the engine's only disk I/O, opened only once mass-write is already elevated; plain content EXONERATES |
| Tree rollup | per-file child processes folded into their tree owner | spawning one child per file no longer evades the per-process bars |
| Low-and-slow | a decaying per-tree accumulator | catches a paced encryptor under every per-window bar; fires only next to live confirmed encryption |
| Recovery destruction (T1490) | multiple distinct argv-classified destroy operations | fires "Inhibit System Recovery" on its own |

The cost is bounded like everything else: entropy sampling reads only a small
prefix of a few files, tightly capped host-wide, through a symlink-refusing
open. And the exoneration path is the point: a
compiler, an rsync or a package upgrade produces a mass-write burst of PLAIN
content, which actively pulls the score down instead of merely not raising it.
We proved the posture with a live-fire run that drove four workloads past the
detector at once:

| Workload | Signals | Fused verdict | Outcome |
|---|---|---|---|
| Paced mass-encrypt burst | high_entropy, mass_write, mass_delete | corroborated | Detected (T1486) |
| Canary-file rename | canary | near-proof | Detected |
| Encrypted-vault create | no corroborating signal | stays low | Silent |
| Compile and delete build | no corroborating signal | exonerated | Silent |

Zero false positives: the legitimate encryptor and the noisy compiler stayed
quiet while the paced encryptor and the canary rename fired. The five pure-logic
deciders behind this are mutation-tested at 84.21%, so the behavior is proven,
not tuned by feel. The reversible freeze that pairs with the detection (a
SIGSTOP on the offending process tree, undone by SIGCONT) is built and runs
observe-first, off by default and behind an opt-in; copy-on-write file rollback
is designed and on the roadmap, not in the tree yet.

## Process injection: the memory map is the tell

The injection scanner (124 test functions) classifies four in-memory shapes from
/proc/<pid>/maps, deliberately with no disassembler: the memory map itself is
the tell, and it keeps the engine small and dependency-clean. The four shapes: a
writable-and-executable anonymous region (rwx_anon, shellcode staging), a
private executable region with no backing file (missing_backing, the
mprotect-flip and memfd injectors), an executable region whose backing file has
been deleted (deleted_backing, the unlink-loader dropper), and a process whose
main executable is a memfd or matches no executable mapping (hollow, process
hollowing and reflective loading, T1055.012 / T1620). Each shape is strong
enough to fire on its own, and only after the structural false-positive screens
below have cleared it.

False-positive screens are structural, not statistical: a curated screen for
the 28 runtimes that map RWX by design (JVM, V8, .NET, LuaJIT and kin), a
replaced-on-disk screen so a package upgrade does not read as a deleted backing
file, and an exemption for kernel pseudo-regions. The sweep itself is
calm-scheduled: it runs as a deferrable task on an idle-priority worker, default
cadence 5 minutes, with a 30-minute starvation floor so a busy week never
becomes a blind month. Confirmed-anomalous regions are handed to YARA-X for a
byte-level second opinion.

## The event chain: write, exec, connect

The classic dropper story is three events that mean little alone and everything
together. The event-chain correlator (47 test functions) side-consumes copies
of the stream, owns no spot in it, and stitches the ordered sequence write(F),
then exec(F), then an outbound connect across process trees: the write and the
exec are linked by file identity, the exec and the connect by process identity
walked up a bounded number of spawn hops, with PID reuse fenced by process
start-ticks.

```
  write(F) --bounded window--> exec(F) --bounded window--> connect(out)
  confidence: low ................... high | certainty never claimed
  tighter timing raises confidence; a relative-path link degrades it
  the last step to certainty belongs to the graph: who owns the destination?
```

The two legs are each bounded by a short correlation window, with bonuses for
tight timing; an absolute-path link scores higher than
a basename fallback, and the whole engine does zero I/O of its own: every fact
arrives in the events. It maps to T1105, Ingress Tool Transfer.

## The graph second pass

Every engine above deliberately tops out below certainty, because the sensor's
picture ends at the host's edge. The second pass runs where the picture is
widest: the destination a chain dialed, the domain a process resolved, is scored
on the Whisper graph (roughly 7.52B nodes / 38.6B relationships) for ownership,
ASN, prevalence and threat band. In the exfiltration fusion, a connect is joined
to the nearest preceding resolve across different processes (by name, or by the
shared source-port flow key even when a shared system resolver did the lookup)
and the fused chain is scored 0 to 200: temporal proximity plus graph-verdict
severity, so a 3-second chain to a rare, newly-registered destination outranks a
3-second chain to a benign popular host. No graph signal means a pure temporal
score, never a claim of malice, and every graph call is fail-open: the local
verdicts always land, the overlay enriches when it can. The full three-graph
story is in [How the sensor works](/docs/architecture).

## Live: per-process verdicts

The verdicts are not a slide. Here is the real per-process census for one fleet
host, read over the control plane. The API key is redacted and it never leaves
the server: you send a name, the graph does the scoring, and the answer comes
back with the reputation attached.

```
# host web-edge-01  ·  /128 2a04:2a01:8dae:4521:45b8:b241:4f91:56e2
$ curl -s https://graph.whisper.online/api/query \
    -H 'content-type: application/json' \
    -H 'X-API-Key: whisper_live_...redacted' \
    --data '{"query":"CALL whisper.agents({op:\'list\', kind:\'proc\',
             address:\'2a04:2a01:8dae:4521:45b8:b241:4f91:56e2\', limit:500})"}'

envelope: op=list  ok=True  status=200  elapsed_ms=66
processes returned                        : 500
hash-verdict known-good (fleet allowlist) : 52
known-good on THIS host                   : 54
carrying a signal                         : 62

--- known-good rows (hash-verdict:known-good rides the signal list) ---
proc=1695119:87841819  trust=T3/unknown  knownGood=True  exe=/usr/bin/wg    signals=[hash-verdict:known-good,hash-verdict:known-good-onhost]
proc=1695161:87842331  trust=T3/unknown  knownGood=True  exe=/usr/bin/ip    signals=[hash-verdict:known-good,hash-verdict:known-good-onhost]
proc=1695016:87838197  trust=T2/unknown  knownGood=True  exe=/usr/bin/bash  signals=[fan-out:8,sensor-rep:32,hash-verdict:known-good,hash-verdict:known-good-onhost]

--- signal-carrying rows ---
proc=705:1107          score=48  trust=T2/unknown  signals=[fan-out:279,sensor-rep:48]
proc=1682122:0         score=48  trust=T2/unknown  signals=[fan-out:26,sensor-rep:48]
proc=1668549:0         score=48  trust=T2/unknown  signals=[fan-out:6,sensor-rep:48]
```

Known-good is not a hardcoded list. The on-host clamp is seeded from 3075
distro-verified OS-binary hashes, and the graph adds internet-scale hash
reputation the fleet's own binaries feed back into. The point is the affirmative
signal: the analyst sees 54 processes cleared as known-good on this host, not
just a wall of red.

![The Hunt surface of the operator console running op:list with kind agents: 320 rows in 324 milliseconds, a table of agent labels, forward names and /128 addresses, with a left panel of saved queries and a Run query button. The API key is not present in the page because the query runs server-side.](/shots/console-hunt.png)

Hunt, live: the same surface, here listing the fleet-identity roster (`op:list`,
`kind:agents`, 320 rows). Switch the kind to `proc` and it returns the
per-process census shown above. The query runs on the control plane, so the key
stays server-side and never reaches the browser.

## Held, not contained

When an engine fires, the detection lands in the incidents queue and in the
exposure-weighted task backlog, ranked worst-exposed-first: a running,
network-reachable finding outranks a dormant one. Its recorded state is held.
That word is deliberate. While enforcement is observe-only, we do not write
"contained" or "blocked" against a detection we have not actually actuated.

![The Incidents surface of the operator console listing detections with technique, confidence and signals, and a status column that reads held rather than contained or blocked.](/shots/console-incidents.png)

Incidents, live: detections ranked worst-first, each with its technique,
confidence and signals. The status reads held, not contained: the true state
while enforcement is observe-only.

Two response levers turn a held detection into an action, and both are
documented in /docs/kill-switch: an opt-in on-host respond tier (quarantine a
file, restart a service, the reversible freeze) and the off-host kill-switch
that revokes an identity's resolution and egress network-wide and survives a
fully compromised host.

## On the roadmap

Future, and labelled as such: ransomware copy-on-write rollback
(designed, enforcement-gated), multi-event Sigma aggregation rules
(single-event rules run today; aggregations are skipped at load, not
half-evaluated), the Windows ETW-TI tier (Windows deep detection itself is
live, proven on Windows Server 2022; the driver blocklist above is already
wired and running; ETW-TI itself is built but dark, gated on Microsoft's
anti-malware attestation), deeper in-memory evasion analytics via the macOS
Endpoint Security tier (entitlement-gated), and a public MITRE ATT&CK
Evaluation in 2027. The detectors above are live today; these extend them.
