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

nordisk/holger

server/lib/tests/the_export_carries_only_what_decides.rs

//! ★ **The export carries only what decides** (lane HOL-NCSC, NCSC P2).
//!
//! What crosses from the online side to the airgap side (`holger export`,
//! `BundleService.Export`, the diode push) carries the bytes and their digests, the signed scan
//! evidence, the SBOM, the approvals, the provenance and the bundle's sequence and expiry, and
//! nothing operational: no importer or operator name beyond the approvers, no free-text reason,
//! no timing, no internal repository name, no source address or path.
//!
//! * every field of every exported structure is classified in `export_profile::PROFILE`; a
//!   field added to one of them without a class fails here (the struct is read from its
//!   source, so nothing escapes by being optional);
//! * `export_profile::decision_entry` makes an intake record line the decision line.
#![cfg(feature = "intake")]

use holger_server_lib::export_profile::{decision_entry, Class, PROFILE};
use holger_server_lib::promote::intake::{
    ArtifactState, Evidence, EvidenceKind, EvidenceVerdict, IntakeEntry,
};

fn root() -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
}

/// Pure: the `pub` field names of `pub struct <name> {` in `text`.
fn fields_of(text: &str, name: &str) -> Vec<String> {
    let start = text
        .find(&format!("pub struct {name} {{"))
        .unwrap_or_else(|| panic!("no `pub struct {name}`"));
    let body = &text[start..];
    let end = body
        .find("\n}")
        .or_else(|| body.find("\n    }"))
        .expect("the struct ends");
    body[..end]
        .lines()
        .skip(1)
        .filter_map(|l| l.trim().strip_prefix("pub "))
        .filter_map(|l| l.split_once(':').map(|(f, _)| f.trim().to_string()))
        .filter(|f| {
            f.chars()
                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
        })
        .collect()
}

/// The structures whose fields cross the boundary, by source file.
const EXPORTED: &[(&str, &str)] = &[
    ("server/lib/src/promote/airgap.rs", "ExportIndex"),
    ("server/lib/src/promote/airgap.rs", "ExportedArtifact"),
    ("server/lib/src/promote/intake.rs", "IntakeEntry"),
    ("server/lib/src/promote/intake.rs", "Evidence"),
    ("update-bundle/src/core.rs", "BundleCore"),
    ("update-bundle/src/core.rs", "SourceProvenance"),
    ("update-bundle/src/core.rs", "PolicyResult"),
    ("update-bundle/src/core.rs", "PayloadEntry"),
    ("update-bundle/src/core.rs", "EvidenceEntry"),
    ("server/lib/src/scanner/mod.rs", "Evidence"),
    ("server/lib/src/scanner/mod.rs", "EvidenceSubject"),
    ("server/lib/src/scanner/mod.rs", "ScanEvidence"),
    ("sync/src/lib.rs", "SyncManifest"),
    ("sync/src/lib.rs", "Invalidation"),
    ("sync/src/lib.rs", "PackageRef"),
];

