Architecture¶
Overview¶
The same diagram is in the README.
flowchart TB
EX["Exports: OpenVAS, Nessus, nmap, SharpHound, topology YAML"] --> CON["Connectors to NormalizedFinding"]
INTEL["Public intel: NVD, FIRST EPSS, CISA KEV"] --> ENR["Version-to-CVE matching and enrichment"]
CON --> ENR --> G["GraphStore: attack graph with edge costs"]
G --> P["Yen k-shortest paths"]
G --> CUT["Chokepoints and exact min cut"]
P --> OPT["Optimizer: exact cut, else greedy set cover"]
CUT --> OPT --> EXP["Templated rationale and evidence"]
EXP --> OUT["CLI, FastAPI, Cytoscape explorer"]
G -.-> N4[("Neo4j mirror and GDS")]
Inputs to findings¶
flowchart TB
subgraph Exports["Offline exports"]
OV["OpenVAS / Greenbone XML"]
NE["Nessus .nessus"]
NM["nmap XML (-sV, vulners)"]
BH["SharpHound JSON"]
INV["topology / inventory YAML"]
end
subgraph Intel["Public exploit intel (local files)"]
NVD["NVD CVE 2.0 feeds"]
EPSS["FIRST EPSS"]
KEV["CISA KEV"]
end
Exports --> C["connectors: NormalizedFinding, host aliasing"]
NVD --> CPE["CPE index: NVD version ranges"]
NVD --> I["CveIntel cache"]
EPSS --> I
KEV --> I
C --> M["match-cpe: versions to CVEs"]
CPE --> M
M --> E["enrich: CVSS, EPSS, KEV"]
I --> E
ML["M11 learned exploit model (optional)"] -.-> E
Engine and outputs¶
flowchart TB
F["NormalizedFinding list"] --> G["GraphStore.build_attack_graph"]
COST["edge_cost: pure function"] --> G
G --> P["paths: Yen k-shortest on the pruned subgraph"]
G --> CUT["cuts: dominators and vertex max-flow"]
G --> INT["interdiction: Israeli and Wood MILP, greedy (for comparison)"]
P --> O["optimizer.recommend"]
CUT --> O
O --> X["explain: templates, no LLM"]
X --> CLI["CLI (JSON)"]
X --> API["FastAPI (localhost)"]
API --> UI["Cytoscape explorer and what-if"]
G -.-> N4[("Neo4j: push / pull, .cypher export, GDS Yen")]
Graph model¶
Edges point in the attacker's direction of travel, so a path is a walk the attacker takes.
flowchart TB
INT["Internet"] -- "CAN_REACH" --> SVC["Service"]
SVC -- "HAS_VULN" --> V["Vuln"]
V -- "ENABLES" --> PR["Privilege (local admin)"]
PR -- "LEADS_TO" --> H["Host"]
H -- "CAN_REACH" --> SVC
H -- "STORED_ON" --> CR["Credential"]
CR -- "GRANTS" --> PR
CR -- "HAS_ACE" --> ACE["Ace (AD ACL entry)"]
ACE -- "ABUSES" --> CR
ACE -- "ABUSES" --> PR
ACE -- "ABUSES" --> DS["DataStore (crown jewel)"]
H -- "HOLDS" --> DS
Remediable nodes are Vuln (patch or upgrade), Credential (rotate, clear cached copies), Ace (remove the ACL
entry) and non-entry, non-crown Host (segmentation rule). The frozen taxonomy lives in
contracts/graph_model.md;
v1.2 added Ace, HAS_ACE and ABUSES, v1.3 the network-reachability prerequisite of credential use.
Modules¶
| Module | Path | Notes |
|---|---|---|
| M1 connectors | src/linchpin/connectors/ |
OpenVAS, Nessus, nmap (+ vulners), BloodHound v5/v6 incl. ACEs, inventory overlay, native JSON |
| intel | src/linchpin/intel/ |
NVD 2.0 / EPSS / KEV parsers, 379k-CVE cache, enrichment, offline CPE version matching |
| M2 GraphStore | src/linchpin/graph/ |
NetworkX store; Neo4j push/pull, .cypher export, GDS Yen |
| M3 edge cost | src/linchpin/engine/edge_cost.py |
frozen formula: CVSS exploitability sub-score, EPSS, KEV floor, credential reachability |
| M4 paths | src/linchpin/engine/paths.py |
Yen k-shortest, kill-chain stages, criticality |
| cuts | src/linchpin/engine/cuts.py |
dominator chokepoints; min vertex cut by count or by effort |
| interdiction | src/linchpin/engine/interdiction.py |
exact budgeted interdiction MILP and greedy interdiction (benchmark baselines) |
| M5 optimizer | src/linchpin/engine/optimizer.py |
exact cut when within budget, else greedy set cover |
| M6 explain | src/linchpin/engine/explain.py |
deterministic templates, no LLM |
| M7 API / M10 UI | src/linchpin/api/ |
FastAPI with host allow-list and limits; Cytoscape.js explorer (also the static demo) |
| M8 CLI | src/linchpin/cli.py |
ingest, scenario, paths, fix, cuts, whatif, export, serve, ... |
| M9 synth | src/linchpin/synth/ |
four topology families using real CVE parameters |
| M11 ML | src/linchpin/ml/exploitability.py |
KEV-membership model, labels known at the cutoff, exploitation-status text masked |