Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

.nornir/provenance-diff-guide.md

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; the base attestation model in provenance-guide.md. The warehouse verbs need --features warehouse; verify-provenance also needs provenance-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