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

nordisk/nornir

tests/security_isolated_live.rs

//! LIVE end-to-end proof of the "press 🛡 Security on nornir" fix.
//!
//! Reproduces the EXACT live failure the owner hit: the server scans an isolated
//! single-repo checkout of `nornir` (no sibling `skade`), so `cargo metadata`
//! couldn't load the manifest (unresolved path-deps) and the scan hard-failed.
//! With the `net-scan` fix the scanner CLONES the missing siblings first, so
//! `cargo metadata` resolves and a real SBOM comes back.
//!
//! The scan TARGET is materialized from THIS checkout via `git archive` (clean,
//! no creds), into an isolated dir with no siblings beside it.
//!
//! ## This test used to authenticate to codeberg.org with the owner's private key
//!
//! It called `prepare_path_deps(&nornir, &git, &|_| None)` with **no env
//! overrides at all**. With `url_for = None` the clone URL falls back to
//! [`security::dep_clone_base`], and the key comes from `deploykey`'s ladder.
//! MEASURED on t14s 2026-09-10, without firing it:
//!
//! ```text
//! dep_clone_base()      = ssh://git@git.gunnar.rs:2222/nordisk
//! nornir_ssh_key_path() = Some("~/.nornir/ssh/id_ed25519")
//! ```
//!
//! So a `--ignored` run — which the old docstring instructed developers to do —
//! opened the operator's real private key and used it to authenticate to a
//! remote. That is a test reaching into real credentials, and `#[ignore]` was the
//! only thing standing in front of it (`net-scan` is a DEFAULT feature, so the
//! `cfg` gate protects nobody).
//!
//! ## What isolates it now — three independent cuts, all asserted
//!
//! 1. **`$NORNIR_DEP_CLONE_BASE` → `file://` at the real sibling checkouts.**
//!    The `&|_| None` fallback is KEPT deliberately: that is the code path the
//!    oden server actually takes, and replacing it with a `url_for` closure would
//!    have tested a path production never runs. Only the destination moves.
//!    `clone_or_fetch` coerces `http(s)://` to SSH but passes `file://` through
//!    untouched, and `is_ssh_url("file://…")` is false, so the key is never read.
//! 2. **`$HOME` → a scratch dir**, which empties the ladder's two `$HOME` rungs.
//! 3. **`$DWARVES_SSH_CONFINE=1`**, which removes the rest. Rungs 3 and 6 are
//!    `/home/dwarves/.ssh` and `/home/nornir/.ssh` — ABSOLUTE, so `$HOME` cannot
//!    reach them, and on a box with a real service install this test would
//!    otherwise pick up a real SERVICE key. The confine deletes them from the
//!    ladder outright (edda `1b70392`), which is what makes this test isolated on
//!    ANY box rather than only on one where those users happen not to exist.
//!
//! [`assert_isolated`] proves all three BEFORE anything clones, so the day one of
//! them regresses this fails loudly instead of quietly borrowing a key again.
//!
//! ## And `$GIT_LFS_SKIP_SMUDGE=1`, which is fidelity, not convenience
//!
//! Moving to `file://` changed which INTERNAL transport runs, and that matters.
//! `clone_or_fetch` has two: SSH remotes go through `ssh_sync`, which
//! materialises raw and filters nothing; everything else goes through gix's
//! `main_worktree`, "the only writer in this crate that applies SMUDGE filters"
//! (nornir-git's own NAMED GAP comment, `gitio.rs`). So the codeberg-over-SSH
//! clone this test used to do never ran an LFS filter, and a `file://` one does.
//!
//! MEASURED: `skade`, `holger` and `Skidbladnir` — the three siblings with real
//! LFS-tracked files (`.pdf`, `.webp`) — failed checkout with
//! `IO error … failed to fill whole buffer`, while the six without LFS content
//! cloned fine. Without this flag the test would have been red for a reason that
//! has nothing to do with what it tests.
//!
//! `GIT_LFS_SKIP_SMUDGE=1` is git-lfs's own supported "write the pointer, not the
//! content", so it makes the `file://` path behave exactly as `ssh_sync` does in
//! production. It RAISES fidelity with the server rather than lowering it — and a
//! dependency scan wants manifests, not the bytes of a PDF.
//!
//! It still tests exactly what it was written to test: the REAL nornir tree, its
//! REAL deep dependency graph, a genuinely missing sibling set, the real
//! `prepare_path_deps` → `cargo metadata` → SBOM path. Only the remote moved, and
//! it is now local, so the test is also no longer network-bound.
//!
//! Heavy (clones every sibling repo, runs `cargo metadata`) → `#[ignore]`. Run:
//!   cargo test --features server,net-scan --test security_isolated_live -- --ignored --nocapture
#![cfg(feature = "net-scan")]

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

/// Where the real sibling checkouts live: the parent of THIS repo. `nornir` is
/// at `<git>/nornir`, so its `../skade` path-deps resolve under `<git>`.
fn siblings_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("nornir has a parent directory")
        .to_path_buf()
}

