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

nordisk/holger

server/scan-worker/tests/worker_seccomp_profile.rs

//! **The scan worker under its GENERATED seccomp profile, on a real kernel** (lane
//! HEIMDALL-ALLIN prof): `engine: heimdall` with the real `heimdall-container-shim` and the
//! real, static `holger-scan-worker`, exactly as `jobs_under_heimdall.rs` lays them, with
//! the worker's generated profile (`jobs/runner/seccomp/holger-scan-worker/`, measured with
//! heimdall-probe over these very jobs) installed by Heimdall on top of its fixed one:
//!
//! * every verify-bundle job (native and the split wasm guest, good and changed bundles)
//!   runs green under it, and its signed result says `Generated` with the pinned digest;
//! * a scan of every real package of the corpus (`HOLGER_PROFILE_PACKAGES`: pip, maven,
//!   rust, npm, helm, debian, rpm, oci, raw; listed with their sha256 in
//!   `jobs/runner/seccomp/holger-scan-worker/README.md`) and of the made-up subjects below
//!   (a gzip bomb, YARA and secret hits, model files, an empty file) runs green, all kinds;
//! * a job past its wall clock is still Holger's timeout, not a seccomp kill;
//! * a call outside the profile ends the job: the probe (which tries a TCP connect on
//!   purpose) under the profile is Holger's killed worker with `heimdall-seccomp-1` first;
//! * the probe without it keeps Heimdall's fixed profile, as the wiring promises.
//!
//! `HOLGER_PROFILE_CAPTURE=1` runs the same jobs under the fixed profile only (the
//! measurement run: scripts/scan-worker-profile.sh probes the jobs it stages). `#[ignore]`d:
//! scripts/heimdall-kernel-tests.sh runs them in privileged rootless podman.

mod common;

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

use server_lib::job_runner::heimdall_core::{
    Job, JobId, JobResult, Nonce, Policy, Protection, Verdict,
};
use server_lib::job_runner::holger_job_runner::{seccomp as profile, worker};
use server_lib::job_runner::holger_sandbox::{EngineKind, HeimdallSettings, SandboxConfig};
use server_lib::job_runner::verify_bundle as vb;
use server_lib::job_runner::{JobDirs, Jobs, SandboxRule};

fn need(var: &str) -> PathBuf {
    PathBuf::from(
        std::env::var(var)
            .unwrap_or_else(|_| panic!("{var} is not set: run scripts/heimdall-kernel-tests.sh")),
    )
}

/// The measurement run: the same jobs under Heimdall's fixed profile only.
fn capture() -> bool {
    std::env::var_os("HOLGER_PROFILE_CAPTURE").is_some()
}

fn cfg(dir: &Path) -> SandboxConfig {
    let worker = std::fs::read(need("HOLGER_SCAN_WORKER_STATIC")).unwrap();
    let tar = dir.join("holger-scan-worker.rootfs.tar");
    std::fs::write(
        &tar,
        server_lib::job_runner::holger_sandbox::media::worker_rootfs_tar(&worker),
    )
    .unwrap();
    let mut c = SandboxConfig {
        engine: EngineKind::Heimdall,
        userns_base: 1,
        work_dir: Some(dir.join("work").display().to_string()),
        heimdall: Some(HeimdallSettings {
            image_layout: dir.join("oci").display().to_string(),
            rootfs_tar: Some(tar.display().to_string()),
            shim: Some(need("HEIMDALL_CONTAINER_SHIM").display().to_string()),
            cgroup_parent: std::env::var("HOLGER_HEIMDALL_CGROUP_PARENT")
                .unwrap_or_else(|_| "heimdall".into()),
            work_dir: Some(dir.join("heimdall").display().to_string()),
            fallback_plain: false,
            host_id_count: 60000,
            generated_seccomp: !capture(),
            ..Default::default()
        }),
        ..Default::default()
    };
    c.limits.timeout_secs = 120;
    c
}

fn jobs(dir: &Path) -> Jobs {
    let j = Jobs::from_config(SandboxRule::Required, Some(cfg(dir))).expect("Heimdall starts");
    assert_eq!(j.fallback(), None, "Heimdall itself, no fallback");
    assert_eq!(j.features().runner, "heimdall");
    j
}

