Skip to content

Architecture

flowchart TB
  subgraph IN["Connectors - untrusted input"]
    W["Windows Sysmon + Security<br/>OTRF / Winlogbeat JSON"]
    O["OCSF 1.x"]
    C["CI/CD + git history"]
    T["CTI / reports"]
  end
  IN --> N["Normalizer<br/>validate, hash, raw_ref"]
  N --> ES[("Append-only event store<br/>hash-chained ledger")]
  N --> KG[("Knowledge graph<br/>every node/edge = claim")]
  ES --> DET["ANVIL Sigma<br/>detection"]
  ES --> PROV["REVENANT stories<br/>ROOTLINE reconstruction"]
  DET --> KG
  PROV --> KG
  KG --> COR["Correlation<br/>incidents + fused confidence"]
  COR --> KG
  KG --> INT["DRAGNET + OCCAM<br/>ACH attribution"]
  KG --> POS["VANTAGE posture<br/>what should have fired"]
  POS --> SIM["GAUNTLET<br/>dry-run emulation plan"]
  INT --> KG
  POS --> KG
  C --> SC["TRACEGATE lineage + OSV"] --> KG
  KG --> INV["Investigator<br/>read-only, cites claims"]
  KG --> LOOP["Feedback loop<br/>draft, then FP gate"]
  INV --> API["FastAPI + console UI / CLI"]
  KG -.optional.-> NEO[("Neo4j mirror")]

Engines talk to each other only through the graph and the frozen contracts (ADR-0001, ADR-0004). Order matters only because later engines read what earlier ones wrote.

Engines

slot sibling (pinned release) writes exercised on
detection ANVIL v1.1.2 Detection ALERTED_ON Process, Process EXHIBITS Technique (Sigma level -> reliability grade) real: 97 OTRF captures, APT29, APT3, LSASS campaigns
provenance REVENANT v1.1.3 technique tags from causal heuristics, PART_OF story incidents real: same
provenance ROOTLINE v1.1.2 incident membership re-derived from its own provenance graph real: APT29 (23 + 22 reconstructions), fixtures
intel DRAGNET v1.1.3 (dragnet-attribution) one competing Incident ATTRIBUTED_TO Actor real: ATT&CK campaigns, families and reports; APT29
intel OCCAM v1.1.3 one competing ATTRIBUTED_TO, UNKNOWN on false flags real: same
posture VANTAGE v1.1.2 Control MITIGATES, Detection SHOULD_DETECT, detected / missed / blind per technique real: ATT&CK + SigmaHQ + CIS v8 mapping
simulation GAUNTLET v1.1.2 (gauntlet-coverage) dry-run Atomic Red Team manifest for every missed technique real gaps (APT29)
supply chain TRACEGATE v1.1.2 Author AUTHORED Commit INTRODUCED Dependency, Vulnerability AFFECTS Dependency real: healthchecks history + OSV
supply chain STRATUM v1.1.3 (stratum-cnapp) code -> build -> image -> workload -> pod lifecycle, Zero-Trust findings, incidents STRATUM's synthetic cluster
attack path LINCHPIN v1.1.3 (linchpin-attackpath) CAN_REACH hops, Vulnerability AFFECTS Host, ranked fixes LINCHPIN's synthetic network
malware VITRINE v1.1.3 Sample EXHIBITS, ATTRIBUTED_TO MalwareFamily, Process USES Sample by hash inert synthetic samples
malware SPECIMEN v1.1.4 same, from a CAPE/Cuckoo report report mapping (unit test)
network FEINT v1.1.2 (its newest release) Host CONNECTED_TO, EXHIBITS for flows over threshold flow mapping (unit test)

The siblings are optional extras installed from those release tags (ADR-0007); none of their code is vendored or modified. throughline.engines.SIBLINGS records the commit each tag pointed to when it was validated: CI fails if an installed sibling is not that commit, and scripts/check_sibling_tags.py (run again by the release workflow before anything is built) fails when a newer release exists, a pinned tag moved, or a newer release renamed its distribution. Without the siblings the core still runs the synthetic demo, and each missing engine is reported as skipped.

The evidence model

Every node and edge is a claim: (assertion, source, method, timestamp, confidence, corroboration[]). The source carries an Admiralty reliability grade (A-F); the method is observed, inferred, predicted or stated-in-report; confidence rises with independent corroboration and falls with contradiction (ADR-0003). GET /claims/{id}/explain decomposes any confidence.

Correlation and cross-host stitching

flowchart LR
  S["signal processes<br/>(technique claims, alerts)"] --> R["story root<br/>(walk SPAWNED to a boundary process)"]
  R --> I["incident per host and root<br/>noisy-OR over independent engines"]
  I --> D["shared rare destination<br/>(non-system process, routable,<br/>not an infrastructure port)"]
  I --> L["lateral link<br/>(admin-port connection A to B, then an<br/>incident on B under a remote-execution service)"]
  D --> C["cluster across hosts"]
  L --> C

Two kinds of evidence join incidents on different hosts:

  • Shared rare destination. Members of both incidents contacted the same remote endpoint. Connections made by operating-system processes (lsass.exe, svchost.exe, backgroundtaskhost.exe, ...) do not count, nor do loopback, link-local and multicast addresses, localhost, or Windows infrastructure ports (DNS, Kerberos, LDAP, RPC and its dynamic range, NetBIOS, SMB). A destination is rare if at most three incidents share it and at most half of the hosts (minimum two) contacted it at all, counted over all traffic.
  • Lateral movement. A member on host A connected to host B on a remote-administration port (SMB, RPC, WinRM, RDP, SSH), and an incident on B starts under a remote-execution service (psexesvc.exe, wsmprovhost.exe, wmiprvse.exe, winrshost.exe) within an hour. Addresses map to hosts through the local address of each host's own outbound connections.

A cluster is a proposal with its evidence in cluster_via (for example lateral scranton->nashua psexec64.exe 10.0.1.6:135), not a proven edge. On the APT29 evaluation it joins exactly the evaluation's three lateral moves; the 1.0.0 rule (any destination shared by at most three incidents) had joined hosts through the domain controller, Azure endpoints and localhost (Evaluation).

Temporal views

Every claim carries its timestamp. throughline.temporal.as_of(kg, ts) keeps the first-order claims known at ts and recomputes confidence; replay() re-derives incidents on that view. The API's read endpoints take as_of=, and throughline investigate --as-of does the same on the command line, so an investigation can be replayed as it would have looked at any moment.