crates/heimdall-container/tests/genprof.rs
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB
//! Resolving a job's GENERATED seccomp profile (heimdall_container::genprof):
//! by workload name + digest from the checked-in profiles, or from an OCI
//! seccomp JSON file pinned by sha256. Heimdall is a defensive sandbox; these
//! tests run no job and install no filter (plain CI).
use std::path::PathBuf;
use heimdall_container::genprof::{resolve, MAX_PROFILE_BYTES};
use heimdall_core::{ProfileSource, RunError, SeccompProfileRef, Sha256Digest};
use heimdall_seccomp::{Arch, Profile};
fn scratch(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("heim-genprof-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn native_entries() -> Vec<&'static heimdall_profiles::Entry> {
heimdall_profiles::ALL
.iter()
.filter(|e| Some(e.profile.arch) == Arch::native())
.collect()
}
fn reference(source: ProfileSource, bytes: &[u8]) -> SeccompProfileRef {
SeccompProfileRef {
source,
sha256: Sha256Digest::of(bytes).to_prefixed(),
fallback_to_default: false,
}
}
fn refusal(r: Result<impl std::fmt::Debug, RunError>) -> String {
match r {
Err(RunError::InvalidJob { reason }) => reason,
other => panic!("expected a refusal: {other:?}"),
}
}
#[test]
fn every_checked_in_oci_json_reads_back_as_its_compiled_profile() {
for e in heimdall_profiles::ALL {
let back = heimdall_seccomp::oci::from_oci(e.oci_json, e.profile.workload, e.profile.libc)
.unwrap();
assert_eq!(
back,
Profile::from(e.profile),
"{} {} {}",
e.profile.workload,
e.profile.arch,
e.profile.libc
);
}
}
#[test]
fn a_checked_in_profile_is_found_by_workload_and_digest_and_each_libc_by_its_own_digest() {
let mine = native_entries();
assert!(!mine.is_empty());
for e in mine {
let r = reference(
ProfileSource::Workload(e.profile.workload.into()),
e.oci_json.as_bytes(),
);
let p = resolve(&r).unwrap().expect("applied, not a fallback");
assert_eq!(p.digest, r.sha256);
assert_eq!(p.profile, Profile::from(e.profile));
}
}
#[test]
fn an_oci_file_pinned_by_its_sha256_resolves_to_the_same_rules() {
let d = scratch("file");
let e = native_entries()[0];
let path = d.join(e.profile.workload).join("x86_64-glibc.oci.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, e.oci_json).unwrap();
let r = reference(ProfileSource::OciFile(path), e.oci_json.as_bytes());
let p = resolve(&r).unwrap().unwrap();
assert_eq!(p.profile.rules, Profile::from(e.profile).rules);
assert_eq!(p.profile.workload, e.profile.workload);
}
#[test]
fn a_changed_byte_in_the_oci_file_refuses_the_job_naming_the_digest_it_found() {
let d = scratch("tamper");
let e = native_entries()[0];
let path = d.join("p.oci.json");
let r = reference(ProfileSource::OciFile(path.clone()), e.oci_json.as_bytes());
// One more allowed call: the attacker's widening.
let widened = e.oci_json.replacen("\"access\"", "\"socket\"", 1);
assert_ne!(widened, e.oci_json);
std::fs::write(&path, &widened).unwrap();
let why = refusal(resolve(&r));
assert!(why.contains("digest mismatch"), "{why}");
assert!(
why.contains(&Sha256Digest::of(widened.as_bytes()).to_prefixed()),
"{why}"
);
}
#[test]
fn a_missing_file_a_relative_path_a_symlink_or_a_huge_file_refuses_the_job() {
let d = scratch("missing");
let e = native_entries()[0];
let ok = d.join("ok.oci.json");
std::fs::write(&ok, e.oci_json).unwrap();
let mk = |p: PathBuf| reference(ProfileSource::OciFile(p), e.oci_json.as_bytes());
assert!(refusal(resolve(&mk(d.join("nope.json")))).contains("missing"));
assert!(refusal(resolve(&mk("rel/p.json".into()))).contains("not absolute"));
let link = d.join("link.json");
std::os::unix::fs::symlink(&ok, &link).unwrap();
refusal(resolve(&mk(link)));
let huge = d.join("huge.json");
std::fs::write(&huge, vec![b' '; MAX_PROFILE_BYTES as usize + 1]).unwrap();
assert!(refusal(resolve(&mk(huge))).contains("larger"));
}
#[test]
fn a_profile_that_is_not_enforcing_refuses_the_job_even_with_the_right_digest() {
let d = scratch("audit");
let e = native_entries()[0];
let audit = e.oci_json.replace("SCMP_ACT_KILL_PROCESS", "SCMP_ACT_LOG");
let path = d.join("audit.oci.json");
std::fs::write(&path, &audit).unwrap();
let r = reference(ProfileSource::OciFile(path), audit.as_bytes());
refusal(resolve(&r));
}
#[test]
fn with_fallback_a_missing_or_tampered_profile_resolves_to_none_but_a_bad_pin_still_refuses() {
let d = scratch("fallback");
let e = native_entries()[0];
let mut r = reference(
ProfileSource::OciFile(d.join("nope.json")),
e.oci_json.as_bytes(),
);
r.fallback_to_default = true;
assert_eq!(resolve(&r).unwrap(), None);
r.source = ProfileSource::Workload("no-such-workload".into());
assert_eq!(resolve(&r).unwrap(), None);
r.source = ProfileSource::Workload(e.profile.workload.into());
r.sha256 = format!("sha256:{}", "ee".repeat(32));
assert_eq!(resolve(&r).unwrap(), None);
r.sha256 = "sha256:XYZ".into();
refusal(resolve(&r));
}
#[test]
fn a_policy_naming_a_profile_round_trips_through_json_and_old_policies_have_none() {
let e = native_entries()[0];
let p = heimdall_core::Policy {
seccomp: Some(reference(
ProfileSource::Workload(e.profile.workload.into()),
e.oci_json.as_bytes(),
)),
..Default::default()
};
let j = serde_json::to_string(&p).unwrap();
assert!(j.contains("\"workload\""), "{j}");
assert_eq!(
serde_json::from_str::<heimdall_core::Policy>(&j).unwrap(),
p
);
let old = serde_json::to_value(heimdall_core::Policy::default()).unwrap();
assert!(old.get("seccomp").is_none(), "{old}");
assert_eq!(
serde_json::from_value::<heimdall_core::Policy>(old)
.unwrap()
.seccomp,
None
);
}