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

nordisk/gunnar

xtask/tests/cve.rs

//! The 0-known-CVE gate, seen to go red.
//!
//! *Never accept a green you have not seen red.* The gap this test
//! closes is named in the analysis as "known CVEs in our own dependencies":
//! gunnar's "0 CVE" must be **proven per release**, not asserted by
//! construction. A `cargo-deny` gate that only ever ran on a clean tree would
//! be exactly the empty green the gap warns against.
//!
//! So the gate is asserted in **both** directions, permanently:
//!
//! * **red**, against a committed fixture (`tests/fixtures/cve_vulnerable`)
//!   whose resolved `Cargo.lock` names `rsa 0.10.0-rc.18`, which carries the
//!   real, published advisory **RUSTSEC-2023-0071** (the Marvin Attack). The
//!   gate must refuse it, and must name that advisory — a gate that returned
//!   "dirty" for every input would pass a bare `assert!(!clean)` and prove
//!   nothing.
//! * **green**, against gunnar's own resolved graph with the repo's `deny.toml`.
//!   A gate that rejects every tree is the identity-value trap; it has to accept
//!   the thing it exists to accept.
//!
//! Both directions run the SAME `xtask::cve::run` the `cargo xtask cve` binary
//! and the `cargo xtask ci` step call — not a second copy of the logic (one implementation, not two).

use std::path::{Path, PathBuf};

use xtask::cve;

fn fixture_dir(name: &str) -> PathBuf {
    let p = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name);
    assert!(
        p.join("Cargo.toml").is_file(),
        "fixture manifest is missing: {}",
        p.display()
    );
    assert!(
        p.join("Cargo.lock").is_file(),
        "fixture must ship a committed Cargo.lock so cargo-deny reads a FIXED graph: {}",
        p.display()
    );
    p
}

/// **Run, or say by name that it did not.** The gate cannot be *proven* red on a machine
/// without the tool that runs it. This was a hard failure, so every `cargo test -p xtask` in the
/// toolchain image (which has no cargo-deny) was red three times over for a missing tool, not
/// for the code (g-reds 2). The gate itself is `cargo xtask ci`'s `cve` step, and that still
/// FAILS without cargo-deny: "0 CVE" that cannot be measured is still not a pass there. Here
/// the test prints `NOT RUN <name>: …` and returns, so the gap is named, not hidden.
fn runs(test: &str) -> bool {
    match cve::not_run(test, cve::tool_available()) {
        Some(line) => {
            // Straight to the process's stderr: libtest captures `eprintln!`, and a NOT RUN
            // nobody can read is the silent skip this replaces.
            use std::io::Write as _;
            let _ = writeln!(std::io::stderr(), "{line}");
            false
        }
        None => true,
    }
}

// ── RED ─────────────────────────────────────────────────────────────────────

/// A dependency tree carrying a known RustSec advisory is REFUSED, and the gate
/// says which advisory. This is the proof the deploy `door cve` only ever had
/// as a one-time note in its history ("Proven red-then-green 2026-08-31"); here
/// it is a test that runs on every `cargo test`.
#[test]
fn red_a_vulnerable_dependency_is_refused() {
    if !runs("red_a_vulnerable_dependency_is_refused") {
        return;
    }
    let dir = fixture_dir("cve_vulnerable");

    // No config override: the fixture ships its OWN deny.toml with an empty
    // `ignore`, so nothing excuses the planted advisory. Offline: the fixture's
    // deps and the advisory-db are in the warm cache, so this is hermetic.
    let report = cve::run(&dir, None, true)
        .unwrap_or_else(|e| panic!("the CVE gate could not run over the fixture: {e}"));

    assert!(
        !report.is_clean(),
        "the CVE gate ACCEPTED a tree containing rsa 0.10.0-rc.18 (RUSTSEC-2023-0071). \
         A gate that passes a known-vulnerable dependency is exactly the empty green the \
         gap warns against.\n{}",
        report.render()
    );
    assert!(
        !report.advisories.passed(),
        "the gate went red, but not on the ADVISORIES check — so it is red for the wrong \
         reason and says nothing about CVEs.\n{}",
        report.render()
    );
    let ids = report.rustsec_ids();
    assert!(
        ids.contains(&"RUSTSEC-2023-0071".to_string()),
        "the advisories check failed, but did not name RUSTSEC-2023-0071, so it is red for \
         the wrong reason. Advisories reported: {ids:?}\n{}",
        report.render()
    );
    // The red must be the advisory, not a bans artifact: bans is clean here.
    assert!(
        report.bans.passed(),
        "the fixture's bans check unexpectedly failed; the red must be the CVE, nothing \
         else.\n{}",
        report.render()
    );
}

// ── GREEN ───────────────────────────────────────────────────────────────────

