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, |
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 |
key_line |
int
|
0-based line that names the package (equal to |
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 |
required |
rev
|
str
|
Revision to walk back from. |
'HEAD'
|
limit
|
int | None
|
Keep only the newest |
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.