Gunnar

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`](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>