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