Skip to content

CLI and API reference

CLI

tracegate <command>; every command prints JSON unless stated. Signing keys come from TRACEGATE_KEYID, TRACEGATE_SIGNING_KEY (Ed25519 PEM) or the HMAC demo key; the gate trusts TRACEGATE_PUBKEY.

Command Purpose
synth <scenario> <out> write a signed synthetic scenario (clean, malicious-dep, cve-origin, unreachable, tampered, unsigned-missing)
gate <events> [--comment] [--osv zip] [--top-pypi] [--reach-repo dir --reach-src ... --reach-manifest f] verdict; exit 1 on block
backtrack <events> <CVE or package> origin story: commit, PR, author, builds, blast radius
blast <events> <layer-digest-prefix> images, services and containers inheriting a layer
ingest [--syft f [--cyclonedx]] [--trivy f]... [--sarif f]... --commit sha -o out sign unmodified tool output as stage events
lineage <repo> <manifest> [--limit n] -o out commit events from manifest / lock-file history
merge <in>... -o out concatenate envelope files
export <events> --format json\|cypher\|opa\|intoto graph exports
keygen <prefix> Ed25519 key pair
serve [--host --port] FastAPI service and lineage explorer
demo, bench run the spec scenarios / synthetic scale check

HTTP API (tracegate serve)

Method Path Returns
GET / lineage explorer UI
GET /healthz {"status": "ok"}
POST /v1/gate body: list of DSSE envelopes; returns verdict summary and run id (422 on malformed envelopes)
POST /v1/demo/{scenario} run a synthetic scenario
GET /v1/runs stored runs
GET /v1/runs/{run}/graph nodes, edges, findings, decision
GET /v1/runs/{run}/backtrack?q= origin stories
GET /v1/runs/{run}/blast?layer= layer blast radius
GET /v1/runs/{run}/cypher Neo4j Cypher script

Python API

tracegate.pipeline

End-to-end: envelopes -> verified graph -> enrichment -> gate decision.

run(envelopes, trusted, warden=None)

Verify envelopes, build the graph, enrich it and evaluate the policy.

Parameters:

Name Type Description Default
envelopes list[Envelope]

Signed stage events.

required
trusted dict[str, bytes]

Key id to trusted key; envelopes signed by any other key are rejected.

required
warden WardenClient | None

Optional malicious-package scorer.

None

Returns:

Type Description
tuple[CollectResult, Decision]

The collection result and the gate decision.

save_envelopes(envs, path)

Write envelopes to a JSON file.

Parameters:

Name Type Description Default
envs list[Envelope]

Envelopes to write.

required
path str | Path

Output path.

required

load_envelopes(path, lenient=False)

Read envelopes from a JSON file (TRACEGATE or standard DSSE layout).

Parameters:

Name Type Description Default
path str | Path

Input path.

required
lenient bool

Turn a malformed entry into a placeholder the collector rejects (so the gate blocks with a reason) instead of raising.

False

Returns:

Type Description
list[Envelope]

The envelopes, not yet verified.

Raises:

Type Description
SignatureError

An entry is not a well-formed envelope (strict mode), or the file is not a list.

tracegate.ingest

Adapters from real tool output to TRACEGATE stage payloads.

syft -o syft-json -> build payload (image, layers, SBOM artifacts with purls) syft -o cyclonedx-json -> build payload (components with purls; no layer info) trivy --format json -> scan payload (vulns with PkgIdentifier.PURL and Layer.DiffID)

Only the fields the collector needs are kept, so a large real scan becomes a small, signable event. Unknown/missing fields degrade gracefully.

