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

nordisk/edda

xtask/tests/advisory_floor.rs

//! The SSH stack's advisory floor over edda's committed `Cargo.lock` (lane OSSF-NEXT,
//! 2026-10-30).
//!
//! `nornir-git` is the git-over-SSH leaf every product links (Gunnar, Holger's estate,
//! dwarves), so the russh it resolves is the russh they ship. Two advisories name the
//! russh 0.63 line this lock carried: GHSA-j7h2-66j4-ghjf (a server accepted a publickey
//! signature made with another algorithm than the one offered; fixed in 0.64.0) and
//! GHSA-4wc5-f2rc-q74m (unbounded allocation from crafted agent constraints; fixed in
//! 0.64.1). pageant below 0.2.3 is GHSA-g4mp-vgx3-xrvm (out-of-bounds read in
//! `MemoryMap::read`).
//!
//! The four russh advisories of 2026-10-30 are fixed only in 0.65.0: GHSA-fw5c-8223-6h42
//! (a peer-declared zero maximum packet size stalls channel writes for good; servers too),
//! GHSA-4p9f-7p9m-5vfg (unbounded allocation from EXT_INFO in the client),
//! GHSA-9wfm-jcj8-jfv9 and GHSA-vp5q-ff62-jwqv (the agent server).
//!
//! gix 0.88.0 up to 0.90.0 is GHSA-q54r-jf2p-rfr2 (an untrusted `.git/config` turns off
//! the `.git` path protection checks; fixed in 0.90.0): `nornir-git` opens and checks out
//! worktrees with it, and Gunnar's client links that (row GIX-090, 2026-10-31).
//!
//! The test reads the lock, not the manifests: a `^0.64` requirement with a stale lock
//! still builds the vulnerable version. A floor that is raised later is one row here.

use std::path::Path;

/// (crate, lowest version without a known advisory, the advisories below it)
const FLOORS: &[(&str, (u64, u64, u64), &str)] = &[
    ("russh", (0, 65, 0), "GHSA-j7h2-66j4-ghjf, GHSA-4wc5-f2rc-q74m, GHSA-fw5c-8223-6h42, GHSA-4p9f-7p9m-5vfg, GHSA-9wfm-jcj8-jfv9, GHSA-vp5q-ff62-jwqv"),
    ("pageant", (0, 2, 3), "GHSA-g4mp-vgx3-xrvm"),
    ("gix", (0, 90, 0), "GHSA-q54r-jf2p-rfr2"),
];

fn parse(v: &str) -> (u64, u64, u64) {
    let core = v.split(['-', '+']).next().unwrap_or(v);
    let mut it = core.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
    (it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0))
}

/// Every `(name, version)` in a Cargo.lock, without a TOML dependency.
fn packages(lock: &str) -> Vec<(String, String)> {
    let mut out = Vec::new();
    let mut name: Option<String> = None;
    for line in lock.lines() {
        let line = line.trim();
        if line == "[[package]]" {
            name = None;
        } else if let Some(v) = line.strip_prefix("name = ") {
            name = Some(v.trim_matches('"').to_string());
        } else if let Some(v) = line.strip_prefix("version = ") {
            if let Some(n) = name.take() {
                out.push((n, v.trim_matches('"').to_string()));
            }
        }
    }
    out
}

fn below_floor(lock: &str) -> Vec<String> {
    let mut bad = Vec::new();
    for (n, v) in packages(lock) {
        for (crate_name, floor, ids) in FLOORS {
            if n == *crate_name && parse(&v) < *floor {
                bad.push(format!("{n} {v} < {}.{}.{} ({ids})", floor.0, floor.1, floor.2));
            }
        }
    }
    bad
}

#[test]
fn the_reader_sees_a_vulnerable_russh() {
    // measure, never assert: the check must go red on the version it exists to refuse
    let lock = "[[package]]\nname = \"russh\"\nversion = \"0.63.3\"\n\n[[package]]\nname = \"pageant\"\nversion = \"0.2.5\"\n";
    let bad = below_floor(lock);
    assert_eq!(bad.len(), 1, "{bad:?}");
    assert!(bad[0].contains("GHSA-j7h2-66j4-ghjf"));
    assert!(below_floor("[[package]]\nname = \"russh\"\nversion = \"0.65.0\"\n").is_empty());
    // 0.64.1 fixed the agent constraints, and is still under the 2026-10-30 advisories
    let bad = below_floor("[[package]]\nname = \"russh\"\nversion = \"0.64.1\"\n");
    assert_eq!(bad.len(), 1, "{bad:?}");
    assert!(bad[0].contains("GHSA-fw5c-8223-6h42"), "{bad:?}");
}

#[test]
fn the_reader_sees_a_vulnerable_gix() {
    let bad = below_floor("[[package]]\nname = \"gix\"\nversion = \"0.88.0\"\n");
    assert_eq!(bad.len(), 1, "{bad:?}");
    assert!(bad[0].contains("GHSA-q54r-jf2p-rfr2"));
    assert!(below_floor("[[package]]\nname = \"gix\"\nversion = \"0.90.0\"\n").is_empty());
    // a gix-* plumbing crate is not gix itself
    assert!(below_floor("[[package]]\nname = \"gix-hash\"\nversion = \"0.27.0\"\n").is_empty());
}

#[test]
fn edda_lock_has_no_ssh_stack_below_its_advisory_floor() {
    let lock_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../Cargo.lock");
    let lock = std::fs::read_to_string(&lock_path).expect("edda's Cargo.lock");
    let bad = below_floor(&lock);
    assert!(bad.is_empty(), "edda's lock carries crates below their advisory floor:\n{}", bad.join("\n"));
}