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

nordisk/gunnar

xtask/tests/gix_advisory_floor.rs

//! **No committed lock resolves a gix with GHSA-q54r-jf2p-rfr2** (lane OSSF-SILVER-CODE,
//! row GIX-090, 2026-10-31).
//!
//! gix 0.88.0 up to 0.90.0 let an untrusted `.git/config` turn off the `.git` path
//! protection checks (`protect_options()` unfiltered). gunnar-cli's worktree checkout
//! reaches gix through gunnar-client and nornir-git, so the advisory was the last one
//! Scorecard counted for Gunnar. The test reads every committed `Cargo.lock` of the
//! repository (the guard fixtures excepted: they are bad on purpose), not the manifests:
//! a `^0.90` requirement over a stale lock still ships the old gix.

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

const FLOOR: (u64, u64, u64) = (0, 90, 0);
const ADVISORY: &str = "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),
    )
}

/// The versions of the package named exactly `name` in a lock.
fn versions_of(lock: &str, name: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut current: Option<String> = None;
    for line in lock.lines() {
        let l = line.trim();
        if l == "[[package]]" {
            current = None;
        } else if let Some(v) = l.strip_prefix("name = ") {
            current = Some(v.trim_matches('"').to_string());
        } else if let Some(v) = l.strip_prefix("version = ") {
            if current.as_deref() == Some(name) {
                out.push(v.trim_matches('"').to_string());
            }
        }
    }
    out
}

fn locks(root: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let mut stack = vec![root.to_path_buf()];
    while let Some(d) = stack.pop() {
        let Ok(rd) = std::fs::read_dir(&d) else {
            continue;
        };
        for e in rd.flatten() {
            let p = e.path();
            let n = e.file_name().to_string_lossy().into_owned();
            if p.is_dir() {
                if !matches!(n.as_str(), "target" | ".git" | "fixtures" | "vendor") {
                    stack.push(p);
                }
            } else if n == "Cargo.lock" {
                out.push(p);
            }
        }
    }
    out.sort();
    out
}

#[test]
fn the_reader_sees_a_vulnerable_gix_and_only_gix() {
    let lock = "[[package]]\nname = \"gix\"\nversion = \"0.88.0\"\n\n[[package]]\nname = \"gix-hash\"\nversion = \"0.27.0\"\n";
    assert_eq!(versions_of(lock, "gix"), ["0.88.0"]);
    assert!(parse("0.88.0") < FLOOR && parse("0.89.0") < FLOOR && parse("0.90.0") >= FLOOR);
}

#[test]
fn no_committed_lock_resolves_gix_below_0_90() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
    let all = locks(&root);
    assert!(
        all.iter()
            .any(|l| l.parent().and_then(|p| p.canonicalize().ok()) == root.canonicalize().ok()),
        "the root lock was not found: {all:?}"
    );
    let mut bad = Vec::new();
    for lock in &all {
        let text = std::fs::read_to_string(lock).unwrap();
        for v in versions_of(&text, "gix") {
            if parse(&v) < FLOOR {
                bad.push(format!(
                    "{}: gix {v} (< 0.90.0: {ADVISORY})",
                    lock.display()
                ));
            }
        }
    }
    assert!(bad.is_empty(), "{}", bad.join("\n"));
}