# 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 in
> [`provenance-policy-and-diff.md`](provenance-policy-and-diff.md); the base attestation
> model in [`provenance-guide.md`](provenance-guide.md). The warehouse verbs need
> `--features warehouse`; `verify-provenance` also needs `provenance-verify`.
## The four verbs at a glance
```sh
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.
```sh
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:
```sh
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**:
```sh
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:
```sh
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).
```sh
# 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:
```sh
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
```rust
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:
```rust
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:
```rust
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
- **`--from` vs `--to` on the empty warehouse:** `--from` omitted means the *empty
beginning*; `--to` omitted 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-diff` compares 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-provenance` exits 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 → REJECTED` regression becomes visible.
- **Scoping happens after the diff**: `--repo` on `provenance-diff` filters the printed
rows/transitions; regression counting for the exit code is over the filtered set.
- `provenance-diff` and `provenance-log` need only `--features warehouse`;
`verify-provenance` additionally needs `provenance-verify` or it bails with a rebuild
hint.
</content>