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

nordisk/holger

server/lib/src/sandbox/tests.rs

//! The server's half of the scan sandbox, without a container engine: the
//! wire's checks, fail-closed refusals, the same verdicts in-process as before,
//! and the image CVE list. The container arms are `tests/scan_sandbox_live.rs`.

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};

use holger_sandbox::{Engine, Output, Refusal, Sandbox, SandboxConfig};

use super::wire::{Job, Report, REPORT_SCHEMA};
use super::*;
use crate::scanner::{ScanSubject, ScanVerdict, Scanner, ScannerKind};

fn template() -> JobTemplate {
    JobTemplate {
        format_name: "raw".into(),
        inspect: holger_inspect::InspectConfig::default(),
        kinds: vec![
            ScannerKind::Inspection,
            ScannerKind::Model,
            ScannerKind::Malware,
            ScannerKind::License,
            ScannerKind::Sbom,
        ],
        license: Default::default(),
        allow_pickle: Vec::new(),
        secrets: None,
    }
}

fn off() -> Arc<ScanWorker> {
    Arc::new(ScanWorker::new(SandboxRule::Off, None, template()).unwrap())
}

/// What a [`Scripted`] engine answers a `run` with.
type Script = Box<dyn FnMut(&[String]) -> Result<Output, Refusal> + Send>;

/// An engine that answers what a test scripts, and counts its runs.
struct Scripted {
    answer: Mutex<Script>,
    runs: Arc<AtomicUsize>,
}

impl Engine for Scripted {
    fn ready(&self, _: &SandboxConfig) -> Result<(), Refusal> {
        Ok(())
    }
    fn run(&self, _: &SandboxConfig, _: &str, args: &[String]) -> Result<Output, Refusal> {
        self.runs.fetch_add(1, Ordering::SeqCst);
        (self.answer.lock().unwrap())(args)
    }
}

struct Absent;
impl Engine for Absent {
    fn ready(&self, _: &SandboxConfig) -> Result<(), Refusal> {
        Err(Refusal::NoEngine("no podman on this host".into()))
    }
    fn run(&self, _: &SandboxConfig, _: &str, _: &[String]) -> Result<Output, Refusal> {
        unreachable!("an absent engine is never run")
    }
}

fn contained(engine: impl Engine + 'static) -> Arc<ScanWorker> {
    let sb = Sandbox::with_engine(SandboxConfig::default(), Box::new(engine)).unwrap();
    Arc::new(ScanWorker::with_sandbox(sb, template()).unwrap())
}

/// The scan job's input directory, read back from the `run` arguments (`/in/i`).
fn input_of(args: &[String]) -> std::path::PathBuf {
    let v = args
        .iter()
        .find(|a| a.ends_with(":/in/i:ro"))
        .expect("the job's input at /in/i");
    std::path::PathBuf::from(
        v.trim_start_matches("--volume=")
            .trim_end_matches(":/in/i:ro"),
    )
}

/// The scan document the server wrote.
fn job_of(args: &[String]) -> Job {
    serde_json::from_slice(&std::fs::read(input_of(args).join(SCAN_FILE)).unwrap()).unwrap()
}

fn artifact_of(args: &[String]) -> Vec<u8> {
    std::fs::read(input_of(args).join(ARTIFACT_FILE)).unwrap()
}

/// What a worker answers: `report` written to the job's `/out`, the answer on stdout.
fn answer(args: &[String], report: &[u8]) -> Result<Output, Refusal> {
    let out = std::path::PathBuf::from(holger_sandbox::out_of(args).expect("the job's /out"));
    std::fs::write(out.join(REPORT_FILE), report).unwrap();
    let a = holger_job_runner::wire::Answer::accepted(vec![REPORT_FILE.into()]);
    Ok(Output {
        report: serde_json::to_vec(&a).unwrap(),
        elapsed: Default::default(),
    })
}

/// A gzip stream of 64 MiB of zeros, as a source distribution.
fn gzip_bomb() -> Vec<u8> {
    use std::io::Write as _;
    let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
    let zeros = vec![0u8; 1 << 20];
    for _ in 0..64 {
        e.write_all(&zeros).unwrap();
    }
    e.finish().unwrap()
}

#[test]
fn sandbox_off_is_a_test_build_only_and_required_is_the_default() {
    assert_eq!(IntakeRules::default().sandbox, SandboxRule::Required);
    let r: IntakeRules = ron::from_str("(sandbox: off)").unwrap();
    assert_eq!(r.sandbox, SandboxRule::Off);
    assert!(ron::from_str::<IntakeRules>("(sandbox: maybe)").is_err());
    assert_eq!(
        check_in(SandboxRule::Off, false).unwrap_err().code().code,
        "holger-sandbox-6"
    );
    check_in(SandboxRule::Required, false).unwrap();
    check_in(SandboxRule::Off, true).unwrap();
}

#[test]
fn no_engine_refuses_every_scan_by_name_and_nothing_runs_in_process() {
    let w = contained(Absent);
    let s = ScanSubject::new("demo-1.0-py3-none-any.whl", "pip", b"PK\x03\x04");
    let f = w.inspect(&s);
    assert_eq!(f.verdict(), holger_inspect::Verdict::Fail);
    assert!(
        f.findings[0].detail.contains("[holger-sandbox-1]"),
        "{:?}",
        f.findings
    );
    assert!(
        f.findings[0].detail.contains("refuses to scan"),
        "{:?}",
        f.findings
    );
    for s2 in WorkerScanner::all(&w) {
        let o = s2.scan(&s).unwrap();
        assert_eq!(o.verdict, ScanVerdict::Block, "{} {o:?}", s2.id());
        assert_eq!(o.findings[0].id, "sandbox-no-engine");
    }
    // `Absent::run` is unreachable: the engine was asked once whether it is
    // there (the answer is remembered) and never asked to run.
}

