Architecture¶
1. Ingest, sign, verify
flowchart TB
subgraph SRC["Real pipeline inputs"]
GIT["git history of lock files: pip, poetry, uv, npm, yarn, pnpm, Go, Cargo"]
SY["Syft JSON / CycloneDX"]
TV["Trivy JSON"]
SA["SARIF: Semgrep / Bandit / CodeQL"]
DEP["deploy / runtime facts"]
end
GIT --> LIN["gitlineage: commit events"]
SY --> ING["ingest adapters"]
TV --> ING
SA --> ING
LIN --> ENV["DSSE envelopes: Ed25519, HMAC, or keyless (Sigstore-bound key)"]
ING --> ENV
DEP --> ENV
ENV --> COL["collector: verify, fail closed"]
COL --> G[("provenance DAG, purl + digest ids")]
2. Enrich, decide, query
flowchart TB
G[("provenance DAG")] --> EN["enrich"]
OSV[("OSV bulk dumps")] --> IDX["OsvIndex"] --> W["HeuristicWarden: MAL records + typosquat"] --> EN
RE["static / runtime reachability"] --> EN
EN --> POL["policy: Python DSL = Rego port"] --> OUT["PR comment + exit code"]
G --> BT["backtrack + blast radius"]
G --> EXP["exports: Cypher/Neo4j, in-toto SLSA, OPA input"]
G --> API["FastAPI + lineage explorer UI"]
Graph shape: commit -introduced-> dependency -installed_in-> layer -layer_of-> image -> deployment -> container, plus commit -> build -> image. Node ids are content-addressed. Dependencies are keyed by a canonical purl, so a package seen in the manifest, by Syft and by Trivy becomes one node, and a shared base layer is one node across every image (ADR 0001).
| Module | File | Notes |
|---|---|---|
| Contracts, content addressing | models.py, ids.py |
canonical purls, memoised |
| Signing | signing.py |
DSSE PAE; Ed25519 (cryptography) or HMAC; in-toto v1 / SLSA statements |
| Collector | collector.py |
verifies every envelope; missing or invalid provenance blocks |
| Real-tool ingest | ingest.py |
Syft JSON, CycloneDX, Trivy JSON (unchanged tool output) |
| Git lineage | gitlineage.py |
first-parent manifest walk, follows renames, PR numbers from subjects |
| OSV index | osv.py |
offline, streams official zip dumps; ECOSYSTEM/SEMVER ranges; CVSS v3 |
| Typosquat / Warden | typosquat.py, warden.py |
deletion-index edit distance, transposition, separator, homoglyph, suffix, combosquat; MultiWarden per ecosystem; WardenApiClient for the real Warden service |
| Reachability | reach.py, enrich.py |
runtime facts first, static fallback (ADR 0004) |
| Policy | policy.py, policies/tracegate.rego |
deterministic; Rego parity checked in CI (ADR 0003) |
| Backtrack | backtrack.py |
origin story + blast radius |
| Exports | export.py |
Neo4j Cypher, JSON, OPA input, in-toto |
| Service | api.py, ui/index.html |
FastAPI + dependency-free lineage explorer |
Reachability tiers: imported (AST imports and dotted strings), entrypoint (named in the Dockerfile, Procfile, CI or config), transitive (pip-compile # via edges, and implied framework dependencies such as django.db.backends.postgresql -> psycopg), referenced (bare string constants such as passlib's "argon2"), unreached (none of these, in a manifest that records dependency edges) and unknown (none of these, without recorded edges). Only unreached findings are downgraded.