Skip to main content

Chain Discovery Architecture

Overview

Chain discovery answers: "given this account's configuration, what can an attacker reach from any starting point?" It computes this exhaustively using formal reasoning, not by simulating attacks or sampling paths.

Three-Engine Pipeline

Snapshot (obs.v0.1)


stave export-sir --format jsonl


┌─────────────────────────────────────────────┐
│ SIR Facts (JSONL triples) │
│ (subject, predicate, object) │
│ can_assume, has_action, has_resource, │
│ trusts_service, conditions, ... │
└────────────────────┬────────────────────────┘


┌─────────────────────────────────────────────┐
│ Soufflé / Datalog │
│ │
│ Input: SIR facts as TSV relations │
│ Rules: reasoning/souffle/iam/rules.dl │
│ reasoning/souffle/discovery/ │
│ discovery.dl │
│ Output: All reachable paths with │
│ classification + condition edges │
│ │
│ Computes: transitive closure (fixpoint) │
│ Guarantee: finds ALL paths, not a sample │
└────────────────────┬────────────────────────┘


┌─────────────────────────────────────────────┐
│ Z3 SMT Solver (deferred) │
│ │
│ Input: Each path + its condition edges │
│ Query: Are all conditions on all edges │
│ simultaneously satisfiable? │
│ Output: SAT (path is viable + witness) │
│ UNSAT (path is semantically dead) │
│ │
│ Filters: structural paths → viable paths │
└────────────────────┬────────────────────────┘


┌─────────────────────────────────────────────┐
│ Deduplication │
│ │
│ Compare viable paths against 622 known │
│ chain YAML definitions. │
│ Novel = not covered by any known chain │
│ Confirmed = matches a known chain pattern │
└────────────────────┬────────────────────────┘


┌─────────────────────────────────────────────┐
│ Report │
│ │
│ Novel chains: paths nobody designed a │
│ control for. These are the discoveries. │
│ │
│ Confirmed chains: known patterns that │
│ exist in this specific account. Validates │
│ both the chain definitions and the engine. │
└─────────────────────────────────────────────┘

Why Three Engines

Each engine contributes something the others cannot:

EngineWhat It DoesWhy It's Needed
Soufflé/DatalogTransitive closure over relationshipsFinds ALL paths exhaustively. A search-based approach finds A path. Datalog finds EVERY path.
Z3Condition satisfiability per pathFilters structurally-present but semantically-dead paths. Without Z3, every structural edge is assumed traversable.
cvc5ConfirmationMulti-engine agreement eliminates solver bugs. A path verified by Z3 must also be confirmed by cvc5.

Relationship Types

The SIR facts exporter (internal/core/sirfacts/facts.go, 21 projectors) projects these relationship types from the snapshot:

Fact TypeEdge MeaningSource Projector
can_assumePrincipal can assume a roleassumeEdgeFacts, trustPolicyFacts
has_actionRole has an IAM action grantiamPolicyFacts
has_resourceRole has a resource ARN patterniamPolicyFacts
trusts_serviceRole trusts a service principaltrustPolicyFacts
has_deny_actionExplicit deny on an actiondenyPolicyFacts
has_conditionStatement has condition constraintsconditionFacts
has_typeAsset type classificationassetFacts
has_identityPrincipal identity metadataidentityFacts
has_exposureResource exposure statusexposureFacts
has_tagAsset taggingtagFacts
has_controlControl evaluation resultcontrolFacts

Security Query Classifications

The discovery engine classifies each path into query types:

QueryWhat It FindsDatalog Relation
escalationPath to a higher-privilege role (admin, wildcard actions)escalation_path
exfilRole can write to resources via assume chainexfil_path
externalExternal principal reaches internal resourcesexternal_reach
confused-deputyService trust without source conditionconfused_deputy_path
privescAny multi-hop privilege increaseprivesc_path

File Structure

reasoning/souffle/
iam/
schema.dl ← 35+ input relations (SIR fact types)
rules.dl ← effective-access derivation, ARN matching,
deny override, unauthorized_access
extract.go ← JSONL → per-predicate TSV splitter
validate.go ← G2 cross-validator (template for discovery)
discovery/
discovery.dl ← path-tracking, security classification,
condition edge output (extends iam/rules.dl)
main.go ← orchestrator (export-sir → extract → souffle → parse → dedup → report)
dedup.go ← compare paths against chains/*.yaml by risk category
report.go ← text + JSON output formatting
LIVE-VALIDATION.md ← runbook for testing against a live AWS account

Deduplication Strategy

Dedup is coarse — by risk category, not exact path. A discovered path is "confirmed" if any existing chain YAML covers the same risk class:

Discovery CategoryMatching Chain FieldMatching Values
escalationpostconditionsprivilege_escalation, admin_access, shadow_admin_access, full_account_admin
exfiltrationpostconditionsdata_access, data_exfiltration, training_data_leak
external-reachpreconditionscross_account_access, external_access
confused-deputypreconditionsconfused_deputy

A path is "novel" if no known chain covers its risk class at all.

Comparison to Offensive Agents

DimensionChain Discovery (Stave)Offensive Agent
FindsEvery path in the graphA path from a foothold
Operates onSnapshot (offline)Live environment (runtime)
CredentialsNoneRequired
CompletenessExhaustive (Datalog fixpoint)Sampled (agent exploration)
Proves absenceYes (no path = no path exists)No (no path found ≠ no path exists)
DeterministicYes (same facts = same paths)No

Chain discovery and offensive testing are complementary. Chain discovery is the screening tool (fast, complete, offline). Offensive testing is the confirmation tool (slow, targeted, live).

Running

# From observations directory
make chain-discover ARGS="-snapshot observations/"

# From pre-exported JSONL
make chain-discover ARGS="-jsonl /tmp/facts.jsonl"

# Filter by query type
make chain-discover ARGS="-snapshot observations/ -query escalation"

# Novel chains only, JSON output
make chain-discover ARGS="-snapshot observations/ -novel-only -json"

# Limit path depth
make chain-discover ARGS="-snapshot observations/ -max-hops 3"

All flags:

FlagDefaultDescription
-snapshotSnapshot observations directory
-jsonlPre-existing JSONL fact stream (mutually exclusive with -snapshot)
-controlscontrolsControls directory
-chainschainsChain YAML directory for dedup
-stave./stavePath to stave binary
-soufflesoufflePath to souffle binary
-outtempOutput directory
-now2026-01-15T00:00:00ZEval-time for deterministic output
-queryallQuery filter: all, escalation, exfil, external, confused-deputy
-novel-onlyfalseOnly show novel paths
-jsonfalseJSON output
-max-hops5Maximum path depth (1-5)