TRIVY_TYPE_ECOSYSTEM = {'alpine': 'apk', 'wolfi': 'apk', 'chainguard': 'apk', 'debian': 'deb', 'ubuntu': 'deb', 'redhat': 'rpm', 'centos': 'rpm', 'rocky': 'rpm', 'alma': 'rpm', 'amazon': 'rpm', 'oracle': 'rpm', 'fedora': 'rpm', 'photon': 'rpm', 'cbl-mariner': 'rpm', 'azurelinux': 'rpm', 'suse linux enterprise server': 'rpm', 'opensuse.leap': 'rpm', 'opensuse.tumbleweed': 'rpm', 'python-pkg': 'pypi', 'pip': 'pypi', 'pipenv': 'pypi', 'poetry': 'pypi', 'uv': 'pypi', 'npm': 'npm', 'yarn': 'npm', 'pnpm': 'npm', 'node-pkg': 'npm', 'bun': 'npm', 'gomod': 'golang', 'gobinary': 'golang', 'cargo': 'cargo', 'rust-binary': 'cargo', 'bundler': 'gem', 'gemspec': 'gem', 'nuget': 'nuget', 'dotnet-core': 'nuget', 'packages-props': 'nuget', 'jar': 'maven', 'pom': 'maven', 'gradle': 'maven', 'sbt': 'maven', 'composer': 'composer', 'composer-vendor': 'composer', 'conan': 'conan', 'cocoapods': 'cocoapods', 'swift': 'swift', 'pub': 'pub', 'hex': 'hex', 'conda-pkg': 'conda'} module-attribute

__all__ = ['syft_json_to_build', 'cyclonedx_to_build', 'trivy_json_to_scan', 'trivy_ecosystem', '_eco_from_purl'] module-attribute

trivy_ecosystem(result_type)

purl type for a Trivy result Type (alpine -> apk, python-pkg -> pypi), else None.

syft_json_to_build(src, build_id, commit=None)

Convert Syft JSON output into a build stage payload.

Parameters:

Name Type Description Default
src str | Path | dict

Path to, or parsed, syft -o syft-json output.

required
build_id str

Build identifier.

required
commit str | None

Commit SHA the build was made from.

None

Returns:

Type Description
dict[str, Any]

A build payload with image, layers and packages.

cyclonedx_to_build(src, build_id, commit=None)

Convert a CycloneDX JSON SBOM into a build stage payload.

Parameters:

Name Type Description Default
src str | Path | dict

Path to, or parsed, CycloneDX JSON.

required
build_id str

Build identifier.

required
commit str | None

Commit SHA the build was made from.

None

Returns:

Type Description
dict[str, Any]

A build payload with the components as packages.

trivy_json_to_scan(src)

Convert trivy --format json output into a scan stage payload.

Parameters:

Name Type Description Default
src str | Path | dict

Path to, or parsed, Trivy JSON.

required

Returns:

Type Description
dict[str, Any]

A scan payload with one finding per vulnerability and package.

tracegate.sarif

SARIF 2.1.0 -> sast stage-event payload.

Works with the SARIF emitted by Semgrep, Bandit (-f sarif), CodeQL and most other static analysers. Severity comes from, in order: properties.security-severity (CVSS-like 0-10, used by CodeQL/GitHub), the rule's defaultConfiguration.level, then the result level. SARIF levels map error->high, warning->medium, note/none->low.

__all__ = ['sarif_to_sast'] module-attribute

sarif_to_sast(src, commit)

Convert a SARIF 2.1.0 log into a SAST stage payload.

Parameters:

Name Type Description Default
src str | Path | dict

Path to, or parsed, SARIF JSON.

required
commit str

Commit SHA the analysis ran on.

required

Returns:

Type Description
dict[str, Any]

A SAST payload with one finding per result.

tracegate.lockfiles

Pinned-version parsers for lock files, dispatched on the manifest file name.

Supported

PyPI : requirements*.txt (pip / pip-compile), poetry.lock, uv.lock npm : package-lock.json (v1-v3), yarn.lock (classic v1 and Berry v2+), pnpm-lock.yaml (v5-v9) Go : go.mod (require directives), go.sum (highest listed module version) Cargo : Cargo.lock