/// The sibling repo dirs `repo`'s path-deps point at and which are NOT already
/// resolvable inside it — mirroring `clone_missing_path_dep_siblings`'s own two
/// skip rules, so the precondition and the code under test agree on what has to
/// be fetched.
///
/// Both rules matter. Without the resolvable-in-place skip this asked for
/// `crates` — MEASURED, the first run failed with
/// `these are not there: ["crates"]` — because nornir path-deps its OWN
/// `crates/nornir-jobs`, whose first path component is `crates` and which the
/// scanner never fetches because it is right there in the archived tree.
///
/// Parsed as TOML, not grepped: `Cargo.toml` carries commented-out path-deps
/// (`# tunnr-vm = { path = "../tunnr" }`) that a line grep would demand and the
/// scanner would never ask for.
fn required_siblings(repo: &Path) -> Vec<String> {
    let text = std::fs::read_to_string(repo.join("Cargo.toml")).expect("read manifest");
    let doc: toml::Value = toml::from_str(&text).expect("parse manifest");
    let mut out: Vec<String> = Vec::new();
    for table in ["dependencies", "dev-dependencies", "build-dependencies"] {
        let Some(deps) = doc.get(table).and_then(|d| d.as_table()) else {
            continue;
        };
        for spec in deps.values() {
            let Some(path) = spec.as_table().and_then(|t| t.get("path")).and_then(|p| p.as_str())
            else {
                continue;
            };
            // Already resolvable in place → the scanner fetches nothing.
            if repo.join(path).join("Cargo.toml").exists() {
                continue;
            }
            if let Some(name) = Path::new(path).components().find_map(|c| match c {
                std::path::Component::Normal(s) => Some(s.to_string_lossy().into_owned()),
                _ => None,
            }) {
                if !out.contains(&name) {
                    out.push(name);
                }
            }
        }
    }
    out.sort();
    out
}

/// Materialize a clean checkout of THIS repo's committed tree into `dest` via
/// `git archive` (no `.git`, no `target`, no creds).
fn archive_into(src_repo: &Path, dest: &Path) {
    std::fs::create_dir_all(dest).unwrap();
    let archive = Command::new("git")
        .args(["-C", src_repo.to_str().unwrap(), "archive", "--format=tar", "HEAD"])
        .output()
        .expect("git archive");
    assert!(
        archive.status.success(),
        "git archive: {}",
        String::from_utf8_lossy(&archive.stderr)
    );
    let mut tar = Command::new("tar")
        .args(["-x", "-C", dest.to_str().unwrap()])
        .stdin(std::process::Stdio::piped())
        .spawn()
        .expect("spawn tar");
    use std::io::Write;
    tar.stdin.take().unwrap().write_all(&archive.stdout).unwrap();
    assert!(tar.wait().unwrap().success(), "tar extract");
}

/// **Prove this process cannot reach the network or the operator's key.**
///
/// Called before a single clone runs. Every assertion here failed, in one form or
/// another, on the version of this test that shipped before 2026-09-10.
fn assert_isolated(real_home: &Path, scratch_home: &Path) {
    // 1. The clone base is local. No `git@`, no codeberg, nothing resolvable to a
    //    remote host.
    let base = nornir::security::dep_clone_base();
    assert!(
        base.starts_with("file://"),
        "the clone base must be a local file:// URL, got `{base}`"
    );
    for forbidden in ["git@", "codeberg.org", "github.com", "https://", "http://"] {
        assert!(
            !base.contains(forbidden),
            "the clone base must not be able to reach a remote (`{forbidden}` in `{base}`)"
        );
    }

    // 2. HOME is the scratch, so the ladder's two $HOME rungs are empty.
    let home = std::env::var("HOME").expect("HOME set");
    assert_eq!(Path::new(&home), scratch_home, "HOME is redirected to the scratch dir");
    assert_ne!(Path::new(&home), real_home, "…and is NOT the operator's real home");

    // 3. THE ONE THAT MATTERS: no key resolves at all. This is the assertion the
    //    old test would have failed — it resolved
    //    `~/.nornir/ssh/id_ed25519` and then authenticated with it.
    let key = nornir::gitio::nornir_ssh_key_path();
    assert!(
        key.is_none(),
        "a deploy key resolved inside an isolated fixture — it would be used to \
         authenticate to a real remote. Got {key:?}"
    );

    // 4. And the confine is the REASON it is none, not luck about which users
    //    happen to exist on this box. Without it, rungs 3 and 6 (/home/dwarves/.ssh,
    //    /home/nornir/.ssh) are absolute and unreachable by any env var.
    let res = nornir_git::deploykey::Resolution::resolve();
    assert!(
        res.confined(),
        "the ladder must be CONFINED, not merely unlucky: on a box with a real \
         service install the absolute rungs would answer. Rungs considered: {:?}",
        res.candidates.iter().map(|c| c.origin.as_str()).collect::<Vec<_>>()
    );
}

