provenance policy & diff — guide
Class:
guide· audience: someone running the provenance CLI to gate a promotion or audit how build-provenance changed over time. The what is inprovenance-policy-and-diff.md; the base attestation model inprovenance-guide.md. The warehouse verbs need--features warehouse;verify-provenancealso needsprovenance-verify.
The four verbs at a glance
modgunn warehouse rollup --warehouse <root> [--engine a|b] [--sql]
modgunn warehouse provenance-log --warehouse <root> [--repo <r>] [--id <artifact_id>]
modgunn warehouse provenance-diff --warehouse <root> [--from <snap>] [--to <snap>] [--repo <r>] [--fail-on-regression]
modgunn warehouse verify-provenance --warehouse <root> --roots <ca-der|dir> \
(--blob <x.build-provenance.znippy> | --blob-dir <dir>) [--sha256 <hash>] \
[--min-slsa <n>] [--trusted-signer <id>]... [--expected-repo <r>] [--expected-commit <sha>]
Exit codes (all verbs): 0 success · 2 usage error (bad/missing flag, unknown subcommand) · 3 gate failure — a verify-provenance policy finding, or a provenance-diff --fail-on-regression regression.
warehouse rollup — fleet health at altitude
The org-overview verb: same fold scan prints, minus the per-finding rows.
modgunn warehouse rollup --warehouse /srv/wh
Clean fleet:
fleet clean — 0 findings across all repos
Fleet with findings (worst-health repo first):
fleet: 3 repo(s) affected, 12 finding(s) [crit 1 · high 4 · med 5 · low 2 · info 0]
payments-api health 41.7 worst critical 6 finding(s) / 4 component(s)
web-frontend health 72.0 worst high 4 finding(s) / 3 component(s)
batch-jobs health 88.5 worst medium 2 finding(s) / 2 component(s)
top components: openssl 1.1.1×3, log4j 2.14×2, libxml2 2.9×2
--engine a|b picks the scan strategy (A linear / B inverted index; unknown value warns and falls back to A). --sql uses the DataFusion pushdown when warehouse-sql is compiled (degrades to in-memory otherwise).
warehouse verify-provenance — verify a seal AND enforce a policy
Plain cryptographic verify (no policy flags) — records each verdict into the ledger and exits 0 as long as it runs:
modgunn warehouse verify-provenance --warehouse /srv/wh \
--roots /etc/modgunn/roots/ \
--blob-dir carried/ --features warehouse,provenance-verify
Add any policy flag and each verified attestation is scored against a ProvenanceRequirement; a policy miss prints the finding and makes the run exit 3:
modgunn warehouse verify-provenance --warehouse /srv/wh \
--roots /etc/modgunn/roots/ \
--blob build/app.build-provenance.znippy \
--sha256 9f2b… \
--min-slsa 3 \
--trusted-signer nordisk-ci \
--trusted-signer release-bot@nordisk \
--expected-repo codeberg.org/nordisk/holger \
--expected-commit 4a1c9de
Each flag maps to exactly one finding id (see the design doc's table):
| Flag | Finding on violation |
--min-slsa <n> |
MODGUNN-PROV-SLSA-TOO-LOW |
--trusted-signer <id>… |
MODGUNN-PROV-UNTRUSTED-SIGNER |
--expected-repo <r> |
MODGUNN-PROV-SOURCE-MISMATCH |
--expected-commit <sha> |
MODGUNN-PROV-COMMIT-MISMATCH |
--trusted-signer is repeatable (any-of). --expected-commit matches case-insensitively (4A1C9DE == 4a1c9de) and rejects a seal that attests no commit. --sha256 pins one artifact's content hash — use it with a single --blob, not a --blob-dir (that combination is a usage error, exit 2). A rejected run still records its verdicts, so the ledger captures the failure for provenance-diff to find later.
Sample output:
policy High MODGUNN-PROV-COMMIT-MISMATCH — source commit `deadbeef` ≠ expected `4a1c9de`
VERIFIED build/app.build-provenance.znippy chain ok; L3; signer nordisk-ci
1/1 blob(s) verified; 1 policy finding(s); 1 verdict(s) recorded into build_provenance
(exit 3 — the seal verified cryptographically, but the policy — the commit pin — was unmet. Verify and policy are separate layers.)
warehouse provenance-log — read back the ledger
Every recorded verdict, newest-first, plus the stored OSSF Scorecard signals:
modgunn warehouse provenance-log --warehouse /srv/wh
modgunn warehouse provenance-log --warehouse /srv/wh --repo holger
modgunn warehouse provenance-log --warehouse /srv/wh --id app@1.4.2
VERIFIED holger app@1.4.2 signer=nordisk-ci verified
REJECTED holger tool@0.9.0 signer=- tampered
scorecard some-crate 3.4/10 maintained=false signed_releases=false
Medium MODGUNN-SCORECARD-LOW — overall score 3.4 below 5.0
2 provenance verdict(s) (1 verified, 1 rejected); 1 scorecard subject(s), 1 scorecard finding(s); worst scorecard Medium
warehouse provenance-diff — what changed between two snapshots
Compares the newest verdict per artifact at two Iceberg snapshots and prints the supply-chain delta. Get snapshot ids from provenance-log context or your own watermark bookkeeping (they are the integer snapshot ids of the build_provenance table).
# everything up to snapshot s (from the empty beginning)
modgunn warehouse provenance-diff --warehouse /srv/wh --to 4310
# the delta between two points
modgunn warehouse provenance-diff --warehouse /srv/wh --from 4310 --to 4525
# scope to one repo, and BLOCK a promotion on any regression
modgunn warehouse provenance-diff --warehouse /srv/wh \
--from 4310 --to 4525 --repo holger --fail-on-regression
Defaults: --from = the empty beginning (nothing attested), --to = the current snapshot. So bare provenance-diff shows everything attested so far, all as additions.
Output — + gained an attestation, - lost one, ~ verdict transitioned:
+ skidbladnir lib@2.0.0 VERIFIED signer=nordisk-ci
- holger old-tool@0.1.0 attestation dropped
~ holger app@1.4.2 VERIFIED → REJECTED [REGRESSION] tampered
~ holger cli@3.1.0 REJECTED → VERIFIED [recovered] verified
~ holger svc@2.2.0 VERIFIED → VERIFIED [signer-changed] verified
1 added, 1 removed, 3 changed (1 regression(s))
The ~ tag is the transition kind: REGRESSION (was verified, now not — the gate-worthy one), recovered, signer-changed, or plain changed. An identical snapshot pair (or a re-verify that reproduces the same outcome) prints 0 added, 0 removed, 0 changed (0 regression(s)) — provenance unchanged.
The gate: --fail-on-regression exits 3 when at least one REGRESSION fires, so a CI promotion step can block:
if ! modgunn warehouse provenance-diff --warehouse /srv/wh --from "$LAST" --to "$NOW" --fail-on-regression; then
echo "provenance regressed since $LAST — refusing to promote"; exit 1
fi
Without --fail-on-regression the same regression still prints (tagged REGRESSION) but the verb exits 0 — report mode.
In code
use modgunn::warehouse::Warehouse;
use std::path::Path;
let wh = Warehouse::open(Path::new(root))?;
wh.ensure_modgunn_tables()?;
let delta = wh.provenance_diff(Some(from_snap), None)?; // None to = current snapshot
if !delta.regressions().is_empty() {
for t in delta.regressions() {
eprintln!("REGRESSION {}: {} → {}", t.artifact_id, t.before.reason, t.after.reason);
}
}
The pure fold takes plain slices — handy in tests / non-warehouse callers:
use modgunn::warehouse::provenance_ledger_delta;
let d = provenance_ledger_delta(&old_rows, &new_rows); // { added, removed, changed }
assert!(d.is_unchanged());
Commit-pinning in the policy layer directly:
use modgunn::provenance::{Attestation, ProvenanceRequirement};
let req = ProvenanceRequirement {
require_min_slsa: 3,
trusted_signers: vec!["nordisk-ci".into()],
expected_source_repo: Some("codeberg.org/nordisk/holger".into()),
expected_source_sha: Some("4a1c9de".into()), // case-insensitive; None = don't care
};
let (prov, findings) = req.evaluate(Some(&att)); // findings empty ⇒ policy satisfied
Gotchas
--fromvs--toon the empty warehouse:--fromomitted means the empty beginning;--toomitted means the current snapshot. They are not symmetric — a bare diff is "everything so far", not "no change".- A re-verify is not a change.
provenance-diffcompares the outcome (verified / signer / reason / blob / repo), not the capture time — re-recording the same verdict shows as unchanged. - Verify ≠ policy. A seal can verify cryptographically yet fail policy (wrong commit, low SLSA).
verify-provenanceexits 3 on the policy miss even though the chain checked out. - A rejected verify still writes a ledger row, so the failure is diffable later — that is how a
VERIFIED → REJECTEDregression becomes visible. - Scoping happens after the diff:
--repoonprovenance-difffilters the printed rows/transitions; regression counting for the exit code is over the filtered set. provenance-diffandprovenance-logneed only--features warehouse;verify-provenanceadditionally needsprovenance-verifyor it bails with a rebuild hint.