#[test]
fn a_planted_zip_bomb_is_refused_in_the_worker_exactly_as_before() {
    let bomb = gzip_bomb();
    let before = holger_inspect::Inspector::default().inspect(
        &bomb,
        holger_inspect::Format::Sdist,
        "demo-1.0.tar.gz",
    );
    let w = Arc::new(
        ScanWorker::new(
            SandboxRule::Off,
            None,
            JobTemplate {
                format_name: "pip".into(),
                ..template()
            },
        )
        .unwrap(),
    );
    let after = w.inspect(&ScanSubject::new("demo-1.0.tar.gz", "pip", &bomb));
    assert_eq!(
        after.verdict(),
        holger_inspect::Verdict::Fail,
        "{:?}",
        after.findings
    );
    assert!(
        after.has(
            holger_inspect::FindingKind::ZipBomb,
            holger_inspect::Severity::Block
        ),
        "{:?}",
        after.findings
    );
    assert_eq!(
        after.findings, before.findings,
        "the worker reaches the server's own verdict"
    );
}

#[test]
fn ingest_and_scanned_share_one_worker_run() {
    let w = off();
    let s = ScanSubject::new("notes.txt", "raw", b"hello").with_repo("incoming");
    w.inspect(&s);
    for p in WorkerScanner::all(&w) {
        p.scan(&s).unwrap();
    }
    assert_eq!(w.runs(), 1);
}

#[test]
fn a_timeout_is_a_refusal_by_name() {
    let runs = Arc::new(AtomicUsize::new(0));
    let w = contained(Scripted {
        answer: Mutex::new(Box::new(|_| Err(Refusal::Timeout { secs: 2 }))),
        runs: runs.clone(),
    });
    let f = w.inspect(&ScanSubject::new("x.whl", "pip", b"PK"));
    assert_eq!(f.verdict(), holger_inspect::Verdict::Fail);
    assert!(
        f.findings[0]
            .detail
            .starts_with("sandbox-timeout: [holger-sandbox-2]"),
        "{:?}",
        f.findings
    );
}

#[test]
fn the_server_trusts_nothing_from_the_worker_beyond_the_checked_report() {
    // A hostile worker answers "pass" for other bytes, a kind nobody asked for,
    // a control character in a detail, and a hold without a warn.
    type Spoil = Box<dyn Fn(&mut serde_json::Value) + Send + Sync>;
    let cases: Vec<(&str, Spoil)> = vec![
        (
            "other bytes",
            Box::new(|v| v["sha256"] = "00".repeat(32).into()),
        ),
        (
            "an unasked kind",
            Box::new(|v| v["records"][0]["kind"] = "secrets".into()),
        ),
        (
            "a control character",
            Box::new(|v| v["records"][0]["detail"] = "ok\u{1b}[2J".into()),
        ),
        (
            "a hold on a pass",
            Box::new(|v| v["records"][0]["hold"] = true.into()),
        ),
        (
            "an unknown field",
            Box::new(|v| v["path_to_open"] = "/etc/shadow".into()),
        ),
    ];
    for (what, tamper) in cases {
        let tamper = Arc::new(tamper);
        let t = Arc::clone(&tamper);
        let runs = Arc::new(AtomicUsize::new(0));
        let w = contained(Scripted {
            answer: Mutex::new(Box::new(move |args: &[String]| {
                let job = job_of(args);
                let bytes = artifact_of(args);
                let dir = tempfile::tempdir().unwrap();
                let r = worker::run(&job, &bytes, dir.path());
                let mut v = serde_json::to_value(&r).unwrap();
                t(&mut v);
                answer(args, &serde_json::to_vec(&v).unwrap())
            })),
            runs,
        });
        let f = w.inspect(&ScanSubject::new("notes.txt", "raw", b"hello"));
        assert_eq!(
            f.verdict(),
            holger_inspect::Verdict::Fail,
            "{what}: {:?}",
            f.findings
        );
        assert!(
            f.findings[0].detail.contains("[holger-sandbox-5]"),
            "{what}: {:?}",
            f.findings
        );
    }
}

#[test]
fn an_honest_worker_report_is_used_as_is() {
    let runs = Arc::new(AtomicUsize::new(0));
    let w = contained(Scripted {
        answer: Mutex::new(Box::new(|args: &[String]| {
            let job = job_of(args);
            assert_eq!(job.schema, super::wire::JOB_SCHEMA);
            let dir = tempfile::tempdir().unwrap();
            let r = worker::run(&job, &artifact_of(args), dir.path());
            answer(args, &serde_json::to_vec(&r).unwrap())
        })),
        runs: runs.clone(),
    });
    let png = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR";
    let s = ScanSubject::new("demo-1.0-py3-none-any.whl", "pip", png);
    let insp = WorkerScanner::all(&w)
        .into_iter()
        .find(|s| s.kind() == ScannerKind::Inspection)
        .unwrap();
    let o = insp.scan(&s).unwrap();
    assert_eq!(o.verdict, ScanVerdict::Block);
    assert_eq!(o.findings[0].id, "mime-mismatch");
    assert_eq!(runs.load(Ordering::SeqCst), 1);
}

/// **The worker names the package a raw tarball is** (lane HOL-FIELD-2 P16): its own
/// manifest (`package/package.json`), so the vulnerability slot matches it as npm, never as raw.
#[test]
fn the_worker_names_the_package_inside_a_raw_tarball() {
    let mut b = tar::Builder::new(Vec::new());
    let json = br#"{"name":"lodash","version":"4.17.15"}"#;
    let mut h = tar::Header::new_gnu();
    h.set_path("package/package.json").unwrap();
    h.set_size(json.len() as u64);
    h.set_mode(0o644);
    h.set_cksum();
    b.append(&h, &json[..]).unwrap();
    let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
    std::io::Write::write_all(&mut gz, &b.into_inner().unwrap()).unwrap();
    let bytes = gz.finish().unwrap();
    let job = Job {
        schema: super::wire::JOB_SCHEMA.into(),
        sha256: crate::attest::ArtifactDigest::of_sha256(&bytes)
            .hex()
            .to_string(),
        file_name: "payload.dat".into(),
        ecosystem: "raw".into(),
        name: None,
        version: None,
        repo: None,
        source: None,
        format_name: "raw".into(),
        inspect: Default::default(),
        kinds: vec![],
        license: Default::default(),
        allow_pickle: vec![],
        provenance: None,
        secrets: None,
    };
    let dir = tempfile::tempdir().unwrap();
    let r = worker::run(&job, &bytes, dir.path());
    assert!(
        r.packages
            .iter()
            .any(|p| p.ecosystem == "npm" && p.name == "lodash" && p.version == "4.17.15"),
        "{:?}",
        r.packages
    );
    let back = Report::parse(&serde_json::to_vec(&r).unwrap(), &job).unwrap();
    assert_eq!(back.packages, r.packages, "the wire takes it");
}

