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

nordisk/holger

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();
}