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()
);
}