pin_entries(manifest, text) returns every pinned (name, version) with the 0-based line that carries the version and the line that names the package. Lock files that hold several versions of one name (yarn, pnpm, package-lock, Cargo/poetry/uv) yield one entry per version, so a package that ships as both debug 2.6.9 and debug 4.3.4 is two pins. parse_manifest_entries is derived from it, and gitlineage.blame_entry_introducers uses the same line map on git blame output, so the benchmark's ground truth and the collector always agree on which line represents a pin.

pin_lines / parse_manifest are the older one-version-per-name views ({name: (version, line)}; the first entry in file order wins for lock files). They are kept for callers that only need names (reachability) and to reproduce the pre-fix benchmark figures.

Readers are small line scanners: no YAML/TOML dependency, works on Python 3.10.

Pins = dict[str, tuple[str, int]] module-attribute

ECOSYSTEM = {'package-lock.json': 'npm', 'npm-shrinkwrap.json': 'npm', 'yarn.lock': 'npm', 'pnpm-lock.yaml': 'npm', 'go.mod': 'golang', 'go.sum': 'golang', 'Cargo.lock': 'cargo'} module-attribute

__all__ = ['Entry', 'parse_manifest', 'parse_manifest_entries', 'parse_package_lock', 'parse_toml_packages', 'ecosystem_for', 'pin_entries', 'pin_lines', 'semver_key', 'yarn_lock_lines', 'yarn_lock_entries', 'pnpm_lock_lines', 'pnpm_lock_entries', 'package_lock_entries', 'package_lock_pairs', 'toml_package_entries', 'go_mod_lines', 'go_sum_lines'] module-attribute

Entry

Bases: NamedTuple

One pinned (name, version) of a manifest.

Attributes:

Name Type Description
name str

Normalised package name.

version str

Pinned version.

line int

0-based line that carries the version (what git blame is run on).

key_line int

0-based line that names the package (equal to line for one-line pins).

name instance-attribute
version instance-attribute
line instance-attribute
key_line instance-attribute

ecosystem_for(manifest)

Ecosystem of a manifest from its file name (pypi when unknown).

requirements_lines(text)

Exact name==version pins of a requirements file with their line numbers.

toml_package_entries(text, eco='pypi')

poetry.lock / uv.lock / Cargo.lock: name+version of every top-level [[package]] table.

toml_package_lines(text, eco='pypi')

One version per name of a TOML lock file (first [[package]] table wins).

package_lock_entries(text)

lockfileVersion 2/3 pretty-printed packages map; None if the layout is not recognised.

package_lock_lines(text)

One version per name of a pretty-printed package-lock (first path wins).

package_lock_pairs(text)

Every (name, version) of an npm package-lock.json (lockfile v1, v2 or v3), any layout.

parse_package_lock(text)

Pins of an npm package-lock.json (lockfile v1, v2 or v3).

Returns:

Type Description
dict[str, str]

Package name to version; one version per name.

yarn_lock_entries(text)

yarn.lock v1 (version "1.2.3") and Berry (version: 1.2.3): one entry per block.

yarn_lock_lines(text)

One version per name of a yarn.lock (first block wins).

pnpm_lock_entries(text)

Every packages: key of a pnpm-lock.yaml (peer-suffixed copies collapse to one).

pnpm_lock_lines(text)

One version per name of a pnpm-lock.yaml (first key wins).

go_mod_lines(text)

require entries of a go.mod with their line numbers.

semver_key(v)

Order Go/npm/Cargo semver strings (leading 'v' ignored, build metadata dropped, pre-releases before the release, Go pseudo-versions compare by timestamp).

go_sum_lines(text)

Module versions of a go.sum (one per module) with their line numbers.

pin_lines(manifest, text)

{name: (version, line)}; None when the format has no line-level mapping (npm v1 JSON).

pin_entries(manifest, text)

Every pinned (name, version) with its lines; None when the format has no line map (npm v1 JSON).

Lock files keep every version of a name. requirements files and go.mod keep one pin per name (the last line wins, as pip and Go do); go.sum keeps the highest version per module (go.mod is authoritative for Go).

