jobs/runner/src/heimdall_tests.rs
//! [`crate::heimdall::HeimdallJobs`] over a runner that plays Heimdall's side of the
//! scan-worker job contract (heimdall docs/API.md): `/in` is the staged input, the
//! worker's argv is exactly `[<worker>]`, its stdout goes nowhere, the answer is a file in
//! `/out`, the runner lists and signs every output with its per-boot key. The real kernel
//! run is `server/scan-worker/tests/jobs_under_heimdall.rs`.
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use heimdall_core::{
BootId, BootKey, ExitInfo, Features, ImageRef, Job, JobKind, JobResult, JobRunner,
KernelFeatures, KillReason, NetAllow, Output, Policy, Protection, RunError, SignedResult,
Verdict, VerifyingKey,
};
use crate::heimdall::{holger_code_of, mint_layout, HeimdallJobs, WORKER_BIN, WORKER_ENV};
use crate::wire::{deliver, Answer, Dirs, Program, WireJob, ANSWER_FILE};
use crate::{accept, worker, ResultKey};
const DIGEST: &str = "sha256:1111111111111111111111111111111111111111111111111111111111111111";
/// What the fake does after the worker ran.
#[derive(Clone, PartialEq, Eq, Debug)]
enum Twist {
None,
/// The signed result says this Heimdall refusal (with this exit).
Refuse(&'static str, ExitInfo),
/// Signed with a key nobody pinned.
ForeignKey,
/// The result of another job's nonce.
OtherNonce,
/// An output's listed sha256 is not its bytes.
WrongListing,
/// The worker writes no answer file.
NoAnswer,
}
#[derive(Default, Clone)]
struct Saw {
image: Option<ImageRef>,
argv: Vec<String>,
wire: Option<WireJob>,
input_tree: Vec<String>,
limits: Option<heimdall_core::Limits>,
/// The staged input file's link count: 1 when it was copied.
input_nlink: u64,
/// The network allowance Heimdall was handed.
net: Option<NetAllow>,
/// The seccomp profile Heimdall was handed.
seccomp: Option<heimdall_core::SeccompProfileRef>,
}
struct FakeHeimdall {
key: BootKey,
prog: Arc<Program>,
twist: Twist,
saw: Arc<Mutex<Saw>>,
}
fn kernel() -> KernelFeatures {
KernelFeatures {
release: "7.0.0-test".into(),
seccomp_filter: true,
user_notif: true,
notif_addfd: true,
landlock_abi: Some(6),
cgroup_v2: true,
cgroup_controllers: vec!["cpu".into(), "memory".into(), "pids".into()],
user_namespaces: true,
}
}
fn heimdall_features() -> Features {
Features {
runner: "heimdall".into(),
container: true,
seccomp: Protection::Default,
landlock: true,
broker: false,
wasm: false,
image_verified_each_start: true,
}
}
fn tree(dir: &Path, base: &Path, out: &mut Vec<String>) {
let mut es: Vec<_> = std::fs::read_dir(dir).unwrap().flatten().collect();
es.sort_by_key(|e| e.file_name());
for e in es {
let rel = e.path().strip_prefix(base).unwrap().display().to_string();
if e.file_type().unwrap().is_dir() {
out.push(format!("{rel}/"));
tree(&e.path(), base, out);
} else {
out.push(rel);
}
}
}
impl JobRunner for FakeHeimdall {
fn run(&self, job: Job) -> Result<SignedResult, RunError> {
let JobKind::Native { image, argv } = &job.kind else {
return Err(RunError::Unsupported {
what: "wasm".into(),
});
};
// The contract: `/in` is input_dir, `/in/job.json` the worker's document.
let wire: WireJob =
serde_json::from_slice(&std::fs::read(job.input_dir.join("job.json")).unwrap())
.unwrap();
let mut t = Vec::new();
tree(&job.input_dir, &job.input_dir, &mut t);
*self.saw.lock().unwrap() = Saw {
image: Some(image.clone()),
argv: argv.clone(),
wire: Some(wire.clone()),
input_tree: t,
limits: Some(job.policy.limits),
input_nlink: {
use std::os::unix::fs::MetadataExt as _;
std::fs::metadata(job.input_dir.join("i/a"))
.unwrap()
.nlink()
},
net: job.policy.net.clone(),
seccomp: job.policy.seccomp.clone(),
};
// The worker, as holger-scan-worker's main runs it over `/in`.
let dirs = Dirs {
input: job.input_dir.join("i"),
read: (0..wire.reads)
.map(|n| job.input_dir.join(format!("r{n}")))
.collect(),
out: job.output_dir.clone(),
};
let a = (self.prog)(&wire, &dirs);
if self.twist != Twist::NoAnswer {
assert_eq!(
deliver(&wire, &dirs.out, &a).unwrap(),
None,
"the answer is a file"
);
}
// Heimdall lists every regular file in /out itself.
let mut outputs = Vec::new();
let mut names: Vec<_> = std::fs::read_dir(&job.output_dir)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
for n in names {
let b = std::fs::read(job.output_dir.join(&n)).unwrap();
outputs.push(Output::of(&n, &b));
}
if self.twist == Twist::WrongListing {
outputs[0] = Output::of(outputs[0].name.clone(), b"not these bytes");
}
let (verdict, exit) = match self.twist.clone() {
Twist::Refuse(code, exit) => (
Verdict::Refused {
code: code.into(),
reason: "the fake says so".into(),
},
exit,
),
_ => (Verdict::Accepted, ExitInfo::exited(0)),
};
let nonce = if self.twist == Twist::OtherNonce {
heimdall_core::Nonce::random()
} else {
job.nonce
};
let result = JobResult {
job_id: job.id,
nonce,
verdict,
outputs,
exit,
started_unix: 1_800_000_000,
finished_unix: 1_800_000_001,
runner: heimdall_features(),
events: Vec::new(),
kernel: Some(kernel()),
};
let key = if self.twist == Twist::ForeignKey {
BootKey::from_seed([3; 32], BootId([3; 16]))
} else {
BootKey::from_seed([7; 32], BootId([7; 16]))
};
key.sign(result).map_err(RunError::Sign)
}
fn verifying_key(&self) -> VerifyingKey {
self.key.verifying_key()
}
fn features(&self) -> Features {
heimdall_features()
}
}
fn adapter(prog: Arc<Program>, twist: Twist) -> (HeimdallJobs, Arc<Mutex<Saw>>) {
let key = BootKey::from_seed([7; 32], BootId([7; 16]));
let pinned = key.pinned();
let saw = Arc::new(Mutex::new(Saw::default()));
let fake = FakeHeimdall {
key,
prog,
twist,
saw: saw.clone(),
};
let image = ImageRef {
name: crate::WORKER.into(),
digest: DIGEST.into(),
};
(
HeimdallJobs::over(
Box::new(fake),
pinned,
Some(kernel()),
image,
WORKER_BIN,
ResultKey::from_seed([9; 32]),
)
.unwrap(),
saw,
)
}
/// The copy program: `in/a` → `out/a`, and `m/b` → `out/b` when there.
fn copy() -> Arc<Program> {
Arc::new(|_w: &WireJob, d: &Dirs| {
std::fs::copy(d.input.join("a"), d.out.join("a")).unwrap();
let mut outs = vec!["a".to_string()];
if d.input.join("m/b").exists() {
std::fs::copy(d.input.join("m/b"), d.out.join("b")).unwrap();
outs.push("b".into());
}
if let Some(r) = d.read.first() {
std::fs::copy(r.join("rule"), d.out.join("rule")).unwrap();
outs.push("rule".into());
}
Answer::accepted(outs)
})
}
struct Dirs2 {
root: tempfile::TempDir,
input: PathBuf,
out: PathBuf,
}
fn dirs() -> Dirs2 {
let root = tempfile::tempdir().unwrap();
let input = root.path().join("in");
let out = root.path().join("out");
std::fs::create_dir_all(input.join("m")).unwrap();
std::fs::create_dir(&out).unwrap();
std::fs::write(input.join("a"), b"alpha").unwrap();
std::fs::write(input.join("m/b"), b"beta").unwrap();
Dirs2 { root, input, out }
}
fn job(d: &Dirs2, policy: Policy) -> Job {
Job::new(
worker(&["scan"]),
d.input.clone(),
d.out.clone(),
serde_json::json!({"x": 1}),
policy,
)
}
fn names(dir: &Path) -> Vec<String> {
let mut v: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
v.sort();
v
}
#[test]
fn a_job_runs_as_heimdalls_contract_says_and_its_answer_is_believed_only_after_heimdalls_signature()
{
let (h, saw) = adapter(copy(), Twist::None);
let d = dirs();
let rules = tempfile::tempdir().unwrap();
std::fs::write(rules.path().join("rule"), b"rule x { condition: true }").unwrap();
let limits = heimdall_core::Limits {
wall: std::time::Duration::from_secs(9),
memory_bytes: 300 << 20,
cpus: 0.5,
pids: 32,
..Default::default()
};
let j = job(
&d,
Policy {
read: vec![rules.path().to_path_buf()],
limits,
..Default::default()
},
);
let t = j.ticket();
let s = h.run(j).unwrap();
// The caller believes it with the ADAPTER's pinned key, as every Holger runner.
let r = accept(&s, &h.verifying_key(), &t, &d.out).unwrap();
assert_eq!(r.verdict, Verdict::Accepted);
assert_eq!(
r.outputs,
vec![
Output::of("a", b"alpha"),
Output::of("b", b"beta"),
Output::of("rule", b"rule x { condition: true }"),
]
);
assert_eq!(
names(&d.out),
["a", "b", "rule"],
"no answer file is handed over"
);
// Heimdall's own words travel unchanged: its per-job features and its kernel.
assert_eq!(r.runner, heimdall_features());
assert_eq!(r.kernel, Some(kernel()));
assert_eq!(
(r.started_unix, r.finished_unix),
(1_800_000_000, 1_800_000_001)
);
assert_eq!(h.features().runner, "heimdall");
assert_eq!(h.kernel(), Some(&kernel()));
// What Heimdall was given: the pinned image, argv exactly the worker, the job's limits,
// and the staged input: job.json (the parameters inside), i/ and r0/.
let saw = saw.lock().unwrap().clone();
assert_eq!(saw.image.unwrap().digest, DIGEST);
assert_eq!(saw.argv, [WORKER_BIN]);
assert_eq!(saw.limits.unwrap(), limits);
assert_eq!(
saw.input_tree,
["i/", "i/a", "i/m/", "i/m/b", "job.json", "r0/", "r0/rule"]
);
let w = saw.wire.unwrap();
assert_eq!(w.argv, ["scan"]);
assert_eq!(w.params, serde_json::json!({"x": 1}));
assert_eq!(w.reads, 1);
assert_eq!(w.answer.as_deref(), Some(ANSWER_FILE));
// Nothing of the staging is left.
assert_eq!(names(d.root.path()), ["in", "out"]);
}
#[test]
fn heimdalls_refusals_are_holgers_frozen_codes_with_heimdalls_code_first() {
let killed = |k| ExitInfo {
killed: Some(k),
..Default::default()
};
for (code, exit, holger) in [
(
"heimdall-limit-wall",
killed(KillReason::Wall),
&holger_errcode::SANDBOX_TIMEOUT,
),
(
"heimdall-limit-memory",
killed(KillReason::Memory),
&holger_errcode::SANDBOX_KILLED,
),
(
"heimdall-limit-pids",
ExitInfo::exited(0),
&holger_errcode::SANDBOX_KILLED,
),
(
"heimdall-seccomp-1",
ExitInfo::default(),
&holger_errcode::SANDBOX_KILLED,
),
(
"heimdall-exit-2",
ExitInfo::default(),
&holger_errcode::SANDBOX_KILLED,
),
(
"heimdall-exit-1",
ExitInfo::exited(1),
&holger_errcode::SANDBOX_WORKER_FAILED,
),
(
"heimdall-out-1",
ExitInfo::exited(0),
&holger_errcode::SANDBOX_JOB_UNDECLARED,
),
(
"heimdall-out-2",
ExitInfo::exited(0),
&holger_errcode::SANDBOX_JOB_UNDECLARED,
),
] {
assert_eq!(holger_code_of(code), Some(holger));
let (h, _) = adapter(copy(), Twist::Refuse(code, exit.clone()));
let d = dirs();
let j = job(&d, Policy::default());
let t = j.ticket();
let s = h.run(j).unwrap();
let r = accept(&s, &h.verifying_key(), &t, &d.out).unwrap();
let Verdict::Refused { code: c, reason } = &r.verdict else {
panic!("{code}: {:?}", r.verdict)
};
assert_eq!(c, holger.code, "{code}");
assert!(reason.contains(code), "{code}: {reason}");
assert_eq!(r.exit, exit, "{code}");
assert!(
r.outputs.is_empty() && names(&d.out).is_empty(),
"{code}: nothing handed over"
);
}
assert_eq!(holger_code_of("heimdall-something-new"), None);
}
#[test]
fn a_heimdall_result_not_signed_by_the_pinned_key_for_this_job_or_with_other_outputs_is_never_used()
{
for twist in [Twist::ForeignKey, Twist::OtherNonce, Twist::WrongListing] {
let (h, _) = adapter(copy(), twist.clone());
let d = dirs();
let e = h.run(job(&d, Policy::default())).unwrap_err();
assert!(matches!(e, RunError::Setup { .. }), "{twist:?}: {e:?}");
assert!(names(&d.out).is_empty(), "{twist:?}: nothing handed over");
assert_eq!(names(d.root.path()), ["in", "out"], "{twist:?}");
}
}
#[test]
fn no_answer_an_undeclared_output_or_the_workers_own_refusal_hands_over_nothing() {
let code = |r: &JobResult| match &r.verdict {
Verdict::Refused { code, .. } => code.clone(),
v => panic!("{v:?}"),
};
let (h, _) = adapter(copy(), Twist::NoAnswer);
let d = dirs();
let j = job(&d, Policy::default());
let t = j.ticket();
let s = h.run(j).unwrap();
let r = accept(&s, &h.verifying_key(), &t, &d.out).unwrap();
assert_eq!(code(r), holger_errcode::SANDBOX_JOB_ANSWER_INVALID.code);
assert!(names(&d.out).is_empty());
let sneaky: Arc<Program> = Arc::new(|_w: &WireJob, d: &Dirs| {
std::fs::write(d.out.join("a"), b"a").unwrap();
std::fs::write(d.out.join("extra"), b"x").unwrap();
Answer::accepted(vec!["a".into()])
});
let (h, _) = adapter(sneaky, Twist::None);
let d = dirs();
let j = job(&d, Policy::default());
let t = j.ticket();
let s = h.run(j).unwrap();
let r = accept(&s, &h.verifying_key(), &t, &d.out).unwrap();
assert_eq!(code(r), holger_errcode::SANDBOX_JOB_UNDECLARED.code);
assert!(names(&d.out).is_empty());
let no: Arc<Program> = Arc::new(|_w: &WireJob, _d: &Dirs| {
Answer::refused("holger-update-7", "the bundle is not signed")
});
let (h, _) = adapter(no, Twist::None);
let d = dirs();
let j = job(&d, Policy::default());
let t = j.ticket();
let s = h.run(j).unwrap();
let r = accept(&s, &h.verifying_key(), &t, &d.out).unwrap();
assert_eq!(code(r), "holger-update-7");
}
#[test]
fn a_wasm_job_a_network_a_foreign_image_a_writable_path_or_an_unpinned_image_is_refused() {
let (h, saw) = adapter(copy(), Twist::None);
let d = dirs();
let mut w = job(&d, Policy::default());
w.kind = JobKind::wasm(heimdall_core::GuestRef {
path: "/g.wasm".into(),
blake3: "00".into(),
});
assert!(matches!(h.run(w), Err(RunError::Unsupported { .. })));
let mut n = job(&d, Policy::default());
n.policy.net = Some(NetAllow {
peers: vec![heimdall_core::NetPeer::new(
"upstream.test",
"10.0.0.1",
443,
)],
});
assert!(matches!(h.run(n), Err(RunError::Unsupported { .. })));
let mut f = job(&d, Policy::default());
f.kind = JobKind::Native {
image: ImageRef {
name: "docker.io/library/busybox".into(),
digest: DIGEST.into(),
},
argv: vec!["/bin/sh".into()],
};
assert!(matches!(h.run(f), Err(RunError::Unsupported { .. })));
let mut wr = job(&d, Policy::default());
wr.policy.write = vec!["/var/tmp".into()];
assert!(matches!(h.run(wr), Err(RunError::Unsupported { .. })));
assert!(
saw.lock().unwrap().image.is_none(),
"none of them reached Heimdall"
);
let key = BootKey::from_seed([7; 32], BootId([7; 16]));
let unpinned = HeimdallJobs::over(
Box::new(FakeHeimdall {
key: BootKey::from_seed([7; 32], BootId([7; 16])),
prog: copy(),
twist: Twist::None,
saw,
}),
key.pinned(),
None,
ImageRef {
name: crate::WORKER.into(),
digest: String::new(),
},
WORKER_BIN,
ResultKey::from_seed([9; 32]),
);
assert!(matches!(unpinned, Err(RunError::InvalidJob { .. })));
}
/// The worker image minted from the medium's rootfs tar is an OCI layout Heimdall's own
/// image code verifies and unpacks (heimdall-plain `OciLayout`, which HeimdallRunner
/// uses): the same tar gives the same digest, a changed byte another.
#[test]
fn the_worker_image_minted_from_its_rootfs_tar_is_one_heimdall_verifies_and_unpacks() {
let tar = {
let mut t = tar::Builder::new(Vec::new());
let mut h = tar::Header::new_gnu();
h.set_size(5);
h.set_mode(0o555);
t.append_data(&mut h, "holger-scan-worker", &b"#!x\n\n"[..])
.unwrap();
t.into_inner().unwrap()
};
let a = tempfile::tempdir().unwrap();
let b = tempfile::tempdir().unwrap();
let ia = mint_layout(a.path(), &tar, WORKER_ENV).unwrap();
let ib = mint_layout(b.path(), &tar, WORKER_ENV).unwrap();
assert_eq!(ia, ib, "deterministic");
assert!(ia.sha256().is_some());
let root = tempfile::tempdir().unwrap();
let u = heimdall_plain::image::OciLayout::new(a.path())
.unpack(&ia, root.path())
.unwrap();
assert_eq!(u.env, WORKER_ENV);
assert_eq!(u.layers, 1);
assert_eq!(
std::fs::read(root.path().join("holger-scan-worker")).unwrap(),
b"#!x\n\n"
);
let mut other = tar.clone();
other[512] ^= 1;
assert_ne!(mint_layout(b.path(), &other, WORKER_ENV).unwrap(), ia);
}
#[test]
fn the_heimdall_block_refuses_what_it_cannot_mean() {
use crate::heimdall::HeimdallSettings as S;
let ok = S {
image_layout: "/var/lib/holger/oci".into(),
image: Some(DIGEST.into()),
..S::default()
};
assert_eq!(ok.validate(), Ok(()));
let bad = [
S {
image_layout: "oci".into(),
..ok.clone()
},
S {
image: Some("latest".into()),
..ok.clone()
},
S {
image: None,
rootfs_tar: None,
..ok.clone()
},
S {
image: None,
rootfs_tar: Some("w.tar".into()),
..ok.clone()
},
S {
shim: Some("shim".into()),
..ok.clone()
},
S {
worker: "holger-scan-worker".into(),
..ok.clone()
},
];
for b in bad {
assert!(b.validate().is_err(), "{b:?}");
}
let minted = S {
image: None,
rootfs_tar: Some("/data/holger/sandbox/holger-scan-worker.rootfs.tar".into()),
..ok
};
assert_eq!(minted.validate(), Ok(()));
let ron: S = ron::from_str(
r#"(image_layout: "/oci", image: Some("sha256:1111111111111111111111111111111111111111111111111111111111111111"))"#,
)
.unwrap();
assert!(ron.strict && ron.fallback_plain && ron.worker == WORKER_BIN);
}
/// Lane HOL-HEIMDALL-03 with Heimdall 0.2.1: as root under `require_rootless`, a job's root
/// maps to the host id range at crun's base (100000, 65536 ids), never to host root; as an
/// ordinary user, or without the rule, it maps to the server's own uid (no range). Heimdall is
/// no longer refused for running as root: a missing shim is what stops this one.
#[test]
fn as_root_with_require_rootless_a_jobs_root_maps_to_the_host_id_range() {
use crate::heimdall::{host_ids_for, start_as, HeimdallSettings, HOST_IDS_COUNT};
assert_eq!(HOST_IDS_COUNT, 65536);
assert_eq!(host_ids_for(true, 0, 100000, 65536), Some((100000, 65536)));
assert_eq!(host_ids_for(true, 1000, 100000, 65536), None);
assert_eq!(host_ids_for(false, 0, 100000, 65536), None);
let dir = tempfile::tempdir().unwrap();
let s = HeimdallSettings {
image_layout: dir.path().join("oci").display().to_string(),
image: Some(DIGEST.into()),
shim: Some(dir.path().join("no-shim").display().to_string()),
plain_shim: Some(dir.path().join("no-shim").display().to_string()),
..Default::default()
};
assert_eq!(s.host_id_count, HOST_IDS_COUNT);
let e =
start_as(&s, dir.path(), host_ids_for(true, 0, 100000, 65536)).expect_err("no shim here");
assert!(e.contains("no-shim") && !e.contains("host root"), "{e}");
}
/// With a host id range (Heimdall 0.2.1 gives the job's root its input tree), the inputs are
/// staged as copies, so the caller's own files are never handed over through a hard link.
#[test]
fn with_a_host_id_range_the_inputs_are_copies_not_hard_links() {
let (h, saw) = adapter(copy(), Twist::None);
assert_eq!(h.host_ids(), None);
let d = dirs();
h.run(job(&d, Policy::default())).unwrap();
assert_eq!(
saw.lock().unwrap().input_nlink,
2,
"a hard link without a range"
);
let (h, saw) = adapter(copy(), Twist::None);
let h = h.with_host_ids(Some((100000, 65536)));
assert_eq!(h.host_ids(), Some((100000, 65536)));
let d = dirs();
let j = job(&d, Policy::default());
let t = j.ticket();
let s = h.run(j).unwrap();
assert_eq!(saw.lock().unwrap().input_nlink, 1, "a copy with a range");
assert!(accept(&s, &h.verifying_key(), &t, &d.out).is_ok());
}
/// ★ **A fetch adapter hands Heimdall its job's declared peers** (lane cds4, HOL-CDS-4): the fetching
/// medium's worker runs as `HeimdallJobs::with_network`, and only its `fetch` program gets the
/// job's `policy.net` (the address Heimdall's broker connects); any other program, and every
/// job of a scan adapter, is still refused a network before it reaches Heimdall.
#[test]
fn only_a_fetch_adapter_hands_heimdall_a_fetch_jobs_declared_peers() {
let peer = NetAllow {
peers: vec![
heimdall_core::NetPeer::new("pypi.org", "151.101.0.223", 443),
heimdall_core::NetPeer::new("files.pythonhosted.org", "151.101.64.223", 443),
],
};
let (h, saw) = adapter(copy(), Twist::None);
let h = h.with_network();
let d = dirs();
let mut f = job(&d, Policy::default());
f.kind = worker(&["fetch"]);
f.policy.net = Some(peer.clone());
h.run(f).expect("a fetch job runs with its peer");
assert_eq!(saw.lock().unwrap().net, Some(peer.clone()));
let d = dirs();
let mut s = job(&d, Policy::default());
s.policy.net = Some(peer.clone());
assert!(
matches!(h.run(s), Err(RunError::Unsupported { .. })),
"a scan job gets no network, even from a fetch adapter"
);
let (plain, _) = adapter(copy(), Twist::None);
let d = dirs();
let mut f = job(&d, Policy::default());
f.kind = worker(&["fetch"]);
f.policy.net = Some(peer);
assert!(
matches!(plain.run(f), Err(RunError::Unsupported { .. })),
"a scan adapter gives no job a network"
);
}
/// ★ **A fetch job never runs under the scan worker's generated profile** (hol312: prof + cds4):
/// the generated seccomp profile is the scan worker's own (lane prof); a fetch job runs the fetch
/// worker, a different program, so its job keeps Heimdall's fixed profile even when the adapter
/// carries the scan worker's; a scan job of the same adapter still gets it.
#[test]
fn a_fetch_job_does_not_take_the_scan_workers_generated_profile() {
let profile = heimdall_core::SeccompProfileRef {
source: heimdall_core::ProfileSource::OciFile("/p/holger-scan-worker.json".into()),
sha256: format!("sha256:{}", "a".repeat(64)),
fallback_to_default: false,
};
let (h, saw) = adapter(copy(), Twist::None);
let h = h.with_network().with_seccomp(Some(profile.clone()));
let d = dirs();
let mut f = job(&d, Policy::default());
f.kind = worker(&["fetch"]);
f.policy.net = Some(NetAllow {
peers: vec![heimdall_core::NetPeer::new(
"pypi.org",
"151.101.0.223",
443,
)],
});
h.run(f).expect("the fetch job runs");
assert_eq!(
saw.lock().unwrap().seccomp,
None,
"the fetch job: Heimdall's fixed profile"
);
let d = dirs();
h.run(job(&d, Policy::default()))
.expect("the scan job runs");
assert_eq!(
saw.lock().unwrap().seccomp,
Some(profile),
"a scan job: the generated profile"
);
}