#[test]
fn every_field_that_crosses_the_boundary_is_classified() {
    let mut missing = Vec::new();
    for (file, name) in EXPORTED {
        let text = std::fs::read_to_string(root().join(file)).unwrap();
        let fields = fields_of(&text, name);
        assert!(!fields.is_empty(), "{file}: {name} has no fields?");
        let Some((_, _, classes)) = PROFILE.iter().find(|(f, s, _)| f == file && s == name) else {
            missing.push(format!("{file}: {name} (the whole structure)"));
            continue;
        };
        for f in &fields {
            if !classes.iter().any(|(c, _)| c == f) {
                missing.push(format!("{file}: {name}.{f}"));
            }
        }
        for (c, _) in classes.iter() {
            assert!(
                fields.contains(&c.to_string()),
                "{file}: {name} has no field `{c}` (the profile names one that is gone)"
            );
        }
    }
    assert!(
        missing.is_empty(),
        "fields that cross to the airgap side with no class in export_profile::PROFILE:\n{}",
        missing.join("\n")
    );
    // What may never cross, said by its class.
    let class = |file: &str, s: &str, f: &str| {
        PROFILE
            .iter()
            .find(|(x, y, _)| *x == file && *y == s)
            .and_then(|(_, _, c)| c.iter().find(|(n, _)| *n == f))
            .map(|(_, c)| *c)
    };
    for (file, s, f) in [
        (
            "server/lib/src/promote/airgap.rs",
            "ExportIndex",
            "exported_by",
        ),
        (
            "server/lib/src/promote/airgap.rs",
            "ExportIndex",
            "exported_at",
        ),
        (
            "server/lib/src/promote/airgap.rs",
            "ExportIndex",
            "production",
        ),
        ("server/lib/src/promote/intake.rs", "IntakeEntry", "reason"),
        ("server/lib/src/promote/intake.rs", "IntakeEntry", "policy"),
        ("server/lib/src/promote/intake.rs", "IntakeEntry", "at"),
        ("server/lib/src/promote/intake.rs", "IntakeEntry", "repo"),
        ("server/lib/src/promote/intake.rs", "IntakeEntry", "origin"),
        (
            "update-bundle/src/core.rs",
            "SourceProvenance",
            "source_repository",
        ),
        (
            "update-bundle/src/core.rs",
            "SourceProvenance",
            "fetched_at",
        ),
        ("sync/src/lib.rs", "Invalidation", "issued_by"),
    ] {
        assert_eq!(
            class(file, s, f),
            Some(Class::Stripped),
            "{s}.{f} never crosses"
        );
    }
}

#[test]
fn an_intake_line_crosses_as_its_decision() {
    let full = |to: ArtifactState, actor: &str| IntakeEntry {
        from: ArtifactState::PolicyChecked,
        to,
        artifact: "demo_pkg@1.0.0".into(),
        sha256: "ab".repeat(32),
        repo: "approved".into(),
        actor: actor.into(),
        at: 1_793_000_000,
        evidence: vec![
            Evidence::new(EvidenceKind::Approval, "bob", EvidenceVerdict::Pass)
                .with_reference(format!("sha256:{}", "cd".repeat(32))),
            Evidence::new(EvidenceKind::Scan, "ci-runner-7.lan", EvidenceVerdict::Pass)
                .with_reference("looked fine to me, ping ida on #security"),
        ],
        policy: "the floor plus team gates passed".into(),
        reason: "approved after the call with the vendor".into(),
        origin: "team".into(),
        effective_policy: None,
    };
    let d = decision_entry(&full(ArtifactState::Approved, "bob"));
    assert_eq!(
        (d.from, d.to, d.artifact.as_str(), d.sha256.as_str()),
        (
            ArtifactState::PolicyChecked,
            ArtifactState::Approved,
            "demo_pkg@1.0.0",
            "ab".repeat(32).as_str()
        )
    );
    assert_eq!(
        d.actor, "bob",
        "the approver of the APPROVED line is the policy's"
    );
    assert_eq!(
        (
            d.repo.as_str(),
            d.at,
            d.policy.as_str(),
            d.reason.as_str(),
            d.origin.as_str()
        ),
        ("", 0, "", "", ""),
        "{d:?}"
    );
    assert!(d.effective_policy.is_none());
    assert_eq!(
        d.evidence[0].reference,
        format!("sha256:{}", "cd".repeat(32)),
        "a digest reference is kept"
    );
    assert_eq!(
        (d.evidence[0].source.as_str(), d.evidence[0].verdict),
        ("bob", EvidenceVerdict::Pass)
    );
    let free = &d.evidence[1].reference;
    assert!(
        free.starts_with("sha256:") && free.len() == 71 && !free.contains("ida"),
        "free text crosses as its digest only: {free}"
    );
    let ingest = decision_entry(&full(ArtifactState::Ingested, "ida"));
    assert_eq!(ingest.actor, "online", "the importer's name stays online");
    let line = serde_json::to_string(&ingest).unwrap();
    for word in [
        "ida",
        "the call with the vendor",
        "team gates",
        "\"approved\"",
        "1793000000",
    ] {
        assert!(!line.contains(word), "{word:?} crossed: {line}");
    }
}