#[test]
fn a_report_is_capped_at_parse_by_the_wire() {
    let job = Job {
        schema: super::wire::JOB_SCHEMA.into(),
        sha256: "ab".repeat(32),
        file_name: "x".into(),
        ecosystem: "raw".into(),
        name: None,
        version: None,
        repo: None,
        source: None,
        format_name: "raw".into(),
        inspect: Default::default(),
        kinds: vec![],
        license: Default::default(),
        allow_pickle: vec![],
        provenance: None,
        secrets: None,
    };
    assert!(Report::parse(b"not json", &job)
        .unwrap_err()
        .contains(REPORT_SCHEMA));
}

/// **The index row rides in the report, checked like the rest** (lane HOL-PERF-2): an
/// honest row reads back as sent; a row out of its bounds, or of the old report schema,
/// refuses the whole report (`holger-sandbox-5` at the caller), never a silent drop.
#[test]
fn the_index_row_rides_in_the_report_and_is_checked_at_parse() {
    let bytes = b"hello";
    let job = Job {
        schema: super::wire::JOB_SCHEMA.into(),
        sha256: crate::attest::ArtifactDigest::of_sha256(bytes)
            .hex()
            .to_string(),
        file_name: "notes.txt".into(),
        ecosystem: "raw".into(),
        name: None,
        version: None,
        repo: None,
        source: None,
        format_name: "raw".into(),
        inspect: Default::default(),
        kinds: vec![],
        license: Default::default(),
        allow_pickle: vec![],
        provenance: None,
        secrets: None,
    };
    assert_eq!(
        REPORT_SCHEMA, "holger.scan-worker-report/v2",
        "a report with a row is the second report schema"
    );
    let dir = tempfile::tempdir().unwrap();
    let mut r = worker::run(&job, bytes, dir.path());
    let honest = holger_index_rows::IndexRow::Deb {
        control: "Package: hello\nVersion: 1.0\n".into(),
    };
    r.index_row = Some(honest.clone());
    let back = Report::parse(&serde_json::to_vec(&r).unwrap(), &job).unwrap();
    assert_eq!(back.index_row, Some(honest));

    r.index_row = Some(holger_index_rows::IndexRow::Deb {
        control: "Package: a\u{1b}[2J\n".into(),
    });
    let e = Report::parse(&serde_json::to_vec(&r).unwrap(), &job).unwrap_err();
    assert!(
        e.contains("index row") && e.contains("control"),
        "the refusal names the row and its field: {e}"
    );

    r.index_row = None;
    let mut v = serde_json::to_value(&r).unwrap();
    v["schema"] = "holger.scan-worker-report/v1".into();
    assert!(
        Report::parse(&serde_json::to_vec(&v).unwrap(), &job).is_err(),
        "a v1 report is not this wire"
    );
}

/// **The worker answers the index row the server used to parse itself** (lane HOL-PERF-2):
/// the same `.deb` control stanza `znippy-deb` read in-process, through the same wire, in a
/// test build (`sandbox: off`) exactly as in the container.
#[test]
fn the_worker_answers_the_index_row_the_server_used_to_parse() {
    let deb = holger_index_rows::deb::tests_support::sample_deb();
    let r = off()
        .report(&ScanSubject::new("hello_2.10-2_amd64.deb", "deb", &deb))
        .unwrap();
    let want = holger_index_rows::deb::control_from_deb(&deb).unwrap();
    assert_eq!(
        r.index_row,
        Some(holger_index_rows::IndexRow::Deb { control: want })
    );
    let none = off()
        .report(&ScanSubject::new("notes.txt", "raw", b"hello"))
        .unwrap();
    assert_eq!(none.index_row, None, "a file with no index has no row");
}

/// **A worker older than this server is refused by name** (lane HOL-PERF-2): an image built
/// before the report schema moved to v2 answers `holger.scan-worker-report/v1`; the refusal
/// says the worker is older than the server and names the command that rebuilds it, never
/// a bare schema mismatch or an exit code.
#[test]
fn a_worker_older_than_this_server_is_refused_by_name_with_the_rebuild_command() {
    // The scan program of a worker built before v2: the same scan, its report spelled v1.
    let old = |job: &holger_job_runner::wire::WireJob, dirs: &holger_job_runner::wire::Dirs| {
        let answer = crate::job_runner::programs::run(job, dirs);
        let p = dirs.out.join(REPORT_FILE);
        let mut v: serde_json::Value = serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap();
        v.as_object_mut().unwrap().remove("index_row");
        v["schema"] = "holger.scan-worker-report/v1".into();
        std::fs::write(&p, serde_json::to_vec(&v).unwrap()).unwrap();
        answer
    };
    let runner = Arc::new(holger_job_runner::InProcessRunner::new(Arc::new(old)));
    let w = ScanWorker::with_jobs(
        Arc::new(crate::job_runner::Jobs::new(runner)),
        None,
        template(),
    )
    .unwrap();
    let f = w.inspect(&ScanSubject::new("notes.txt", "raw", b"hello"));
    assert_eq!(f.verdict(), holger_inspect::Verdict::Fail);
    let d = &f.findings[0].detail;
    assert!(
        d.contains("[holger-sandbox-5]") && d.contains("older than this server"),
        "{d}"
    );
    assert!(
        d.contains("cargo xtask scan-worker image") && d.contains("cargo xtask scan-worker stage"),
        "the refusal names the rebuild: {d}"
    );
}

// ── the image CVE scanner ───────────────────────────────────────────────────

fn tar_of(files: &[(&str, &[u8])]) -> Vec<u8> {
    let mut b = tar::Builder::new(Vec::new());
    for (p, d) in files {
        // `x:<path>` is an executable file.
        let (p, mode) = match p.strip_prefix("x:") {
            Some(p) => (p, 0o755),
            None => (*p, 0o644),
        };
        let mut h = tar::Header::new_ustar();
        h.set_size(d.len() as u64);
        h.set_mode(mode);
        h.set_entry_type(tar::EntryType::Regular);
        b.append_data(&mut h, p, *d).unwrap();
    }
    b.into_inner().unwrap()
}