#[test]
#[ignore = "heavy: clones every sibling repo locally + runs cargo metadata"]
fn isolated_nornir_checkout_security_scan_resolves_via_clone() {
    let tmp = tempfile::tempdir().unwrap();
    let git = tmp.path().join("git");
    let nornir = git.join("nornir");
    let scratch_home = tmp.path().join("home");
    std::fs::create_dir_all(&scratch_home).unwrap();

    let siblings = siblings_root();
    let real_home = PathBuf::from(std::env::var("HOME").expect("HOME set"));

    // Isolated checkout: ONLY nornir (committed tree), exactly as the server
    // lays it out — NO siblings, the precondition that broke the scan.
    archive_into(Path::new(env!("CARGO_MANIFEST_DIR")), &nornir);
    assert!(nornir.join("Cargo.toml").exists(), "nornir materialized");
    assert!(!git.join("skade").exists(), "precondition: skade sibling absent");

    // LOUD SKIP, never a silent pass: the clones now come from the sibling
    // checkouts beside this repo. A missing one would make `cargo metadata` fail
    // for a reason that has nothing to do with what is under test.
    let needed = required_siblings(&nornir);
    assert!(!needed.is_empty(), "the archived manifest has sibling path-deps to resolve");
    let missing: Vec<&String> =
        needed.iter().filter(|n| !siblings.join(n).join(".git").is_dir()).collect();
    assert!(
        missing.is_empty(),
        "this test clones from the sibling checkouts at {} and these are not there: {missing:?}. \
         Clone them (SSH only — see the workspace law) and re-run. It refuses to pass without \
         them rather than prove nothing.",
        siblings.display()
    );

    // ── CUT EVERY ROUTE OUT, THEN PROVE IT ────────────────────────────────────
    // SAFETY: this is the only test in this file, so it runs single-threaded and
    // nothing else observes the process env while it is swapped. Restored below.
    let prev_base = std::env::var_os("NORNIR_DEP_CLONE_BASE");
    let prev_home = std::env::var_os("HOME");
    let prev_confine = std::env::var_os("DWARVES_SSH_CONFINE");
    let prev_dw_dir = std::env::var_os("DWARVES_SSH_DIR");
    let prev_no_dir = std::env::var_os("NORNIR_SSH_DIR");
    let prev_smudge = std::env::var_os("GIT_LFS_SKIP_SMUDGE");
    unsafe {
        std::env::set_var("NORNIR_DEP_CLONE_BASE", format!("file://{}", siblings.display()));
        std::env::set_var("HOME", &scratch_home);
        std::env::set_var("DWARVES_SSH_CONFINE", "1");
        // Check LFS pointers out RAW, as `ssh_sync` does.
        std::env::set_var("GIT_LFS_SKIP_SMUDGE", "1");
        // Confined, these two ARE the whole ladder — so they must be empty, not
        // merely pointed somewhere harmless.
        std::env::remove_var("DWARVES_SSH_DIR");
        std::env::remove_var("NORNIR_SSH_DIR");
    }
    assert_isolated(&real_home, &scratch_home);

    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        // BEFORE the fix, `components()` here died (cargo metadata +
        // generate-lockfile both fail: manifest won't load with unresolved
        // path-deps). Prove the bug.
        let before = nornir::security::components(&nornir);
        assert!(
            before.is_err(),
            "without prep the isolated scan still fails (the bug): {before:?}"
        );

        // THE FIX: clone the missing path-dep sibling(s) into the scan root.
        // `&|_| None` is the SERVER's own code path — kept, with the base
        // redirected at the local checkouts.
        let (cloned, _linked) = nornir::security::prepare_path_deps(&nornir, &git, &|_| None);
        assert!(cloned >= 1, "the missing siblings were cloned (got {cloned})");
        assert!(
            git.join("skade").join("skade").join("Cargo.toml").exists(),
            "`../skade/skade` now resolves to a real manifest"
        );

        // The real SBOM now resolves through cargo metadata.
        let (name, comps) =
            nornir::security::components(&nornir).expect("SBOM resolves after clone");
        assert_eq!(name, "nornir");
        assert!(comps.len() > 50, "deep dep tree resolved: {} components", comps.len());
        assert!(comps.iter().any(|c| c.name == "skade"), "skade present in the SBOM");
        eprintln!("LIVE OK: isolated nornir scan resolved {} components after cloning skade", comps.len());
    }));

    // Restore before re-raising, so a failure inside the body cannot leave the
    // process pointed at a scratch HOME for whatever runs next.
    // SAFETY: as above.
    unsafe {
        let restore = |k: &str, v: Option<std::ffi::OsString>| match v {
            Some(v) => std::env::set_var(k, v),
            None => std::env::remove_var(k),
        };
        restore("NORNIR_DEP_CLONE_BASE", prev_base);
        restore("HOME", prev_home);
        restore("DWARVES_SSH_CONFINE", prev_confine);
        restore("DWARVES_SSH_DIR", prev_dw_dir);
        restore("NORNIR_SSH_DIR", prev_no_dir);
        restore("GIT_LFS_SKIP_SMUDGE", prev_smudge);
    }
    if let Err(p) = result {
        std::panic::resume_unwind(p);
    }
}