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