Skip to content

How it works

This page follows one finding from the pipeline to the PR comment, using the cve-origin scenario of the live demo. The demo data is synthetic (PR #42, dev@example.com); the same code path runs on the real repositories in the Evaluation.

The cve-origin scenario in the lineage explorer after backtracking CVE-2020-14343

1. Every stage emits a signed event

Each CI stage writes one stage event and wraps it in a DSSE envelope:

Stage Producer Payload
commit tracegate lineage REPO MANIFEST the pins the commit introduced or changed, its PR number and author
build tracegate ingest --syft the unchanged Syft (or CycloneDX) SBOM: packages, layers, image digest
scan tracegate ingest --trivy / --sarif the unchanged Trivy or SARIF findings
deploy, runtime your deploy job which image runs as which service, and which modules containers loaded

Envelopes are signed with Ed25519, HMAC, or (in CI) an ephemeral Ed25519 key whose public key is itself signed keylessly with Sigstore, so the gate can check that the key came from a specific GitHub workflow (Fulcio certificate plus Rekor transparency-log entry).

2. The collector verifies and fails closed

collector.py verifies every envelope against the configured trust root. An envelope with a bad signature or an unknown keyid is rejected, and a run missing any of the required commit, build and scan stages blocks with a provenance_integrity reason. With no trust root configured at all, the CLI exits 2 and the API answers 503: nothing is trusted by default.

3. The events become one content-addressed graph

Verified payloads are merged into a DAG: commit -introduced-> dependency -installed_in-> layer -layer_of-> image -> deployment -> container. Dependencies are keyed by a canonical purl, so the manifest pin, the Syft package and the Trivy finding for pyyaml==5.3 become one node, and a base layer shared by three images is one node too. In the screenshot the three image nodes share layers and builds with pyyaml==5.3.

4. Enrichment adds risk and reachability

  • OSV and Warden. OsvIndex matches pins against the offline OSV dump, including malicious-package (MAL-*) records, and the typosquat detector scores names against the ecosystem's most-downloaded packages.
  • Reachability. Runtime facts win: here two containers report that module yaml was loaded, so the finding is reachable and keeps its block. Without runtime facts, a static pass over the app's imports, entrypoints and # via edges can mark a package unreached, which downgrades block to warn but never hides the finding (see the threat model for why this is advisory on untrusted PRs).

5. The policy explains itself with a graph path

policy.py (mirrored in policies/tracegate.rego, parity-checked in CI) turns the enriched graph into a verdict. Every reason carries the path that produced it and its evidence. This is the PR comment tracegate gate --comment prints for the scenario:

## TRACEGATE: BLOCK

- **[block] vulnerable_dependency**: CVE-2020-14343
  - path: `commit:a1b2c3d4e5f6 -> dependency:pyyaml==5.3`
  - evidence: module 'yaml' loaded in ['api-7d9f-0', 'worker-7d9f-0']

The exit code is 1 on block, so the CI job fails.

6. Backtrack and blast radius

tracegate backtrack events.json CVE-2020-14343 walks the graph backwards to the commit that introduced the vulnerable version and forwards to everything that inherits it:

{
  "artifact": "pyyaml==5.3",
  "introduced_by": {"sha": "a1b2c3d4e5f6...", "author": "dev@example.com", "pr": 42},
  "builds": ["run-42-api", "run-42-web", "run-42-worker"],
  "blast_radius": {"images": ["ghcr.io/demo/api", "ghcr.io/demo/web", "ghcr.io/demo/worker"],
                   "services": ["api", "web", "worker"]}
}

The introducing commit is found by version-aware diffing of the lock-file history (which pin changed version in which first-parent commit), not by line blame, so a later commit that only re-formats or re-hashes the line does not take the credit. A CVE disclosed months after the merge still gets its origin story. The Evaluation measures this against git blame, two git log -S pickaxes and commit-message labels on real repositories.

7. Version diff vs line blame

This is the part that is new. A lock file is a list of (package, version) entries, and many commits rewrite its lines without changing any version: a package-manager upgrade, a format migration, a merge that re-sorts entries, another package's bump that moves a shared line. Line attribution (git blame, or git log -S on the pin line) credits whoever last wrote the line. TRACEGATE instead diffs the set of (package, version) pairs between consecutive first-parent commits and credits the commit where the pair appeared, keeping the newest such commit if a version was removed and re-added.

flowchart LR
  A["Dependabot PR 14636 bumps browserify-sign 4.2.0 to 4.2.1<br/>yarn v1 entry, version 4.2.1"]
  B["757d7c73c0 Upgrade to Yarn 4 (2023)<br/>same entry rewritten in Yarn 4 syntax, version 4.2.1"]
  S["later snapshot: which commit introduced browserify-sign 4.2.1?"]
  A -->|"lock file rewritten, no version change"| B
  S -.->|"git blame / pickaxe on the line"| B
  S ==>|"version diff: the pair browserify-sign@4.2.1 first appears here"| A

Two real cases from the benchmark repositories:

  • mastodon 757d7c73c0 "Upgrade to Yarn 4, remove support for Node 16 (#27073)" rewrote yarn.lock (+17,813 / -12,955 lines) without changing the versions it pins. For packages bumped before it, git blame at later snapshots names the Yarn-4 commit: in the bot-bump oracle that happens for browserify-sign 4.2.1 (Dependabot #14636), cssnano 6.0.1, yargs 17.7.2, mixin-deep 1.3.2 and the webpack-cli 3.3.12 revert. The version diff keeps the original bump (results/lineage_bot_bump_oracle.json, misses).
  • bat ansi_term 0.11.0 has been in Cargo.lock since the initial commit 8f5a80e, next to ansi_term 0.10.2. Commit 5b421b4 "Update dependencies" removed 0.10.2. The old one-version-per-name readers saw the first entry switch from 0.10.2 to 0.11.0 there and credited 5b421b4, which is wrong; git blame was right. Since the readers keep every version, the version diff credits 8f5a80e as well.

The second case is the price of the approach: it is only as good as the lock-file reader. The Evaluation reports both readers on the same data and checks every attribution against git blame, a package-specific git log -S and commit-message labels.

8. Admission

The kind-admission CI workflow shows the last step: two images are built and pushed to a localhost registry, only one is signed keylessly, and each is applied to a kind cluster only if cosign verify accepts it against the workflow identity and tracegate gate passes on its signed events. The signed image runs; the unsigned one is denied by cosign; a third image that is validly signed but whose signed provenance lacks the build and scan stages is denied by the TRACEGATE gate. Each reason is recorded in decisions.json.