parse_manifest_entries(manifest, text)

Every pinned (name, version) of any supported manifest or lock file, in file order.

parse_toml_packages(text)

Pins of a TOML lock file with [[package]] tables (such as Cargo.lock).

parse_manifest(manifest, text)

Pins of any supported manifest or lock file.

Parameters:

Name Type Description Default
manifest str

File path; its name selects the parser.

required
text str

File contents.

required

Returns:

Type Description
dict[str, str]

Package name to pinned version.

tracegate.gitlineage

Commit-stage collector for real git repositories.

Walks the first-parent history of a pinned dependency manifest (requirements.txt / pip-compile output, package-lock.json, yarn.lock, pnpm-lock.yaml, go.mod, go.sum, Cargo.lock, poetry.lock, uv.lock) and emits one commit stage event per commit that changed a pin: which (package, version) pairs it introduced and which it removed. This is the "commit -> dependency" half of the lineage, derived from the repository itself rather than from CI metadata.

Pins are (name, version) pairs, so a lock file that ships two versions of one package (debug 2.6.9 and debug 4.3.4) tracks both; multi_version=False reproduces the older one-version-per-name behaviour (first entry wins) for before/after comparisons.

blame_entry_introducers is the line-based reference the benchmark compares against: git blame --first-parent on the manifest line of each pinned (package, version).

Pair = tuple[str, str] module-attribute

ManifestCommit dataclass

One first-parent commit that changed a manifest, with the pins after it.

Attributes:

Name Type Description
pins dict[str, str]

One version per name (first entry in file order), for callers that need names only.

entries frozenset[Pair]

Every pinned (name, version) after this commit.

added list[Pair]

Pairs present after this commit and not before it, sorted.

removed list[Pair]

Pairs present before this commit and not after it, sorted.

sha instance-attribute
author instance-attribute
timestamp instance-attribute
subject instance-attribute
pins instance-attribute
added = field(default_factory=list) class-attribute instance-attribute
removed = field(default_factory=list) class-attribute instance-attribute
path = '' class-attribute instance-attribute
entries = frozenset() class-attribute instance-attribute
pr property

Pull-request number parsed from the commit subject, if any.

__init__(sha, author, timestamp, subject, pins, added=list(), removed=list(), path='', entries=frozenset())

parse_requirements(text)

Pinned name==version lines -> {normalised name: version}. Ignores -r, -e, URLs, hashes.

direct_deps_from_pip_compile(text)

pip-compile # via -r requirements.in annotations -> direct deps (None if not pip-compile).

follow_log(repo, manifest, rev='HEAD')

Oldest-first (sha, author, ts, subject, path-at-that-commit) for first-parent commits touching manifest, following renames (a manifest moved from requirements.txt to requirements/main.txt keeps its history, like git blame does).

manifest_pairs(path, text, multi_version=True)

Pinned (name, version) pairs of a manifest; one per name when multi_version is False.

manifest_history(repo, manifest, rev='HEAD', limit=None, multi_version=True)

Oldest-first list of first-parent commits that changed a pin in manifest (renames followed).

Parameters:

Name Type Description Default
repo str | Path

Repository path.

required
manifest str

Manifest path at rev.

required
rev str

Revision to walk back from.

'HEAD'
limit int | None

Keep only the newest limit manifest commits.

None
multi_version bool

Track every version of a name (default). False keeps one version per name (the first entry), the behaviour before the multi-version fix.

True

Returns:

Type Description
list[ManifestCommit]

Commits that added or removed at least one (name, version) pin.

commit_events(history, manifest, run_prefix='git')

Turn a manifest history into commit stage events.

Parameters:

Name Type Description Default
history list[ManifestCommit]

Commits that changed the manifest, oldest first.

required
manifest str

Manifest path, used to pick the ecosystem.

required
run_prefix str

Prefix for the synthetic run ids.

'git'

Returns:

Type Description
list[StageEvent]