fn sha(b: &[u8]) -> String {
    crate::attest::ArtifactDigest::of_sha256(b)
        .hex()
        .to_string()
}

/// An image layout with one gzip layer holding `files`.
fn image(files: &[(&str, &[u8])]) -> Vec<u8> {
    use std::io::Write as _;
    let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
    e.write_all(&tar_of(files)).unwrap();
    let layer = e.finish().unwrap();
    let manifest = serde_json::to_vec(&serde_json::json!({"schemaVersion": 2, "layers": [
        {"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": format!("sha256:{}", sha(&layer))}]}))
    .unwrap();
    let index = serde_json::to_vec(
        &serde_json::json!({"manifests": [{"digest": format!("sha256:{}", sha(&manifest))}]}),
    )
    .unwrap();
    tar_of(&[
        ("index.json", &index),
        (&format!("blobs/sha256/{}", sha(&manifest)), &manifest),
        (&format!("blobs/sha256/{}", sha(&layer)), &layer),
    ])
}

fn advisories() -> Arc<dyn Scanner> {
    use crate::security::{Advisory, AdvisoryDb, Severity};
    Arc::new(crate::scanner::osv::OsvVulnScanner::new(AdvisoryDb::new(
        "test-db",
        vec![
            Advisory::basic(
                "CGA-test-1",
                "wolfi",
                "openssl",
                vec!["3.0.13-r2".into()],
                Severity::Critical,
                "a test CVE",
            ),
            Advisory::basic(
                "CGA-test-2",
                "wolfi",
                "zlib",
                vec!["1.3-r0".into()],
                Severity::Low,
                "another",
            ),
        ],
    )))
}

#[test]
fn an_image_gets_a_cve_list_with_severities_from_its_own_package_database() {
    let img = image(&[
        ("etc/os-release", b"ID=wolfi\n"),
        (
            "usr/lib/apk/db/installed",
            b"P:openssl\nV:3.0.13-r2\no:openssl\n\nP:zlib\nV:1.3.1-r0\n",
        ),
    ]);
    let w = off();
    let v = VulnFromWorker::new(advisories(), Arc::clone(&w));
    let o = v
        .scan(&ScanSubject::new(
            "chainguard/demo@sha256_x.oci.tar",
            "docker",
            &img,
        ))
        .unwrap();
    assert_eq!(o.findings.len(), 1, "{o:?}");
    assert_eq!(o.findings[0].id, "CGA-test-1");
    assert_eq!(
        o.findings[0].severity,
        crate::scanner::FindingSeverity::Critical
    );
    assert!(
        o.findings[0].summary.contains("wolfi openssl@3.0.13-r2"),
        "{o:?}"
    );
    assert!(
        o.detail.contains("2 package(s) from layers (os wolfi)"),
        "{}",
        o.detail
    );
    assert!(
        o.verdict != ScanVerdict::Error,
        "an image's vulnerability scan is a verdict now: {o:?}"
    );
}

