xtask/src/scan_worker.rs
//! **`cargo xtask scan-worker`**: the scan sandbox's worker image (lane SANDBOX-SCAN).
//!
//! * `image [--bin <path>]`: build `holger-scan-worker` static (musl, release)
//! unless `--bin` names one, then mint `localhost/holger-scan-worker:<ver>`
//! from `distribution/scan-worker/Containerfile` (FROM scratch: no registry,
//! `--network none`, `--pull never`).
//! * `save --out <dir>`: the image as an OCI archive plus its sha256, the form
//! it crosses the airgap in (the secure side `podman load`s it; it never pulls).
//! * `stage [--out <dir>] [--bin <path>] [--crun <file>]`: **the sandbox entries a
//! main/twin medium carries** (lane HOLGER-MEDIA): the worker's rootfs as a tar
//! from the static binary (no engine needed), the pinned static `crun` (fetched
//! once into `~/.cache/holger/`, or `--crun`/`$HOLGER_CRUN`; its sha256 is pinned
//! in `holger_sandbox::media`, a mismatch is refused by name), the pinned ClamAV
//! image flattened to one tar (`podman create` + `export`, pulled by digest when
//! absent: this is the build side), Heimdall's static shim (`--heimdall-shim`,
//! `$HOLGER_HEIMDALL_SHIM`, or built once by `scripts/heimdall-shim-static.sh`) and the
//! released verify-bundle guests (`guests/heimdall/0.2.1/`, lane HOL-HEIMDALL-03), and
//! `manifest.toml` with each entry's sha256.
//! Written to `.sandbox-out/` (the installer rows name them as payload entries,
//! dwarves packs them into the app archive beside the server).
//! * `tests`: the container arms. Mints the image when it is missing, then
//! `scripts/scan-sandbox-live.sh`: a podman API service of its own on a socket
//! under the target directory and clamd in its sandbox (both stopped after),
//! then `holger-sandbox`'s and server-lib's live tests and the pull-through
//! tests with `HOLGER_TEST_SANDBOX=required`, in the toolchain container.
//!
//! The verb runs on the host (it needs the host's rootless podman); every build
//! and test it starts runs in the toolchain container.
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{bail, Context, Result};
/// The image the default sandbox config names (`holger_sandbox::DEFAULT_IMAGE`).
pub const IMAGE: &str = holger_sandbox::DEFAULT_IMAGE;
/// The static target the worker is built for.
pub const MUSL: &str = "x86_64-unknown-linux-musl";
fn run(cmd: &mut Command) -> Result<()> {
let shown = format!("{cmd:?}");
let st = cmd.status().with_context(|| format!("starting {shown}"))?;
if !st.success() {
bail!("{shown} exited {st}");
}
Ok(())
}
fn target_dir(root: &Path) -> PathBuf {
std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| root.join("target"))
}
/// Build the static worker in the toolchain container (the repository's parent
/// and the target directory mounted at their own paths); its path.
pub fn build(root: &Path) -> Result<PathBuf> {
let parent = root.parent().unwrap_or(root);
let target = target_dir(root);
std::fs::create_dir_all(&target)?;
let registry = std::env::var_os("CARGO_REGISTRY_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join(".cargo/registry")
});
let toolchain = std::env::var("TOOLCHAIN_IMAGE")
.unwrap_or_else(|_| "localhost/nordisk-cargo-toolchain:live".into());
run(Command::new("podman")
.args(["run", "--rm"])
.arg(format!("--volume={0}:{0}", parent.display()))
.arg(format!("--volume={0}:{0}", target.display()))
.arg(format!(
"--volume={}:/usr/local/cargo/registry",
registry.display()
))
.arg(format!("--env=CARGO_TARGET_DIR={}", target.display()))
.arg(format!("--workdir={}", root.display()))
.arg(&toolchain)
.args([
"cargo",
"build",
"--release",
"--locked",
"-p",
"holger-scan-worker",
"--target",
MUSL,
]))?;
Ok(target.join(MUSL).join("release").join("holger-scan-worker"))
}
/// Mint the image from `bin`.
pub fn image(root: &Path, bin: &Path) -> Result<()> {
let ctx = target_dir(root).join("scan-worker-context");
let _ = std::fs::remove_dir_all(&ctx);
std::fs::create_dir_all(&ctx)?;
std::fs::copy(bin, ctx.join("holger-scan-worker"))
.with_context(|| format!("{}", bin.display()))?;
std::fs::copy(
root.join("distribution/scan-worker/Containerfile"),
ctx.join("Containerfile"),
)?;
std::fs::write(
ctx.join("passwd"),
"nonroot:x:65532:65532:nonroot:/scratch:/sbin/nologin\n",
)?;
std::fs::write(ctx.join("group"), "nonroot:x:65532:\n")?;
run(Command::new("podman")
.args([
"build",
"--pull=never",
"--network=none",
"--layers=false",
"-t",
IMAGE,
])
.arg(&ctx))?;
println!("minted {IMAGE}");
Ok(())
}
/// The image as an OCI archive and its sha256.
pub fn save(out: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(out)?;
let tar = out.join("holger-scan-worker.oci.tar");
let _ = std::fs::remove_file(&tar);
run(Command::new("podman")
.args(["save", "--format", "oci-archive", "-o"])
.arg(&tar)
.arg(IMAGE))?;
let bytes = std::fs::read(&tar)?;
let hex = sha256_hex(&bytes);
std::fs::write(
out.join("holger-scan-worker.oci.tar.sha256"),
format!("{hex} holger-scan-worker.oci.tar\n"),
)?;
println!("saved {} (sha256 {hex})", tar.display());
Ok(tar)
}
/// The default staging directory of `stage` (the installer rows name its files).
pub const STAGE_OUT: &str = ".sandbox-out";
/// **`stage`**: write the sandbox entries into `out`. Returns the manifest.
pub fn stage(
root: &Path,
out: &Path,
bin: Option<PathBuf>,
crun: Option<PathBuf>,
) -> Result<holger_sandbox::media::Manifest> {
stage_with(root, out, bin, crun, None)
}
/// [`stage`] with Heimdall's shim given (`--heimdall-shim`).
pub fn stage_with(
root: &Path,
out: &Path,
bin: Option<PathBuf>,
crun: Option<PathBuf>,
shim: Option<PathBuf>,
) -> Result<holger_sandbox::media::Manifest> {
use holger_sandbox::media as m;
std::fs::create_dir_all(out).with_context(|| out.display().to_string())?;
let worker = match bin {
Some(b) => b,
None => build(root)?,
};
let worker_bytes = std::fs::read(&worker).with_context(|| worker.display().to_string())?;
if !worker_bytes.starts_with(b"\x7fELF") {
bail!("{} is not an ELF binary", worker.display());
}
let rootfs = m::worker_rootfs_tar(&worker_bytes);
let crun_bytes = crun_pinned(crun)?;
let clamav = clamav_rootfs_tar(out)?;
// Lane HOL-HEIMDALL-03: Heimdall's static shim and the released verify-bundle guests.
let shim_bytes = heimdall_shim(root, shim)?;
let guests = heimdall_guests(root)?;
let mut manifest = m::Manifest::of(&crun_bytes, &rootfs, &clamav).with(
m::HEIMDALL_SHIM,
m::Kind::Program,
&shim_bytes,
Some(format!(
"nordisk-heimdall-container {HEIMDALL_CONTAINER_VERSION} --features youki, static musl"
)),
);
for (n, b) in &guests {
manifest = manifest.with(n, m::Kind::Guest, b, Some(GUESTS_RELEASE.into()));
}
let mut entries: Vec<(&str, &Vec<u8>)> = vec![
(m::CRUN, &crun_bytes),
(m::WORKER_ROOTFS, &rootfs),
(m::CLAMAV_ROOTFS, &clamav),
(m::HEIMDALL_SHIM, &shim_bytes),
];
entries.extend(guests.iter().map(|(n, b)| (n.as_str(), b)));
for (name, bytes) in entries {
let p = out.join(name);
std::fs::write(&p, bytes).with_context(|| p.display().to_string())?;
std::fs::write(
out.join(format!("{name}.sha256")),
format!("{} {name}\n", m::sha256_hex(bytes)),
)?;
}
std::fs::write(out.join(m::MANIFEST), toml::to_string(&manifest)?)?;
for e in &manifest.entries {
println!(
"staged {}/{} ({} bytes, sha256 {})",
out.display(),
e.name,
e.bytes,
&e.sha256[..16]
);
}
Ok(manifest)
}
/// Is `out` a complete staging (every entry present and as its manifest says)?
/// The doors that compose a medium ask before the forge runs.
pub fn staged_ok(out: &Path) -> Result<holger_sandbox::media::Manifest> {
use holger_sandbox::media as m;
let text = std::fs::read_to_string(out.join(m::MANIFEST)).with_context(|| {
format!(
"{}: run `cargo xtask scan-worker stage`",
out.join(m::MANIFEST).display()
)
})?;
let manifest: m::Manifest = toml::from_str(&text)?;
manifest.check().map_err(|e| anyhow::anyhow!("{e}"))?;
for e in &manifest.entries {
let bytes = std::fs::read(out.join(&e.name)).with_context(|| {
format!(
"{}: run `cargo xtask scan-worker stage`",
out.join(&e.name).display()
)
})?;
manifest
.verify(&e.name, &bytes)
.map_err(|e| anyhow::anyhow!("{}: {e}", out.display()))?;
}
Ok(manifest)
}
/// **Does the staging carry Heimdall, as this tree has it** (lane HOL-HEIMDALL-03)? The shim
/// and every file of the vendored guest release, each the tree's own bytes; a staging from
/// before them (or from another release) is staged again.
pub fn heimdall_is_current(root: &Path, manifest: &holger_sandbox::media::Manifest) -> Result<()> {
use holger_sandbox::media as m;
if !manifest.carries_heimdall() {
bail!(
"[sandbox-stage-stale] the staging carries no {}: staging again",
m::HEIMDALL_SHIM
);
}
for (n, b) in heimdall_guests(root)? {
let staged = manifest
.entry(&n)
.map(|e| e.sha256.clone())
.unwrap_or_default();
if staged != m::sha256_hex(&b) {
bail!("[sandbox-stage-stale] the staged {n} is not this tree's: staging again");
}
}
Ok(())
}
/// **Is the staging's worker rootfs THIS tree's worker** (lane HOL-NCSC)? `staged_ok` holds a
/// staging to its own manifest only; a worker staged from an older tree would ride a medium
/// (the KVM installtest of 2026-10-31: a worker from before the job API, every scan exit 1).
pub fn worker_is_current(
manifest: &holger_sandbox::media::Manifest,
worker_bytes: &[u8],
) -> Result<()> {
use holger_sandbox::media as m;
let want = m::sha256_hex(&m::worker_rootfs_tar(worker_bytes));
let staged = manifest
.entries
.iter()
.find(|e| e.name == m::WORKER_ROOTFS)
.map(|e| e.sha256.clone())
.unwrap_or_default();
if staged == want {
return Ok(());
}
bail!(
"[sandbox-stage-stale] the staged {} (sha256 {}) is not this tree's worker (its rootfs is {}): staging again",
m::WORKER_ROOTFS,
staged.get(..16).unwrap_or(&staged),
&want[..16]
)
}
/// The heimdall-container whose shim the medium carries (`scripts/heimdall-shim-static.sh`).
pub const HEIMDALL_CONTAINER_VERSION: &str = "0.2.1";
/// The released guests the medium carries.
pub const GUESTS_RELEASE: &str = "heimdall-guests-0.2.1";
/// **Heimdall's static shim**: `--heimdall-shim <file>`, `$HOLGER_HEIMDALL_SHIM`, the cache,
/// else built once into the cache by `scripts/heimdall-shim-static.sh` (static musl, libseccomp
/// linked in). A shim that is no ELF, or one that needs a dynamic loader, is refused by name:
/// the appliance has no C library.
fn heimdall_shim(root: &Path, given: Option<PathBuf>) -> Result<Vec<u8>> {
let cache = PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
.join(".cache/holger")
.join(format!(
"heimdall-container-shim-{HEIMDALL_CONTAINER_VERSION}-musl"
));
let from = given
.or_else(|| std::env::var_os("HOLGER_HEIMDALL_SHIM").map(PathBuf::from))
.unwrap_or_else(|| cache.clone());
if !from.is_file() && from == cache {
println!(
"building heimdall-container-shim {HEIMDALL_CONTAINER_VERSION} static into {}",
cache.display()
);
run(Command::new(root.join("scripts/heimdall-shim-static.sh")).arg(&cache))?;
}
let bytes = std::fs::read(&from).with_context(|| from.display().to_string())?;
check_static_elf(&bytes).with_context(|| from.display().to_string())?;
Ok(bytes)
}
/// A static ELF: no `PT_INTERP` (no dynamic loader named).
pub fn check_static_elf(b: &[u8]) -> Result<()> {
if b.len() < 64 || !b.starts_with(b"\x7fELF") || b[4] != 2 {
bail!("[heimdall-shim-not-static] not a 64-bit ELF");
}
let u16_at = |o: usize| u16::from_le_bytes([b[o], b[o + 1]]) as usize;
let u64_at = |o: usize| {
let mut a = [0u8; 8];
a.copy_from_slice(&b[o..o + 8]);
u64::from_le_bytes(a) as usize
};
let (phoff, phentsize, phnum) = (u64_at(0x20), u16_at(0x36), u16_at(0x38));
for i in 0..phnum {
let o = phoff + i * phentsize;
if o + 4 > b.len() {
bail!("[heimdall-shim-not-static] a program header outside the file");
}
if u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) == 3 {
bail!("[heimdall-shim-not-static] it names a dynamic loader (PT_INTERP): the appliance has no C library");
}
}
Ok(())
}
/// The released guests, every file of `guests/heimdall/0.2.1/` but its pins file (held to
/// their pins by holger-job-runner's `vendored_heimdall_guests`). An LFS pointer is refused.
fn heimdall_guests(root: &Path) -> Result<Vec<(String, Vec<u8>)>> {
let dir = root.join(holger_recipe::packaging::HEIMDALL_GUESTS_DIR);
let mut v = Vec::new();
for n in holger_recipe::packaging::HEIMDALL_GUESTS {
let p = dir.join(n);
let b = std::fs::read(&p).with_context(|| p.display().to_string())?;
if b.starts_with(b"version https://git-lfs") {
bail!(
"{} is an LFS pointer: run `git lfs pull` first",
p.display()
);
}
v.push((n.to_string(), b));
}
Ok(v)
}
/// The pinned static crun: `--crun <file>`, `$HOLGER_CRUN`, the cache, else fetched once.
fn crun_pinned(given: Option<PathBuf>) -> Result<Vec<u8>> {
use holger_sandbox::media as m;
let cache = PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
.join(".cache/holger")
.join(format!(
"crun-{}-linux-amd64-disable-systemd",
m::CRUN_VERSION
));
let from = given
.or_else(|| std::env::var_os("HOLGER_CRUN").map(PathBuf::from))
.unwrap_or_else(|| cache.clone());
let bytes = match std::fs::read(&from) {
Ok(b) => b,
Err(_) if from == cache => {
println!(
"fetching crun {} (pinned sha256 {}…) into {}",
m::CRUN_VERSION,
&m::CRUN_SHA256[..16],
cache.display()
);
let resp = ureq::get(m::CRUN_URL)
.call()
.with_context(|| m::CRUN_URL.to_string())?;
let mut b = Vec::new();
std::io::Read::read_to_end(&mut resp.into_body().into_reader(), &mut b)?;
if m::sha256_hex(&b) != m::CRUN_SHA256 {
bail!(
"the fetched crun is not the pinned one: sha256 {} (pinned {})",
m::sha256_hex(&b),
m::CRUN_SHA256
);
}
std::fs::create_dir_all(cache.parent().unwrap())?;
std::fs::write(&cache, &b)?;
b
}
Err(e) => bail!("{}: {e}", from.display()),
};
if m::sha256_hex(&bytes) != m::CRUN_SHA256 {
bail!(
"{} is not the pinned crun {}: sha256 {} (pinned {})",
from.display(),
m::CRUN_VERSION,
m::sha256_hex(&bytes),
m::CRUN_SHA256
);
}
Ok(bytes)
}
/// The pinned ClamAV image, flattened: one container created from it, exported.
fn clamav_rootfs_tar(out: &Path) -> Result<Vec<u8>> {
use holger_sandbox::media as m;
let exists = Command::new("podman")
.args(["image", "exists", m::CLAMAV_IMAGE])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success());
if !exists {
println!(
"pulling {} (the build side; the secure side never pulls)",
m::CLAMAV_IMAGE
);
run(Command::new("podman").args(["pull", m::CLAMAV_IMAGE]))?;
}
let name = format!("holger-stage-clamav-{}", std::process::id());
let tar = out.join(format!("{}.exporting", m::CLAMAV_ROOTFS));
let _ = std::fs::remove_file(&tar);
run(Command::new("podman")
.args([
"create",
"--pull=never",
"--network=none",
"--name",
&name,
m::CLAMAV_IMAGE,
])
.stdout(Stdio::null()))?;
let exported = run(Command::new("podman")
.args(["export", "-o"])
.arg(&tar)
.arg(&name));
let _ = Command::new("podman")
.args(["rm", "--force", &name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
exported?;
let bytes = std::fs::read(&tar)?;
let _ = std::fs::remove_file(&tar);
Ok(bytes)
}
fn sha256_hex(b: &[u8]) -> String {
use sha2::Digest as _;
sha2::Sha256::digest(b)
.iter()
.map(|x| format!("{x:02x}"))
.collect()
}
fn image_exists() -> bool {
Command::new("podman")
.args(["image", "exists", IMAGE])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
/// The live tests: `scripts/scan-sandbox-live.sh` (a podman API service and a
/// sandboxed clamd of its own on the host, the tests in the toolchain container).
pub fn tests(root: &Path, extra: &[String]) -> Result<()> {
if !image_exists() {
let bin = build(root)?;
image(root, &bin)?;
}
run(Command::new(root.join("scripts/scan-sandbox-live.sh"))
.args(extra)
.env("CARGO_TARGET_DIR", target_dir(root)))
}
/// The verb.
pub fn main(root: &Path, args: &[String]) -> Result<()> {
let flag = |f: &str| {
args.iter()
.position(|a| a == f)
.and_then(|i| args.get(i + 1))
.map(PathBuf::from)
};
match args.first().map(String::as_str) {
Some("image") => {
let bin = match flag("--bin") {
Some(b) => b,
None => build(root)?,
};
image(root, &bin)
}
Some("save") => save(&flag("--out").unwrap_or_else(|| target_dir(root).join("scan-worker"))).map(|_| ()),
Some("stage") => stage_with(root, &flag("--out").unwrap_or_else(|| root.join(STAGE_OUT)), flag("--bin"), flag("--crun"), flag("--heimdall-shim")).map(|_| ()),
Some("tests") => tests(root, &args[1..]),
_ => bail!("usage: cargo xtask scan-worker <image [--bin <path>] | save [--out <dir>] | stage [--out <dir>] [--bin <path>] [--crun <file>] [--heimdall-shim <file>] | tests>"),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Lane HOL-HEIMDALL-03: the medium's shim must be static (the appliance has no C library):
/// a 64-bit ELF with no PT_INTERP passes, this (dynamic) test binary and a non-ELF do not.
#[test]
fn the_heimdall_shim_must_be_a_static_elf() {
let me = std::fs::read(std::env::current_exe().unwrap()).unwrap();
let e = check_static_elf(&me).unwrap_err().to_string();
assert!(
e.contains("PT_INTERP") || e.contains("not a 64-bit ELF"),
"{e}"
);
assert!(check_static_elf(b"#!/bin/sh\n").is_err());
// A minimal static ELF header: one PT_LOAD program header.
let mut b = vec![0u8; 64 + 56];
b[..4].copy_from_slice(b"\x7fELF");
b[4] = 2;
b[0x20..0x28].copy_from_slice(&64u64.to_le_bytes());
b[0x36..0x38].copy_from_slice(&56u16.to_le_bytes());
b[0x38..0x3a].copy_from_slice(&1u16.to_le_bytes());
b[64..68].copy_from_slice(&1u32.to_le_bytes());
check_static_elf(&b).unwrap();
b[64..68].copy_from_slice(&3u32.to_le_bytes());
assert!(check_static_elf(&b)
.unwrap_err()
.to_string()
.contains("PT_INTERP"));
}
/// `staged_ok` is the door's gate: a staging whose entry differs from its
/// manifest is refused by name, and nothing composes.
#[test]
fn a_staging_is_checked_entry_by_entry() {
use holger_sandbox::media as m;
let dir = tempfile::tempdir().unwrap();
assert!(staged_ok(dir.path())
.unwrap_err()
.to_string()
.contains("scan-worker stage"));
let manifest = m::Manifest::of(b"crun", b"worker", b"clamav");
std::fs::write(
dir.path().join(m::MANIFEST),
toml::to_string(&manifest).unwrap(),
)
.unwrap();
for (n, b) in [
(m::CRUN, &b"crun"[..]),
(m::WORKER_ROOTFS, b"worker"),
(m::CLAMAV_ROOTFS, b"clamav"),
] {
std::fs::write(dir.path().join(n), b).unwrap();
}
assert_eq!(staged_ok(dir.path()).unwrap(), manifest);
std::fs::write(dir.path().join(m::CRUN), b"crux").unwrap();
assert!(staged_ok(dir.path())
.unwrap_err()
.to_string()
.contains("sha256"));
}
/// ★ The KVM installtest (2026-10-31): a staging from 2026-10-30 carried a worker built
/// before the job API (HOL-SANDBOX-TRAIT) and rode the medium, because `staged_ok` only
/// checks a staging against its own manifest. **The staged worker is the tree's worker**,
/// or the door stages again.
#[test]
fn a_staged_worker_from_another_tree_is_stale() {
use holger_sandbox::media as m;
let (old, new) = (
b"\x7fELF old worker".as_slice(),
b"\x7fELF new worker".as_slice(),
);
assert_eq!(
m::worker_rootfs_tar(new),
m::worker_rootfs_tar(new),
"the rootfs tar is a function of the worker"
);
let staged = m::Manifest::of(b"crun", &m::worker_rootfs_tar(old), b"clamav");
worker_is_current(&staged, old).expect("the staging holds this worker");
let e = worker_is_current(&staged, new).unwrap_err().to_string();
assert!(e.contains("[sandbox-stage-stale]"), "{e}");
}
/// **No shipped build can scan in-process.** `sandbox-off` (the test build's
/// `sandbox: off`) is named by dev-dependencies only: no declared row and no
/// `[features]` entry of any manifest turns it on.
#[test]
fn no_shipped_row_or_feature_turns_the_sandbox_off() {
for v in holger_recipe::variants::variants() {
let row = format!("{v:?}");
assert!(
!row.contains("sandbox-off"),
"the row {} names sandbox-off: {row}",
v.name
);
}
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
for manifest in [
"server/lib/Cargo.toml",
"server/cli/Cargo.toml",
"server/scan-worker/Cargo.toml",
] {
let text = std::fs::read_to_string(root.join(manifest)).unwrap();
let mut section = String::new();
for line in text.lines() {
let t = line.trim();
if t.starts_with('[') {
section = t.to_string();
continue;
}
if t.starts_with('#') || !t.contains("sandbox-off") {
continue;
}
let defines_it = section == "[features]" && t.starts_with("sandbox-off =");
assert!(
section == "[dev-dependencies]" || defines_it,
"{manifest} {section} turns sandbox-off on outside a dev-dependency: {t}"
);
}
}
}
}