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

mikronova/modgunn

tests/cli_provenance.rs

//! CLI glue tests for `warehouse verify-provenance` that do NOT need a real sealed
//! blob — the flag-parsing + guard/bail path in `src/main.rs`
//! (`read_der_roots` empty-roots bail, the `--sha256` + `--blob-dir` guard, and the
//! "no blobs found" bail). The positive verify + policy-score path is driven
//! end-to-end at the library level (`provenance::verify_tests`), where a fixture can
//! be sealed with the upstream crates; here we only prove the CLI rejects malformed
//! invocations with a clear, non-zero exit.
//!
//! Gated on `provenance-verify` (the feature that compiles the verify verb).
#![cfg(all(feature = "provenance-verify", feature = "warehouse"))]

use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn temp_dir(tag: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let dir = std::env::temp_dir().join(format!("modgunn-provcli-{tag}-{nanos}"));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

fn run(args: &[&str]) -> (String, String, i32) {
    let out = Command::new(env!("CARGO_BIN_EXE_modgunn"))
        .args(args)
        .output()
        .expect("spawn modgunn binary");
    (
        String::from_utf8_lossy(&out.stdout).into_owned(),
        String::from_utf8_lossy(&out.stderr).into_owned(),
        out.status.code().unwrap_or(-1),
    )
}

/// An empty `--roots` directory yields no certs → the verifier bails non-zero rather
/// than silently accepting an unverifiable blob.
#[test]
fn empty_roots_dir_bails() {
    let base = temp_dir("emptyroots");
    let roots = base.join("roots"); // empty dir
    std::fs::create_dir_all(&roots).unwrap();
    let blob = base.join("x.build-provenance.znippy");
    std::fs::write(&blob, b"not a real seal").unwrap();
    let wh = base.join("wh");

    let (_out, err, code) = run(&[
        "warehouse",
        "verify-provenance",
        "--warehouse",
        wh.to_str().unwrap(),
        "--roots",
        roots.to_str().unwrap(),
        "--blob",
        blob.to_str().unwrap(),
    ]);
    assert_ne!(code, 0, "empty roots must fail");
    assert!(
        err.contains("no certificate files"),
        "clear bail reason: {err}"
    );

    std::fs::remove_dir_all(&base).ok();
}

/// `--sha256` pins ONE artifact, so combining it with a `--blob-dir` that holds more
/// than one blob is a usage error — the guard fires before any verification.
#[test]
fn sha256_with_multi_blob_dir_bails() {
    let base = temp_dir("sha-guard");
    // A dummy roots file (read as bytes; the CertStore is only built during verify,
    // which the guard short-circuits before reaching).
    let roots = base.join("ca.der");
    std::fs::write(&roots, b"dummy-der-bytes").unwrap();
    // A blob dir with TWO discovered blobs.
    let blobdir = base.join("blobs");
    std::fs::create_dir_all(&blobdir).unwrap();
    std::fs::write(blobdir.join("a.build-provenance.znippy"), b"a").unwrap();
    std::fs::write(blobdir.join("b.build-provenance.znippy"), b"b").unwrap();
    let wh = base.join("wh");

    let (_out, err, code) = run(&[
        "warehouse",
        "verify-provenance",
        "--warehouse",
        wh.to_str().unwrap(),
        "--roots",
        roots.to_str().unwrap(),
        "--blob-dir",
        blobdir.to_str().unwrap(),
        "--sha256",
        &"a".repeat(64),
    ]);
    assert_ne!(code, 0, "--sha256 with a multi-blob dir must fail");
    assert!(err.contains("single --blob"), "clear guard reason: {err}");

    std::fs::remove_dir_all(&base).ok();
}

/// A `--blob-dir` with no `*.build-provenance.znippy` files bails "no blobs found".
#[test]
fn empty_blob_dir_bails() {
    let base = temp_dir("noblobs");
    let roots = base.join("ca.der");
    std::fs::write(&roots, b"dummy").unwrap();
    let blobdir = base.join("blobs");
    std::fs::create_dir_all(&blobdir).unwrap();
    std::fs::write(blobdir.join("unrelated.txt"), b"x").unwrap();
    let wh = base.join("wh");

    let (_out, err, code) = run(&[
        "warehouse",
        "verify-provenance",
        "--warehouse",
        wh.to_str().unwrap(),
        "--roots",
        roots.to_str().unwrap(),
        "--blob-dir",
        blobdir.to_str().unwrap(),
    ]);
    assert_ne!(code, 0, "no blobs found must fail");
    assert!(
        err.contains("no ") && err.contains("blobs found"),
        "clear bail reason: {err}"
    );

    std::fs::remove_dir_all(&base).ok();
}

// ── Positive-verify + policy-gate paths (need a REAL sealed blob) ──────────────
// Gated on `test-support`, which exposes `provenance::seal_provenance_fixture` —
// the same seal the in-crate `verify_tests` mint, now reusable out-of-crate. Run:
// `cargo test --features "warehouse test-support" --test cli_provenance`.

/// The VERIFIED branch end-to-end: seal a real fixture, drive `verify-provenance`,
/// assert it verifies + records the verdict, then read it back with `provenance-log`
/// — proving the verb's warehouse write actually landed.
#[cfg(feature = "test-support")]
#[test]
fn verify_provenance_records_verdict_and_reads_back() {
    let base = temp_dir("verifyok");
    let (blob, ca_der, art_sha) =
        modgunn::provenance::seal_provenance_fixture(&base, "nordisk-ci").unwrap();
    let roots = base.join("ca.der");
    std::fs::write(&roots, &ca_der).unwrap();
    let wh = base.join("wh");
    let wh_s = wh.to_str().unwrap().to_string();

    let (out, err, code) = run(&[
        "warehouse",
        "verify-provenance",
        "--warehouse",
        &wh_s,
        "--roots",
        roots.to_str().unwrap(),
        "--blob",
        blob.to_str().unwrap(),
        "--sha256",
        &art_sha,
    ]);
    assert_eq!(code, 0, "a genuine seal verifies (stderr: {err})");
    assert!(
        out.contains("VERIFIED"),
        "the blob is reported VERIFIED: {out}"
    );
    assert!(
        out.contains("1 verdict(s) recorded"),
        "the verdict is recorded: {out}"
    );

    // Read it back from the ledger (single-writer — the verify subprocess dropped
    // its lock on exit).
    let (log, _e, lcode) = run(&["warehouse", "provenance-log", "--warehouse", &wh_s]);
    assert_eq!(lcode, 0, "provenance-log exits 0");
    assert!(
        log.contains("VERIFIED"),
        "the verified row is read back: {log}"
    );
    assert!(
        log.contains("1 verified"),
        "the ledger holds one verified verdict: {log}"
    );

    std::fs::remove_dir_all(&base).ok();
}

/// The policy-gate exit-3 path: a genuinely-sealed L2 fixture fails a `--min-slsa 3`
/// requirement, so the verb emits a policy finding and exits 3 (distinct from the
/// exit-2 usage errors above) — the signal CI/promotion blocks on.
#[cfg(feature = "test-support")]
#[test]
fn verify_provenance_policy_gate_exits_3() {
    let base = temp_dir("verifygate");
    let (blob, ca_der, _art_sha) =
        modgunn::provenance::seal_provenance_fixture(&base, "nordisk-ci").unwrap();
    let roots = base.join("ca.der");
    std::fs::write(&roots, &ca_der).unwrap();
    let wh = base.join("wh");

    let (out, _err, code) = run(&[
        "warehouse",
        "verify-provenance",
        "--warehouse",
        wh.to_str().unwrap(),
        "--roots",
        roots.to_str().unwrap(),
        "--blob",
        blob.to_str().unwrap(),
        // The seal attests SLSA L2; requiring L3 must trip the gate.
        "--min-slsa",
        "3",
    ]);
    assert_eq!(code, 3, "a provenance-policy violation exits 3: {out}");
    assert!(
        out.contains("policy"),
        "a policy finding line is printed: {out}"
    );
    assert!(
        out.contains("policy finding(s)"),
        "the policy summary is printed: {out}"
    );
    // The seal itself is valid — the FAILURE is the policy, not the crypto.
    assert!(out.contains("VERIFIED"), "the crypto still verifies: {out}");

    std::fs::remove_dir_all(&base).ok();
}