/// The pinned profile, as the server names it.
fn the_profile(dir: &Path) -> server_lib::job_runner::heimdall_core::SeccompProfileRef {
    profile::scan_worker_profile(&dir.join("profile"))
        .unwrap()
        .expect("x86_64 has a measured profile")
}

/// Under the generated profile (not in the measurement run) the signed result says so.
fn assert_generated(r: &JobResult) {
    if capture() {
        return;
    }
    assert_eq!(
        r.runner.seccomp,
        Protection::Generated {
            profile_digest: profile::SCAN_WORKER_SHA256.into()
        },
        "{r:?}"
    );
}

fn verify_job(
    base: &Path,
    bundle: &[u8],
    policy: &update_bundle::TrustPolicy,
    guest: bool,
) -> (Job, JobDirs) {
    let id = JobId::random();
    let dirs = JobDirs::new(base, &id).unwrap();
    std::fs::write(dirs.input.join(vb::BUNDLE_FILE), bundle).unwrap();
    std::fs::write(
        dirs.input.join(vb::POLICY_FILE),
        serde_json::to_vec(policy).unwrap(),
    )
    .unwrap();
    let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let mut read = Vec::new();
    if guest {
        let pem = std::fs::read(repo.join("holger-recipe/trust/korp-root-ca.pem")).unwrap();
        std::fs::write(
            dirs.input.join(vb::GUEST_ROOT_FILE),
            holger_job_runner::guests::der_of(&pem).unwrap(),
        )
        .unwrap();
        read.push(repo.join(holger_job_runner::guests::DIR));
    }
    let params = serde_json::to_value(vb::Params {
        time: vb::TimeParam::At {
            unix_ms: chrono::Utc::now().timestamp_millis(),
            trusted: false,
        },
        product: None,
        model: None,
        revision: None,
        guest: guest.then_some(vb::GuestParam { read: 0 }),
    })
    .unwrap();
    let job = Job {
        id,
        nonce: Nonce::random(),
        kind: worker(&[vb::PROGRAM]),
        input_dir: dirs.input.clone(),
        output_dir: dirs.output.clone(),
        params,
        policy: Policy {
            read,
            limits: server_lib::job_runner::heimdall_core::Limits {
                wall: std::time::Duration::from_secs(120),
                memory_bytes: if guest { 2 << 30 } else { 512 << 20 },
                ..Default::default()
            },
            ..Default::default()
        },
    };
    (job, dirs)
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn every_verify_bundle_job_runs_green_under_the_generated_profile() {
    let tmp = tempfile::tempdir().unwrap();
    let j = jobs(tmp.path());
    let (bundle, policy) = common::signed_bundle(tmp.path());
    let good = std::fs::read(&bundle).unwrap();
    let mut bad = good.clone();
    let n = bad.len() / 2;
    bad[n] ^= 0x40;
    for guest in [false, true] {
        let (job, dirs) = verify_job(&tmp.path().join("jobs"), &good, &policy, guest);
        let r = j.run(job).expect("a believed result");
        assert_eq!(r.verdict, Verdict::Accepted, "guest {guest}: {r:?}");
        assert_generated(&r);
        let (members, _, raw) = vb::read_outputs(&dirs.output).unwrap();
        assert_eq!(
            members.get("payload/firmware.bin").map(Vec::as_slice),
            Some(&b"hello"[..])
        );
        assert_eq!(raw.unwrap(), good);

        let (job, _dirs) = verify_job(&tmp.path().join("jobs"), &bad, &policy, guest);
        let r = j.run(job).expect("a believed refusal");
        match &r.verdict {
            Verdict::Refused { reason, .. } => assert!(
                !reason.contains("heimdall-seccomp"),
                "guest {guest}: the job's own refusal, not a kill: {reason}"
            ),
            v => panic!("guest {guest}: a changed bundle is refused: {v:?}"),
        }
        assert_generated(&r);
    }
}

/// The made-up subjects beside the real packages: each drives a scanner path the packages
/// do not (a bomb, a YARA hit, a secret, model files, an empty file).
fn made_up() -> Vec<(&'static str, String, Vec<u8>)> {
    use std::io::Write as _;
    let bomb = {
        let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
        for _ in 0..64 {
            e.write_all(&vec![0u8; 1 << 20]).unwrap();
        }
        e.finish().unwrap()
    };
    let eicar = b"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*".to_vec();
    let secret = b"aws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA\n-----END RSA PRIVATE KEY-----\n".to_vec();
    let safetensors = {
        let header = br#"{"w":{"dtype":"F32","shape":[2],"data_offsets":[0,8]},"__metadata__":{"format":"pt"}}"#;
        let mut b = (header.len() as u64).to_le_bytes().to_vec();
        b.extend_from_slice(header);
        b.extend_from_slice(&[0u8; 8]);
        b
    };
    // A protocol-2 pickle that names os.system (the pickle family is blocked).
    let pickle = b"\x80\x02cos\nsystem\nq\x00X\x04\x00\x00\x00trueq\x01\x85q\x02Rq\x03.".to_vec();
    vec![
        ("pip", "demo-1.0.tar.gz".into(), bomb),
        ("raw", "eicar.com".into(), eicar),
        ("raw", "settings.env".into(), secret),
        ("raw", "model.safetensors".into(), safetensors),
        ("raw", "model.pkl".into(), pickle),
        ("raw", "notes.txt".into(), b"hello".to_vec()),
        ("raw", "empty".into(), Vec::new()),
    ]
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn scans_of_real_packages_run_green_under_the_generated_profile() {
    use server_lib::sandbox::{JobTemplate, ScanWorker};
    use server_lib::scanner::{ScanSubject, ScannerKind};
    let tmp = tempfile::tempdir().unwrap();
    let j = Arc::new(jobs(tmp.path()));
    let rules = tmp.path().join("yara");
    std::fs::create_dir_all(&rules).unwrap();
    std::fs::write(
        rules.join("eicar.yar"),
        "rule eicar_test_string { strings: $a = \"EICAR-STANDARD-ANTIVIRUS-TEST-FILE\" condition: $a }\n",
    )
    .unwrap();
    let mut subjects: Vec<(String, String, Vec<u8>)> = Vec::new();
    let pkgs = need("HOLGER_PROFILE_PACKAGES");
    let mut formats: Vec<_> = std::fs::read_dir(&pkgs)
        .unwrap()
        .map(|e| e.unwrap().path())
        .collect();
    formats.sort();
    for d in formats {
        let format = d.file_name().unwrap().to_string_lossy().into_owned();
        let mut files: Vec<_> = std::fs::read_dir(&d)
            .unwrap()
            .map(|e| e.unwrap().path())
            .collect();
        files.sort();
        for f in files {
            let name = f.file_name().unwrap().to_string_lossy().into_owned();
            subjects.push((format.clone(), name, std::fs::read(&f).unwrap()));
        }
    }
    assert!(
        subjects.len() >= 9,
        "the package corpus is there: {}",
        pkgs.display()
    );
    for (format, name, bytes) in made_up() {
        subjects.push((format.into(), name, bytes));
    }
    let mut workers = std::collections::BTreeMap::new();
    for (format, name, bytes) in &subjects {
        let w = workers.entry(format.clone()).or_insert_with(|| {
            let inspect = holger_inspect::InspectConfig {
                yara: Some(holger_inspect::malware::YaraConfig {
                    rules: Some(rules.display().to_string()),
                    ..Default::default()
                }),
                ..Default::default()
            };
            ScanWorker::with_jobs_config(
                j.clone(),
                SandboxRule::Required,
                Some(cfg(tmp.path())),
                JobTemplate {
                    format_name: format.clone(),
                    inspect,
                    kinds: ScannerKind::ALL.to_vec(),
                    secrets: Some(server_lib::scanner::secrets::SecretRules::builtin()),
                    ..Default::default()
                },
            )
            .unwrap()
        });
        let r = w
            .report(&ScanSubject::new(name, format, bytes))
            .unwrap_or_else(|e| panic!("{format}/{name}: the scan job ran: {e:?}"));
        eprintln!("{format}/{name}: {} finding(s)", r.inspect.findings.len());
    }
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_job_past_its_wall_clock_is_still_holgers_timeout_under_the_generated_profile() {
    use server_lib::sandbox::{JobTemplate, ScanWorker};
    use server_lib::scanner::{ScanSubject, ScannerKind};
    use std::io::Write as _;
    let tmp = tempfile::tempdir().unwrap();
    let j = Arc::new(jobs(tmp.path()));
    let mut c = cfg(tmp.path());
    c.limits.timeout_secs = 2;
    let loose = holger_inspect::InspectConfig {
        limits: holger_inspect::Limits {
            max_ratio: u64::MAX / 2,
            ratio_floor_bytes: u64::MAX / 2,
            max_total_expanded: u64::MAX / 2,
            max_entry_bytes: u64::MAX / 2,
            ..Default::default()
        },
        ..Default::default()
    };
    let w = ScanWorker::with_jobs_config(
        j,
        SandboxRule::Required,
        Some(c),
        JobTemplate {
            format_name: "pip".into(),
            inspect: loose,
            kinds: vec![ScannerKind::Inspection],
            ..Default::default()
        },
    )
    .unwrap();
    let member = {
        let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
        for _ in 0..64 {
            e.write_all(&vec![0u8; 1 << 20]).unwrap();
        }
        e.finish().unwrap()
    };
    let outer: Vec<u8> = std::iter::repeat_n(member.as_slice(), 512)
        .flatten()
        .copied()
        .collect();
    let f = w.inspect(&ScanSubject::new("demo-1.0.tar.gz", "pip", &outer));
    let d = &f.findings[0].detail;
    assert!(d.contains("holger-sandbox-2"), "a timeout: {d}");
    assert!(!d.contains("heimdall-seccomp"), "not a seccomp kill: {d}");
}

fn probe_job(base: &Path, policy: Policy) -> (Job, JobDirs) {
    let id = JobId::random();
    let dirs = JobDirs::new(base, &id).unwrap();
    let job = Job {
        id,
        nonce: Nonce::random(),
        kind: worker(&[holger_job_runner::PROBE_PROGRAM]),
        input_dir: dirs.input.clone(),
        output_dir: dirs.output.clone(),
        params: serde_json::json!({ "fill_mib": 65 }),
        policy,
    };
    (job, dirs)
}

fn probe_policy() -> Policy {
    Policy {
        limits: server_lib::job_runner::heimdall_core::Limits {
            wall: std::time::Duration::from_secs(60),
            memory_bytes: 512 << 20,
            ..Default::default()
        },
        ..Default::default()
    }
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_call_outside_the_generated_profile_ends_the_job_as_a_seccomp_kill() {
    if capture() {
        return;
    }
    let tmp = tempfile::tempdir().unwrap();
    let j = jobs(tmp.path());
    let mut policy = probe_policy();
    policy.seccomp = Some(the_profile(tmp.path()));
    let (job, _dirs) = probe_job(&tmp.path().join("jobs"), policy);
    let r = j.run(job).expect("a believed refusal");
    match &r.verdict {
        Verdict::Refused { code, reason } => {
            assert_eq!(code, server_lib::errcode::SANDBOX_KILLED.code, "{r:?}");
            // Holger's frozen code first, Heimdall's code first in the reason.
            assert!(reason.contains("] heimdall-seccomp-1: "), "{reason}");
            assert!(reason.contains(profile::SCAN_WORKER_SHA256), "{reason}");
        }
        v => panic!("the probe's connect is outside the profile: {v:?}"),
    }
    assert_generated(&r);
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn the_probe_keeps_heimdalls_fixed_profile() {
    let tmp = tempfile::tempdir().unwrap();
    let j = jobs(tmp.path());
    let (job, _dirs) = probe_job(&tmp.path().join("jobs"), probe_policy());
    let r = j.run(job).expect("a believed result");
    assert_eq!(r.verdict, Verdict::Accepted, "{r:?}");
    assert_eq!(r.runner.seccomp, Protection::Default, "{r:?}");
}