Unsigned stage events, one per commit.

blame_introducers(repo, manifest, rev='HEAD')

{normalised package: sha of the first-parent commit that last wrote its pin line}.

One version per name (the first entry of a lock file); see blame_entry_introducers for every (name, version).

blame_entry_introducers(repo, manifest, rev='HEAD', multi_version=True)

{(normalised package, version): sha of the first-parent commit that last wrote its pin line}.

The pin line comes from lockfiles.pin_entries, so every supported format (requirements, package-lock v2/v3, yarn, pnpm, go.mod, go.sum, Cargo/poetry/uv locks) gets the same reference: the commit that last wrote the line holding that version. With multi_version=False only the first entry per name is kept (the older behaviour).

pickaxe_exact(repo, manifest, token, rev='HEAD')

Newest first-parent commit up to rev that changed how often token occurs in the manifest.

git log --first-parent -1 -S<token>; token may span several lines.

pickaxe_first_mention(repo, manifest, name, rev='HEAD')

Naive baseline: oldest first-parent commit whose manifest diff mentions the package name.

safe_relpath(path)

True for a relative tree path with no '..', '.', '.git', backslash or drive component.

materialize(repo, sha, prefixes, dest, suffixes=('.py',), names=('Dockerfile', 'Procfile'), anywhere=())

Write the files under prefixes (plus top-level names) as of sha into dest.

Works on blobless clones: missing blobs are fetched in batches first. Used to run static reachability against the sources that actually shipped with a historical snapshot. anywhere are basename globs (e.g. reach.ENTRYPOINT_FILES) matched across the whole tree.

tracegate.backtrack

Finding -> origin commit backtracking and blast-radius analysis.

blast_radius(g, node_id)

List everything downstream of a node.

Parameters:

Name Type Description Default
g ProvenanceGraph

Verified provenance graph.

required
node_id str

Node to start from, usually a dependency or layer.

required

Returns:

Type Description
dict[str, list[str]]

Downstream node ids grouped by kind (images, services, containers, ...).

origin_story(g, query)

query: a CVE id, finding id, or package name. Returns one story per matching finding.

layer_blast_radius(g, layer_digest_prefix)

Blast radius of the image layer whose digest starts with a prefix.

Parameters:

Name Type Description Default
g ProvenanceGraph

Verified provenance graph.

required
layer_digest_prefix str

Leading characters of the layer digest.

required

Returns:

Type Description
dict[str, list[str]]

Downstream node ids grouped by kind.

tracegate.policy

Deterministic Python policy DSL, mirrored by policies/tracegate.rego (parity checked in CI, ADR 0003).

Each rule inspects the enriched CollectResult and yields (verdict, reason dict). The overall verdict is the max over rules; every reason carries its graph path. No ML / LLM participates in the decision.

Rule = Callable[[CollectResult], Iterator[tuple[Verdict, dict]]] module-attribute

RULES = [] module-attribute

SCANNERS = frozenset({'trivy', 'grype', 'osv'}) module-attribute

rule(fn)

Decorator that registers a policy rule.

provenance_integrity(res)

Block on rejected attestations and missing required stages.

malicious_dependency(res)

Block on dependencies flagged as malicious.

vulnerable_dependency(res)

Block on reachable high or critical CVEs; warn on the rest.

unattributed_finding(res)

Block on HIGH/CRITICAL scanner findings that match no SBOM node; warn on the rest.

A finding the graph cannot attribute has no path, no reachability and no origin, so it cannot be triaged; it must not vanish from the verdict either.

sast(res)

Turn SAST findings into reasons by severity.

evaluate(res, rules=None)

Apply policy rules and combine them into one verdict.

Parameters:

Name Type Description Default
res CollectResult

Collection result.

required
rules list[Rule] | None

Rules to apply; the registered rules when None.

None

Returns:

Type Description
Decision

The strictest verdict and every reason.

pr_comment(d)

Render a decision as a Markdown pull-request comment.