/// gunnar's own tree passes the gate. This is the accept-side: a gate that
/// refused everything would pass the red test above and still be useless.
///
/// It uses the repo's real `deny.toml`, whose `[advisories] ignore` entries are
/// the four triaged-unreachable advisories in the tree (rsa's Marvin Attack;
/// wasmtime 45's three build/dev-only advisories). If a NEW upstream advisory
/// lands on a gunnar dependency, this test correctly turns red until it is
/// triaged in `deny.toml` — that is the gate doing its job, surfaced in CI. It
/// did exactly that on 2026-09-29 with RUSTSEC-2026-0316 (wasmtime 45.0.3,
/// published after the 2026-08-31 triage), red on master and every release
/// branch until the reachability was measured and written down in deny.toml.
#[test]
fn green_gunnars_own_tree_passes() {
    if !runs("green_gunnars_own_tree_passes") {
        return;
    }
    // A `siblings_present(&root)` skip stood here (2026-09-09, deleted): without
    // `../edda` and `../znippy` on disk the root workspace did not RESOLVE, and
    // cargo-deny's non-zero exit would have read as a CVE rather than a missing
    // checkout. Every one of those path dependencies is a crates.io version now, so
    // the workspace resolves from the registry alone and a non-zero exit here means
    // exactly what it says again. Deleted rather than left true-by-default: a skip
    // whose condition can no longer fire is a branch nobody will ever see run.

    let report = cve::run_gunnar()
        .unwrap_or_else(|e| panic!("the CVE gate could not run over gunnar's tree: {e}"));

    assert!(
        report.is_clean(),
        "the CVE gate REFUSED gunnar's own tree. If this is a newly-published advisory, triage \
         it in deny.toml (a dated reachability rationale) rather than muting it. Advisories \
         firing: {:?}\n{}",
        report.rustsec_ids(),
        report.render()
    );
}

/// **The `sources` check is wired, and it bites.**
///
/// `deny.toml` has declared `unknown-registry = "deny"` with a one-entry
/// `allow-registry` since it was written, and until 2026-09-11 the gate ran
/// `advisories` and `bans` and nothing else — so that policy had never once
/// been evaluated. A declared policy nobody runs is a comment, and it reads
/// exactly like an enforced one.
///
/// The red is made the way the `cve_vulnerable` fixture makes its red: not by
/// planting a bad input, but by handing the SAME gate a config that refuses the
/// input it has. `allow-registry = []` means "no registry is allowed", so
/// crates.io — where every one of gunnar's dependencies comes from — becomes an
/// unknown registry.
///
/// The other two checks are asserted GREEN in the same run, which is what makes
/// this a proof about `sources` rather than about the gate having failed
/// somehow: the override changes one section, and exactly one verdict moves.
#[test]
fn red_the_sources_check_refuses_a_registry_the_policy_does_not_allow() {
    if !runs("red_the_sources_check_refuses_a_registry_the_policy_does_not_allow") {
        return;
    }

    // `xtask` has zero dependencies on purpose, so no `tempfile`: a directory
    // named after this process, removed at the end, and never inside the
    // repository — a checker that wrote a hostile config into `deny.toml` to
    // test itself would be one panic away from leaving it there.
    let dir = std::env::temp_dir().join(format!("gunnar-cve-sources-{}", std::process::id()));
    std::fs::create_dir_all(&dir).expect("a scratch directory for the override config");
    // The repository's OWN deny.toml with one line changed. Written from
    // scratch it would differ in every section, and `advisories` and `bans`
    // would move for reasons that have nothing to do with sources — MEASURED:
    // a bare `[sources]`-only config exits 2 on all three checks.
    let real = std::fs::read_to_string(cve::workspace_root().join("deny.toml"))
        .expect("the repository's deny.toml");
    let needle = "allow-registry = [\"https://github.com/rust-lang/crates.io-index\"]";
    assert!(
        real.contains(needle),
        "deny.toml no longer allows crates.io by that spelling, so this override edits \
         nothing and the test would prove nothing"
    );
    let config = dir.join("deny.toml");
    std::fs::write(&config, real.replace(needle, "allow-registry = []"))
        .expect("write the override config");

    let report = cve::run(&cve::workspace_root(), Some(&config), true)
        .unwrap_or_else(|e| panic!("the CVE gate could not run: {e}"));
    let _ = std::fs::remove_dir_all(&dir);

    assert!(
        !report.sources.passed(),
        "`cargo deny check sources` ACCEPTED a tree whose every crate comes from a registry \
         the config allows none of. The check is not reaching the policy.\n{}",
        report.render()
    );
    assert!(
        !report.is_clean(),
        "the sources check went red and the gate still called itself clean, so `is_clean` \
         is not reading it.\n{}",
        report.render()
    );
    assert!(
        report.advisories.passed() && report.bans.passed(),
        "the override was supposed to move ONE verdict. Advisories or bans moved with it, \
         so this proves nothing about the sources check in particular.\n{}",
        report.render()
    );
}