/// A little-endian ELF64 whose `.dep-v0` section is `data` (cargo-auditable's).
fn elf_with_dep_v0(data: &[u8]) -> Vec<u8> {
    let strtab = b"\0.dep-v0\0.shstrtab\0";
    let (data_off, str_off) = (64usize, 64 + data.len());
    let sh_off = (str_off + strtab.len() + 7) & !7;
    let mut b = vec![0u8; sh_off + 3 * 64];
    b[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
    b[0x28..0x30].copy_from_slice(&(sh_off as u64).to_le_bytes());
    b[0x3a..0x3c].copy_from_slice(&64u16.to_le_bytes());
    b[0x3c..0x3e].copy_from_slice(&3u16.to_le_bytes());
    b[0x3e..0x40].copy_from_slice(&2u16.to_le_bytes());
    b[data_off..str_off].copy_from_slice(data);
    b[str_off..str_off + strtab.len()].copy_from_slice(strtab);
    for (i, name, off, size) in [
        (1usize, 1u32, data_off, data.len()),
        (2, 9, str_off, strtab.len()),
    ] {
        let h = sh_off + i * 64;
        b[h..h + 4].copy_from_slice(&name.to_le_bytes());
        b[h + 0x18..h + 0x20].copy_from_slice(&(off as u64).to_le_bytes());
        b[h + 0x20..h + 0x28].copy_from_slice(&(size as u64).to_le_bytes());
    }
    b
}

/// An ELF carrying Go 1.18+ build info for `go_version` and one dependency.
fn go_elf(go_version: &str, dep: &str, dep_version: &str) -> Vec<u8> {
    let mut b = b"\x7fELF\x02\x01\x01".to_vec();
    b.resize(64, 0);
    b.extend_from_slice(holger_inspect::binmeta::GO_MAGIC);
    b.extend_from_slice(&[8, 2]);
    b.extend_from_slice(&[0u8; 16]);
    let info = format!(
        "{}path\tapp\ndep\t{dep}\tv{dep_version}\th1:x=\n{}",
        "0".repeat(16),
        "1".repeat(16)
    );
    for s in [go_version, info.as_str()] {
        assert!(s.len() < 128);
        b.push(s.len() as u8);
        b.extend_from_slice(s.as_bytes());
    }
    b.extend_from_slice(&[0u8; 32]);
    b
}

/// rpm, Go and Rust packages of an image reach the advisory match: the rpm
/// database (a real UBI 9 micro `rpmdb.sqlite`), a Go binary's build info and a
/// `cargo auditable` binary's `.dep-v0`, each matched in its own ecosystem.
#[test]
fn an_images_rpm_go_and_cargo_packages_reach_the_vulnerability_match() {
    use crate::security::{Advisory, AdvisoryDb, Severity};
    use std::io::{Read as _, Write as _};
    let gz = include_bytes!("../../../inspect/tests/fixtures/rpm/ubi9-micro-rpmdb.sqlite.gz");
    let mut rpmdb = Vec::new();
    flate2::read::GzDecoder::new(&gz[..])
        .read_to_end(&mut rpmdb)
        .unwrap();
    let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
    z.write_all(br#"{"packages":[{"name":"time","version":"0.3.20","source":"crates.io"}]}"#)
        .unwrap();
    let rust = elf_with_dep_v0(&z.finish().unwrap());
    let go = go_elf("go1.22.1", "golang.org/x/net", "0.17.0");
    let img = image(&[
        ("etc/os-release", b"ID=\"rhel\"\n"),
        ("var/lib/rpm/rpmdb.sqlite", &rpmdb),
        ("x:usr/bin/goapp", &go),
        ("x:usr/bin/rustapp", &rust),
    ]);
    let db: Arc<dyn Scanner> = Arc::new(crate::scanner::osv::OsvVulnScanner::new(AdvisoryDb::new(
        "test-db",
        vec![
            Advisory::basic(
                "RHSA-test-1",
                "rpm",
                "glibc",
                vec!["2.34-283.el9_8".into()],
                Severity::High,
                "glibc",
            ),
            Advisory::basic(
                "GO-test-1",
                "go",
                "stdlib",
                vec!["1.22.1".into()],
                Severity::Medium,
                "net/http",
            ),
            Advisory::basic(
                "GO-test-2",
                "go",
                "golang.org/x/net",
                vec!["0.17.0".into()],
                Severity::High,
                "x/net",
            ),
            Advisory::basic(
                "RUSTSEC-test-1",
                "cargo",
                "time",
                vec!["0.3.20".into()],
                Severity::Low,
                "time",
            ),
        ],
    )));
    let o = VulnFromWorker::new(db, off())
        .scan(&ScanSubject::new("ubi/demo.oci.tar", "docker", &img))
        .unwrap();
    let ids: Vec<&str> = o.findings.iter().map(|f| f.id.as_str()).collect();
    for want in ["RHSA-test-1", "GO-test-1", "GO-test-2", "RUSTSEC-test-1"] {
        assert!(ids.contains(&want), "{want} in {ids:?}: {}", o.detail);
    }
    let doc = o.document.as_ref().unwrap();
    let pkgs = doc["packages"].as_array().unwrap();
    for src in ["rpmdb-sqlite", "go-buildinfo", "cargo-auditable"] {
        assert!(pkgs.iter().any(|p| p["source"] == src), "{src}: {doc}");
    }
    assert!(
        o.detail.contains("23 package(s)"),
        "20 rpms, stdlib, x/net and time: {}",
        o.detail
    );
}

/// HOL-OCI-EVIDENCE item 1: **an image's CycloneDX SBOM is made in the scan worker**, from
/// its layers (the OS package database and the language manifests), never in the server.
/// The scripted engine runs the worker's own code, as the container does; the server's
/// SBOM slot holds only a proxy that reads the worker's record. With no engine, the slot
/// is a refusal and carries no document: the server never builds one itself.
#[test]
fn an_images_cyclonedx_sbom_is_made_in_the_scan_worker_from_its_layers() {
    let img = image(&[
        ("etc/os-release", b"ID=wolfi\n"),
        (
            "usr/lib/apk/db/installed",
            b"P:openssl\nV:3.0.13-r2\no:openssl\n\nP:libcrypto3\nV:3.0.13-r2\no:openssl\n",
        ),
        (
            "usr/lib/python3.12/site-packages/idna-3.7.dist-info/METADATA",
            b"Metadata-Version: 2.1\nName: idna\nVersion: 3.7\n\n",
        ),
        (
            "app/node_modules/@scope/x/package.json",
            br#"{"name":"@scope/x","version":"1.2.3"}"#,
        ),
        (
            "x:usr/bin/goapp",
            &go_elf("go1.22.1", "golang.org/x/net", "0.17.0"),
        ),
    ]);
    let runs = Arc::new(AtomicUsize::new(0));
    let w = contained(Scripted {
        answer: Mutex::new(Box::new(|args: &[String]| {
            let dir = tempfile::tempdir().unwrap();
            let r = worker::run(&job_of(args), &artifact_of(args), dir.path());
            answer(args, &serde_json::to_vec(&r).unwrap())
        })),
        runs: runs.clone(),
    });
    let subject = ScanSubject::new("demo/app/sha256-x.oci.tar", "docker", &img);
    let all = WorkerScanner::all(&w);
    let sbom = all
        .iter()
        .find(|s| s.id() == "holger-image-sbom")
        .expect("the image SBOM scanner fills the SBOM slot, in the worker");
    assert_eq!(sbom.kind(), ScannerKind::Sbom);
    let o = sbom.scan(&subject).unwrap();
    assert_eq!(
        runs.load(Ordering::SeqCst),
        1,
        "made by the worker's one run"
    );
    assert_eq!(o.verdict, ScanVerdict::Pass, "{o:?}");
    let doc = o.document.clone().expect("a CycloneDX document");
    assert_eq!(doc["bomFormat"], "CycloneDX", "{doc}");
    assert_eq!(doc["specVersion"], "1.5", "{doc}");
    assert!(
        doc["serialNumber"]
            .as_str()
            .is_some_and(|s| s.starts_with("urn:uuid:")),
        "{doc}"
    );
    let top = doc["metadata"]["component"].clone();
    assert_eq!(top["type"], "container", "{doc}");
    assert!(
        top["bom-ref"]
            .as_str()
            .is_some_and(|r| r.starts_with("sha256:")),
        "the image digest: {doc}"
    );
    assert_eq!(top["hashes"][0]["alg"], "SHA-256", "{doc}");
    let purls: Vec<String> = doc["components"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|c| c["purl"].as_str().map(str::to_string))
        .collect();
    for want in [
        "pkg:apk/wolfi/openssl@3.0.13-r2?distro=wolfi",
        "pkg:apk/wolfi/libcrypto3@3.0.13-r2?distro=wolfi",
        "pkg:pypi/idna@3.7",
        "pkg:npm/%40scope/x@1.2.3",
        "pkg:golang/golang.org/x/net@v0.17.0",
        "pkg:golang/stdlib@v1.22.1",
    ] {
        assert!(purls.iter().any(|p| p == want), "{want} not in {purls:?}");
    }
    let os = doc["components"]
        .as_array()
        .unwrap()
        .iter()
        .find(|c| c["type"] == "operating-system")
        .cloned()
        .unwrap_or_default();
    assert_eq!(os["name"], "wolfi", "{doc}");
    // Each package names where in the image it was read.
    let openssl = doc["components"]
        .as_array()
        .unwrap()
        .iter()
        .find(|c| c["name"] == "openssl")
        .unwrap()
        .clone();
    assert!(
        openssl["properties"]
            .as_array()
            .unwrap()
            .iter()
            .any(|p| p["name"] == "holger:source" && p["value"] == "apk-db"),
        "{openssl}"
    );
    // Byte-stable: the same image folds to the same document.
    let again = WorkerScanner::all(&off())
        .into_iter()
        .find(|s| s.id() == "holger-image-sbom")
        .unwrap()
        .scan(&subject)
        .unwrap();
    assert_eq!(
        again.document,
        Some(doc.clone()),
        "deterministic, in-process (a test build) or in the container alike"
    );
    // A wheel is not an image: no document from this scanner.
    let wheel = sbom
        .scan(&ScanSubject::new(
            "demo-1.0-py3-none-any.whl",
            "pip",
            b"PK\x03\x04",
        ))
        .unwrap();
    assert!(wheel.document.is_none(), "{wheel:?}");
    // No engine: a refusal by name, and no document made here instead.
    let none = WorkerScanner::all(&contained(Absent))
        .into_iter()
        .find(|s| s.id() == "holger-image-sbom")
        .unwrap()
        .scan(&subject)
        .unwrap();
    assert_eq!(none.verdict, ScanVerdict::Block);
    assert!(
        none.document.is_none(),
        "the server never builds the SBOM itself: {none:?}"
    );
}

#[test]
fn a_clean_image_scans_clean_rather_than_error() {
    let img = image(&[
        ("etc/os-release", b"ID=wolfi\n"),
        ("lib/apk/db/installed", b"P:tzdata\nV:2026e-r0\n"),
    ]);
    let o = VulnFromWorker::new(advisories(), off())
        .scan(&ScanSubject::new("static.oci.tar", "docker", &img))
        .unwrap();
    assert_eq!(o.verdict, ScanVerdict::Pass, "{o:?}");
    assert!(o.findings.is_empty());
}

#[test]
fn the_vulnerability_slot_never_reads_a_wheel_itself() {
    // No coordinates anywhere: an error, not a METADATA read in this process.
    let o = VulnFromWorker::new(advisories(), off())
        .scan(&ScanSubject::new("blob", "raw", b"\0\0"))
        .unwrap();
    assert_eq!(o.verdict, ScanVerdict::Error, "{o:?}");
}

// ── G3: the pinned APK keys reach verify_artifact inside the worker ──

const PROV_FX: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../provenance/tests/fixtures"
);

fn fx(p: &str) -> Vec<u8> {
    let b = std::fs::read(format!("{PROV_FX}/{p}")).unwrap_or_else(|e| panic!("{p}: {e}"));
    assert!(
        !b.starts_with(b"version https://git-lfs"),
        "{p} is an LFS pointer: run `git lfs pull`"
    );
    b
}

#[test]
fn the_worker_verifies_a_wolfi_apk_with_the_apk_keys_it_is_handed() {
    let w = off();
    let apk = fx("wolfi/stage3/libffi-3.4.2-r0.apk");
    let m = vec![(
        "APKINDEX.tar.gz".to_string(),
        fx("wolfi/stage3/APKINDEX.tar.gz"),
    )];
    let policy = holger_provenance::effective(
        &holger_provenance::ProvenanceFloor {
            require_provenance: true,
            ..Default::default()
        },
        &holger_provenance::ProvenanceGate::Off,
    );
    let key = [(
        "wolfi-signing.rsa.pub".to_string(),
        fx("wolfi/wolfi-signing.rsa.pub"),
    )];
    let root = fx("sigstore/trusted_root.json");
    let r = w
        .verify_provenance(&apk, &m, &policy, Some(&root), &key)
        .unwrap();
    assert_eq!(r.verdict, holger_provenance::Verdict::Pass, "{}", r.detail);
    // The keys alone (no Sigstore root) are enough for an APK.
    let r = w.verify_provenance(&apk, &m, &policy, None, &key).unwrap();
    assert_eq!(r.verdict, holger_provenance::Verdict::Pass, "{}", r.detail);
    // No key handed in: refused by name inside the worker.
    let r = w
        .verify_provenance(&apk, &m, &policy, Some(&root), &[])
        .unwrap();
    assert_eq!(
        r.code.as_deref(),
        Some("holger-provenance-4"),
        "{}",
        r.detail
    );
}

// ── T9: the pinned OpenPGP keys reach verify_artifact inside the worker ──

#[test]
fn the_worker_verifies_a_maven_central_asc_with_the_pgp_keys_it_is_handed() {
    let w = off();
    let pom = fx("maven/guava-33.3.1-jre.pom");
    let m = vec![(
        "guava-33.3.1-jre.pom.asc".to_string(),
        fx("maven/guava-33.3.1-jre.pom.asc"),
    )];
    let policy = holger_provenance::effective(
        &holger_provenance::ProvenanceFloor {
            require_provenance: true,
            ..Default::default()
        },
        &holger_provenance::ProvenanceGate::Off,
    );
    let povirk = "povirk-BDB5FA4FE719D787FB3D3197F6D4A1D411E9D1AE.asc";
    let ceki = "ceki-60200AC4AE761F1614D6C46766D68DAA073BE985.asc";
    let key = |n: &str| (n.to_string(), fx(&format!("maven/{n}")));
    let root = fx("sigstore/trusted_root.json");
    if !holger_provenance::pgp::BUILT {
        // T9b: a worker built without `maven-pgp` refuses the keys by name, never a pass.
        let e = w
            .verify_provenance_with_keys(&pom, &m, &policy, Some(&root), &[], &[key(povirk)])
            .map(|r| r.detail)
            .unwrap_or_else(|e| format!("{e:#}"));
        assert!(e.contains("[provenance-pgp-not-built]"), "{e}");
        let r = w
            .verify_provenance(&pom, &m, &policy, Some(&root), &[])
            .unwrap();
        assert_eq!(
            r.code.as_deref(),
            Some("holger-provenance-4"),
            "{}",
            r.detail
        );
        return;
    }
    let r = w
        .verify_provenance_with_keys(
            &pom,
            &m,
            &policy,
            Some(&root),
            &[],
            &[key(ceki), key(povirk)],
        )
        .unwrap();
    assert_eq!(r.verdict, holger_provenance::Verdict::Pass, "{}", r.detail);
    // The pgp keys alone (no Sigstore root) are enough for an .asc.
    let r = w
        .verify_provenance_with_keys(&pom, &m, &policy, None, &[], &[key(povirk)])
        .unwrap();
    assert_eq!(r.verdict, holger_provenance::Verdict::Pass, "{}", r.detail);
    // The signer's key not handed in: unverified inside the worker, never a pass.
    let r = w
        .verify_provenance_with_keys(&pom, &m, &policy, Some(&root), &[], &[key(ceki)])
        .unwrap();
    assert_eq!(
        r.code.as_deref(),
        Some("holger-provenance-4"),
        "{}",
        r.detail
    );
    let r = w
        .verify_provenance(&pom, &m, &policy, Some(&root), &[])
        .unwrap();
    assert_eq!(
        r.code.as_deref(),
        Some("holger-provenance-4"),
        "{}",
        r.detail
    );
}

// ── every scan is a job (lane HOL-SANDBOX-TRAIT) ────────────────────────────

struct SpyRunner {
    inner: Arc<dyn crate::job_runner::heimdall_core::JobRunner>,
    argv: Mutex<Vec<Vec<String>>>,
}

impl crate::job_runner::heimdall_core::JobRunner for SpyRunner {
    fn run(
        &self,
        job: crate::job_runner::heimdall_core::Job,
    ) -> Result<
        crate::job_runner::heimdall_core::SignedResult,
        crate::job_runner::heimdall_core::RunError,
    > {
        if let crate::job_runner::heimdall_core::JobKind::Native { argv, .. } = &job.kind {
            self.argv.lock().unwrap().push(argv.clone());
        }
        self.inner.run(job)
    }
    fn verifying_key(&self) -> crate::job_runner::heimdall_core::VerifyingKey {
        self.inner.verifying_key()
    }
    fn features(&self) -> crate::job_runner::heimdall_core::Features {
        self.inner.features()
    }
}

#[test]
fn every_scan_is_a_job_of_the_servers_runner() {
    let inner = Arc::new(holger_job_runner::InProcessRunner::new(Arc::new(
        crate::job_runner::programs::run,
    )));
    let spy = Arc::new(SpyRunner {
        inner,
        argv: Mutex::new(Vec::new()),
    });
    let w = ScanWorker::with_jobs(
        Arc::new(crate::job_runner::Jobs::new(spy.clone())),
        None,
        template(),
    )
    .unwrap();
    let bytes = b"hello from a job\n";
    let s = ScanSubject::new("hello.txt", "raw", bytes);
    let r = w.report(&s).unwrap();
    assert_eq!(
        r.sha256,
        s.sha256_hex(),
        "the worker read the bytes the server wrote"
    );
    assert_eq!(
        *spy.argv.lock().unwrap(),
        vec![vec!["scan".to_string()]],
        "one scan job"
    );
    assert_eq!(
        r.inspect.verdict(),
        off().report(&s).unwrap().inspect.verdict()
    );
    assert_eq!(w.runs(), 1);
    // The INGEST inspection of the same bytes reuses the report: no second job.
    let _ = w.inspect(&s);
    assert_eq!(spy.argv.lock().unwrap().len(), 1);
}

/// NCSC P1 (lane HOL-NCSC): a real build talks to ClamAV only over the sandboxed clamd's unix
/// socket. TCP, `clamscan`, no socket (the host's defaults) and a host clamd's socket are
/// refused by name; a test build may use any of them.
#[test]
fn a_real_build_talks_only_to_the_sandboxed_clamd_socket() {
    use holger_inspect::malware::ClamAvConfig;
    let sock = |s: &str| ClamAvConfig {
        socket: Some(s.into()),
        required: true,
        ..Default::default()
    };
    check_clamav(None, false).unwrap();
    check_clamav(Some(&sock("/data/holger/sandbox/clamd/clamd.sock")), false).unwrap();
    for (bad, why) in [
        (sock("127.0.0.1:3310"), "TCP"),
        (sock("clamd.local:3310"), "TCP"),
        (sock("run/clamd.sock"), "absolute"),
        (sock("/run/clamav/clamd.ctl"), "host"),
        (
            ClamAvConfig {
                required: true,
                ..Default::default()
            },
            "no socket",
        ),
        (
            ClamAvConfig {
                socket: Some("/s/clamd.sock".into()),
                clamscan: Some("/usr/bin/clamscan".into()),
                ..Default::default()
            },
            "clamscan",
        ),
    ] {
        let e = check_clamav(Some(&bad), false).unwrap_err();
        assert_eq!(e.code().code, "holger-sandbox-15", "{bad:?}: {e}");
        assert!(e.to_string().contains(why), "{why}: {e}");
        check_clamav(Some(&bad), true).unwrap();
    }
}

/// A real build's worker refuses such a block at the start, before a scan.
#[test]
fn a_tcp_clamd_refuses_the_worker_in_a_real_build() {
    use holger_inspect::malware::ClamAvConfig;
    let mut t = template();
    t.inspect.clamav = Some(ClamAvConfig {
        socket: Some("127.0.0.1:3310".into()),
        required: true,
        ..Default::default()
    });
    let jobs = Arc::new(crate::job_runner::Jobs::new(Arc::new(
        holger_job_runner::InProcessRunner::new(Arc::new(crate::job_runner::programs::run)),
    )));
    let e = ScanWorker::with_jobs_in(jobs, None, t, false).unwrap_err();
    assert_eq!(e.code().code, "holger-sandbox-15", "{e}");
}

/// clamd flags what it could not scan whole (a file past `MaxFileSize`, a nest past
/// `MaxRecursion`) as FOUND, so the intake blocks it, never passes it unscanned.
#[test]
fn the_sandboxed_clamd_alerts_what_exceeds_its_limits() {
    assert!(
        super::media::CLAMD_CONF
            .lines()
            .any(|l| l.trim() == "AlertExceedsMax yes"),
        "{}",
        super::media::CLAMD_CONF
    );
}

/// **A worker that is not this server's build is refused by name** (lane HOL-FIELD-TEST P1). In
/// the field, oden's `localhost/holger-scan-worker:0.1.0` was built before `holger-image-sbom`
/// existed: it answered the same report schema without that record, and every artifact a
/// required sbom scan saw was BLOCKED as "the scan worker sent no sbom record from
/// holger-image-sbom". The report names the worker's build (its version and its scanners), and a
/// report that names none, another version, or fewer scanners than this server runs is refused,
/// saying which and how to rebuild.
#[test]
fn a_worker_that_is_not_this_servers_build_is_refused_by_name() {
    let bytes = b"hello";
    let job = Job {
        schema: super::wire::JOB_SCHEMA.into(),
        sha256: crate::attest::ArtifactDigest::of_sha256(bytes)
            .hex()
            .to_string(),
        file_name: "notes.txt".into(),
        ecosystem: "raw".into(),
        name: None,
        version: None,
        repo: None,
        source: None,
        format_name: "raw".into(),
        inspect: Default::default(),
        kinds: vec![ScannerKind::Sbom],
        license: Default::default(),
        allow_pickle: vec![],
        provenance: None,
        secrets: None,
    };
    let dir = tempfile::tempdir().unwrap();
    let r = worker::run(&job, bytes, dir.path());
    let mut v = serde_json::to_value(&r).unwrap();

    // The field's stale worker: the same schema, and no word of its build.
    v.as_object_mut().unwrap().remove("worker");
    let e = Report::parse(&serde_json::to_vec(&v).unwrap(), &job)
        .expect_err("a worker that names no build is refused");
    assert!(
        e.contains("older than this server") && e.contains("cargo xtask scan-worker image"),
        "the refusal says what is wrong and how to rebuild: {e}"
    );

    // A worker of this version that lacks a scanner this server runs: refused, naming it.
    v["worker"] = serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "scanners": ["holger-wheel-sbom"] });
    let e = Report::parse(&serde_json::to_vec(&v).unwrap(), &job)
        .expect_err("a worker without the image SBOM generator is refused");
    assert!(e.contains("holger-image-sbom"), "{e}");

    // Another version: refused, naming both.
    let mut other = serde_json::to_value(&r).unwrap();
    other["worker"]["version"] = "0.1.0".into();
    let e = Report::parse(&serde_json::to_vec(&other).unwrap(), &job)
        .expect_err("a worker of another version is refused");
    assert!(
        e.contains("0.1.0") && e.contains(env!("CARGO_PKG_VERSION")),
        "{e}"
    );

    // This server's own worker passes.
    Report::parse(&serde_json::to_vec(&r).unwrap(), &job).expect("this build's worker");
}

