server/cli/tests/bundle_import_audit.rs
//! **B3: a bundle import that leaves no audit record does not happen.**
//!
//! Before the fix `run_import` wrote the bundle into the served store, advanced
//! the anti-rollback floor and wrote its receipt, and only THEN tried the audit
//! record, which on failure printed a warning. An audit sink that could not be
//! opened was silently replaced by a no-op, so the import went through with no
//! trace at all. The MVP doc promises fail-closed.
//!
//! W4: the import no longer writes into a raw repository's data directory; it
//! lands the bundle AVAILABLE through the intake chain, so the downstream here
//! declares one.
//!
//! These run the REAL binary against a real signed bundle
//! (`bench_scenarios::secure_update::finalized_bundle_expiring_at`).
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use chrono::{Duration, Utc};
const BIN: &str = env!("CARGO_BIN_EXE_holger");
struct Downstream {
_dir: tempfile::TempDir,
config: PathBuf,
bundles: PathBuf,
root: PathBuf,
audit: PathBuf,
}
/// A downstream store whose audit block is `audit_block` (`{}` is the audit dir).
fn downstream(audit_block: &str, audit_is_a_file: bool) -> Downstream {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("store");
let bundles = root.join("bundles");
let audit = root.join("audit");
std::fs::create_dir_all(&bundles).unwrap();
if audit_is_a_file {
// An audit "directory" that cannot be opened as one: the sink fails to
// initialise, exactly as a full, missing or read-only volume would.
std::fs::write(&audit, b"not a directory").unwrap();
} else {
std::fs::create_dir_all(&audit).unwrap();
}
let config = dir.path().join("holger.ron");
let audit_line = audit_block.replace("{}", &audit.display().to_string());
std::fs::write(&config, intake_ron(&root, &bundles, &audit_line)).unwrap();
Downstream {
_dir: dir,
config,
bundles,
root,
audit,
}
}
fn import(d: &Downstream) -> Output {
import_with(d, &d.config, &[])
}
/// `bundle import` of a fresh signed bundle against `config`, with `extra` args.
fn import_with(d: &Downstream, config: &Path, extra: &[&str]) -> Output {
let out = d.root.parent().unwrap().join("candidate.holger.znippy");
let policy = bench_scenarios::secure_update::finalized_bundle_expiring_at(
7,
"bundle-7",
Utc::now() + Duration::days(30),
&out,
)
.expect("a signed bundle");
let policy_path = d.root.parent().unwrap().join("trust-policy.ron");
std::fs::write(&policy_path, policy.to_ron().unwrap()).unwrap();
allow_first_import(BIN, config, &out, Some(&policy_path));
Command::new(BIN)
.args(["bundle", "import", "--config"])
.arg(config)
.args(extra)
.arg("--bundle")
.arg(&out)
.arg("--policy")
.arg(&policy_path)
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY")
.output()
.expect("run holger-server")
}
/// The downstream's config: a raw repository `bundles` (where the REMOVED
/// raw-directory import used to write, kept to prove nothing lands there any
/// more), the four intake repositories every bundle now lands through, and the
/// audit block `audit_line`.
fn intake_ron(root: &Path, bundles: &Path, audit_line: &str) -> String {
let repo = |name: &str| {
format!(
r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "e")),
ron_data_dir: Some("{}/{name}")),"#,
root.display()
)
};
format!(
r#"(
exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{root}") ],
repositories: [
( ron_name: "bundles", ron_repo_type: "raw", ron_data_dir: Some("{bundles}"), ron_upstreams: [], ron_in: None, ron_out: None ),
{incoming} {quarantine} {approved} {production}
],
audit: {audit_line},
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"),
(name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"),
(name: "production", repo: "production"),
],
intake: Some((rules: (sandbox: off), incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production")),
)),
)
"#,
root = root.display(),
bundles = bundles.display(),
incoming = repo("incoming"),
quarantine = repo("quarantine"),
approved = repo("approved"),
production = repo("production"),
)
}
fn text(o: &Output) -> String {
format!(
"{}\n{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
)
}
fn walk(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
} else {
out.push(p);
}
}
}
out
}
/// No bundle in the served store, no receipt, no anti-rollback floor.
fn nothing_written(d: &Downstream) {
let files = walk(&d.bundles);
assert!(
files.is_empty(),
"a refused import wrote into the served store: {files:?}"
);
let landed = walk(&d.root.join("holger-prod-store").join("lineage"));
assert!(
landed.is_empty(),
"a refused import landed something through intake: {landed:?}"
);
for marker in ["holger-import-receipts", "holger-update-state"] {
let left = walk(&d.root.join(marker));
assert!(left.is_empty(), "a refused import left {marker}: {left:?}");
}
}
#[test]
fn an_import_whose_audit_sink_cannot_open_is_refused_and_writes_nothing() {
let d = downstream("( dir: \"{}\", enabled: true )", true);
let out = import(&d);
let t = text(&out);
assert!(
!out.status.success(),
"an import with a broken audit sink went through:\n{t}"
);
assert!(
t.to_lowercase().contains("audit"),
"the refusal must say it is the audit record:\n{t}"
);
nothing_written(&d);
}
#[test]
fn an_import_with_audit_switched_off_is_refused_and_writes_nothing() {
let d = downstream("( dir: \"{}\", enabled: false )", false);
let out = import(&d);
let t = text(&out);
assert!(
!out.status.success(),
"an import that records nothing went through:\n{t}"
);
assert!(t.to_lowercase().contains("audit"), "{t}");
nothing_written(&d);
}
#[test]
fn an_import_with_a_working_audit_sink_imports_and_is_recorded() {
let d = downstream("( dir: \"{}\", enabled: true )", false);
let out = import(&d);
let t = text(&out);
assert!(out.status.success(), "{t}");
assert!(t.contains("IMPORTED (new)"), "{t}");
// W4: the bundle lands AVAILABLE through the intake chain, not in the raw store.
assert!(t.contains("LANDED "), "{t}");
assert!(
t.contains("AVAILABLE in 'production'"),
"the bundle lands in production through intake: {t}"
);
assert!(
walk(&d.bundles).is_empty(),
"nothing is written into the raw repository's data directory"
);
let recorded: String = walk(&d.audit)
.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect();
assert!(
recorded.contains("bundle-import") && recorded.contains("bundle_id=bundle-7"),
"the import must leave its audit record: {recorded:?}"
);
// S8: the airgap side's trail — each payload hash-verified, then imported,
// keyed by its sha256, in an intact chain.
assert!(
recorded.contains("\"trail\":\"airgap_imported\"")
&& recorded.contains("\"trail\":\"hash_verified\""),
"the import writes the airgap trail: {recorded:?}"
);
let rep = server_lib::audit::chain::verify_dir(&d.audit, &[]).unwrap();
assert!(rep.is_intact(), "{}", rep.summary());
}
/// ★ HOL-NCSC (NCSC P1 item 5): **a refused import leaves its audit row**, with its code: a
/// floorless bundle with no admin's grant (`holger-import-9`) and a file that is not a bundle.
#[test]
fn a_refused_import_leaves_its_audit_row() {
let d = downstream("( dir: \"{}\", enabled: true )", false);
let out = d.root.parent().unwrap().join("floorless.holger.znippy");
let policy = bench_scenarios::secure_update::finalized_bundle_expiring_at(
7,
"bundle-7",
Utc::now() + Duration::days(30),
&out,
)
.expect("a signed bundle");
let policy_path = d.root.parent().unwrap().join("trust-policy.ron");
std::fs::write(&policy_path, policy.to_ron().unwrap()).unwrap();
let junk = d.root.parent().unwrap().join("junk.holger.znippy");
std::fs::write(&junk, b"not a bundle").unwrap();
for (bundle, code) in [(&out, "[holger-import-9]"), (&junk, "[holger-")] {
let o = Command::new(BIN)
.args(["bundle", "import", "--config"])
.arg(&d.config)
.arg("--bundle")
.arg(bundle)
.arg("--policy")
.arg(&policy_path)
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY")
.output()
.expect("run holger-server");
assert!(!o.status.success(), "{}", text(&o));
let recorded: String = walk(&d.audit)
.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect();
let name = bundle.file_name().unwrap().to_string_lossy().into_owned();
let row = recorded
.lines()
.find(|l| l.contains("bundle import refused") && l.contains(&name))
.unwrap_or_else(|| panic!("no audit row for the refused {name}: {recorded}"));
assert!(row.contains(code), "{row}");
}
nothing_written(&d);
}
/// **The removed bypass** (lane W4): `--repo <raw>` used to write the bundle
/// straight into that raw repository's data directory, past every intake state.
/// Naming it is refused by name, and nothing is written anywhere.
#[test]
fn the_old_raw_directory_import_is_refused_by_name() {
let d = downstream("( dir: \"{}\", enabled: true )", false);
let out = import_with(&d, &d.config, &["--repo", "bundles"]);
let t = text(&out);
assert!(
!out.status.success(),
"the raw-directory import went through:\n{t}"
);
assert!(t.contains("[holger-import-1]"), "refused by name:\n{t}");
assert!(t.contains("bypassed the intake chain"), "{t}");
nothing_written(&d);
}
/// A server with no intake chain has nowhere to land a bundle, so the import is
/// refused by name instead of falling back to a raw directory.
#[test]
fn a_bundle_import_without_an_intake_chain_is_refused_by_name() {
let d = downstream("( dir: \"{}\", enabled: true )", false);
let bare = d.root.parent().unwrap().join("no-intake.ron");
std::fs::write(
&bare,
format!(
r#"(
exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
storage_endpoints: [],
repositories: [
( ron_name: "bundles", ron_repo_type: "raw", ron_data_dir: Some("{}"), ron_upstreams: [], ron_in: None, ron_out: None ),
],
audit: ( dir: "{}", enabled: true ),
)
"#,
d.bundles.display(),
d.audit.display()
),
)
.unwrap();
let out = import_with(&d, &bare, &[]);
let t = text(&out);
assert!(!out.status.success(), "{t}");
assert!(t.contains("[holger-import-2]"), "refused by name:\n{t}");
nothing_written(&d);
}
/// The admin's first-import authorization on the secure side (lane HOL-CDS, NCSC gap 2), for
/// the vendor, product and sequence of `bundle` (verified under `policy`). Its refusal (a
/// floor that already stands) is the import's business, so it is not asserted here.
fn allow_first_import(bin: &str, config: &Path, bundle: &Path, policy: Option<&Path>) {
let mut c = Command::new(bin);
c.args(["bundle", "allow-first-import", "--config"])
.arg(config)
.arg("--from-bundle")
.arg(bundle);
if let Some(p) = policy {
c.arg("--policy").arg(p);
}
let _ = c
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY")
.output();
}