// ── the probe door looks into Heimdall's runner (HOL-HEIMDALL-06) ─────────────

/// A runner that says it is Heimdall's, over the in-process one: what the probe door sees
/// when `engine: heimdall` runs the jobs (no sandbox of this crate to look into).
struct HeimdallLike {
    inner: Arc<dyn crate::job_runner::heimdall_core::JobRunner>,
    argv: Mutex<Vec<Vec<String>>>,
}

impl crate::job_runner::heimdall_core::JobRunner for HeimdallLike {
    fn run(
        &self,
        job: crate::job_runner::heimdall_core::Job,
    ) -> Result<
        crate::job_runner::heimdall_core::SignedResult,
        crate::job_runner::heimdall_core::RunError,
    > {
        if let crate::job_runner::heimdall_core::JobKind::Native { argv, .. } = &job.kind {
            self.argv.lock().unwrap().push(argv.clone());
        }
        self.inner.run(job)
    }
    fn verifying_key(&self) -> crate::job_runner::heimdall_core::VerifyingKey {
        self.inner.verifying_key()
    }
    fn features(&self) -> crate::job_runner::heimdall_core::Features {
        crate::job_runner::heimdall_core::Features {
            runner: "heimdall".into(),
            ..self.inner.features()
        }
    }
}

/// The worker's `probe` program as the test stands it in: what the box looks like from
/// inside, into `/out/probe.json`, and the scratch fill the job asked for echoed back.
fn probe_stand_in(
    job: &holger_job_runner::wire::WireJob,
    dirs: &holger_job_runner::wire::Dirs,
) -> holger_job_runner::wire::Answer {
    if job.program() != PROBE_PROGRAM {
        return crate::job_runner::programs::run(job, dirs);
    }
    let v = serde_json::json!({
        "uid": "65532 65532 65532 65532",
        "no_new_privs": "1",
        "fill_mib": job.params["fill_mib"],
    });
    std::fs::write(dirs.out.join(PROBE_FILE), v.to_string()).unwrap();
    holger_job_runner::wire::Answer::accepted(vec![PROBE_FILE.into()])
}

#[test]
fn the_probe_door_looks_into_heimdalls_runner() {
    let runner = Arc::new(HeimdallLike {
        inner: Arc::new(holger_job_runner::InProcessRunner::new(Arc::new(
            probe_stand_in,
        ))),
        argv: Mutex::new(Vec::new()),
    });
    let jobs = Arc::new(crate::job_runner::Jobs::new(runner.clone()));
    let w = ScanWorker::with_jobs(jobs, None, template()).unwrap();
    assert!(w.is_isolated(), "Heimdall's runner is not this process");
    let v = w
        .probe()
        .expect("the probe runs as a job of Heimdall's runner, not a refusal");
    assert_eq!(
        *runner.argv.lock().unwrap(),
        vec![vec![PROBE_PROGRAM.to_string()]],
        "one probe job, the worker's probe program"
    );
    assert_eq!(v["uid"], "65532 65532 65532 65532", "{v}");
    assert_eq!(v["no_new_privs"], "1", "{v}");
    assert_eq!(
        v["fill_mib"],
        holger_sandbox::Limits::default().scratch_mib + 1,
        "the probe writes one MiB past the scratch cap: {v}"
    );
    assert!(
        v["engine"].as_str().is_some_and(|e| e.contains("Heimdall")),
        "the answer names the engine it looked into: {v}"
    );
    assert_eq!(v["runs"], 1, "{v}");
    assert!(v["elapsed_ms"].is_u64(), "{v}");
}