server/lib/src/export_profile.rs
//! ★ **The export's decision profile** (lane HOL-NCSC, NCSC P2): what crosses from the online
//! side to the airgap side.
//!
//! An export (`holger export`, `BundleService.Export`, the diode push of `export:`, all through
//! [`crate::export_door::ExportDoor`]) carries what the secure side decides by: the bytes and
//! their digests, the signed scan evidence, the SBOM, the approvals, the provenance material,
//! the bundle's sequence and validity window, and the sync manifest's packages and
//! invalidations. It never carries what only the online box knows about itself: the
//! importer's or an operator's name (the approvers are the policy's, and cross), free-text
//! reasons and policy words, timings, its repository names, source names, addresses or paths.
//!
//! [`PROFILE`] classifies every field of every structure that crosses; the guard
//! `server/lib/tests/the_export_carries_only_what_decides.rs` reads the structures from their
//! source and fails on a field with no class. There is one profile and no switch: nothing on
//! the secure side needs the full record, and a switch would be a way to send it.
use crate::promote::intake::{ArtifactState, Evidence, IntakeEntry};
/// How a field of an exported structure crosses the boundary.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
/// Carried as it is: the secure side decides by it.
Kept,
/// Carried only on the transition that needs it: the approver on the APPROVED line.
KeptOnApproval,
/// Carried inside a signature made on the online side (the scan evidence, the sync
/// manifest): what a scanner said, verified on the secure side; it holds no operator text.
Signed,
/// Carried as its SHA-256 only: free text whose binding matters, never its words. A
/// value that already is a digest reference (`sha256:`, `blake3:`, `scan-evidence `)
/// is kept.
Digested,
/// Never carried (left empty, zero or absent).
Stripped,
}
use Class::*;
/// One exported structure: `(file, struct, [(field, class)])`.
pub type Structure = (&'static str, &'static str, &'static [(&'static str, Class)]);
/// Every exported structure, every field.
pub const PROFILE: &[Structure] = &[
(
"server/lib/src/promote/airgap.rs",
"ExportIndex",
&[
("schema", Kept),
("exported_at", Stripped),
("exported_by", Stripped),
("production", Stripped),
("verdict", Kept),
("computed_from", Kept),
("artifacts", Kept),
],
),
(
"server/lib/src/promote/airgap.rs",
"ExportedArtifact",
&[
("namespace", Kept),
("name", Kept),
("version", Kept),
("payload", Kept),
("sha256", Kept),
("blake3", Kept),
("size", Kept),
("verdict", Kept),
// Computed from the evidence by the fold, never typed.
("reasons", Kept),
("ran", Kept),
("not_run", Kept),
("evidence", Kept),
("attestations", Kept),
("signer_cert", Kept),
("sbom", Kept),
("intake_record", Kept),
("approvers", Kept),
("provenance", Kept),
// What the package says about itself for its index (lane HOL-PERF-2): read
// out of the bytes by the online side's scan worker, no operator text.
("index_row", Kept),
],
),
(
"server/lib/src/promote/intake.rs",
"IntakeEntry",
&[
("from", Kept),
("to", Kept),
("artifact", Kept),
("sha256", Kept),
("repo", Stripped),
("actor", KeptOnApproval),
("at", Stripped),
("evidence", Kept),
("policy", Stripped),
("reason", Stripped),
("origin", Stripped),
("effective_policy", Stripped),
],
),
(
"server/lib/src/promote/intake.rs",
"Evidence",
&[
("kind", Kept),
("source", Kept),
("verdict", Kept),
("reference", Digested),
],
),
(
"update-bundle/src/core.rs",
"BundleCore",
&[
("schema_version", Kept),
("bundle_id", Kept),
("sequence", Kept),
// The validity window the secure side holds the bundle to.
("created_at", Kept),
("expires_at", Kept),
// The program and its version (`holger <v> BundleService.Export`).
("producer", Kept),
("supplier", Kept),
("product", Kept),
("release_version", Kept),
("supported_hardware_models", Kept),
("supported_hardware_revisions", Kept),
("source_provenance", Kept),
("payloads", Kept),
("evidence", Kept),
("policy_result", Kept),
("supersedes", Kept),
("withdraws", Kept),
],
),
(
"update-bundle/src/core.rs",
"SourceProvenance",
&[
("source_kind", Kept),
("source_uri", Stripped),
("source_repository", Stripped),
("source_asset_path", Stripped),
("fetched_at", Stripped),
("reported_size", Stripped),
("vendor_checksum", Stripped),
("vendor_signature_status", Kept),
("tls_origin", Stripped),
("transfer_receipt_digest", Stripped),
],
),
(
"update-bundle/src/core.rs",
"PolicyResult",
&[
("policy_digest", Kept),
("advisory_snapshot_digest", Kept),
("verdict", Kept),
("non_production", Kept),
],
),
(
"update-bundle/src/core.rs",
"PayloadEntry",
&[
("relative_path", Kept),
("media_type", Kept),
("size_bytes", Kept),
("sha256", Kept),
("blake3", Kept),
],
),
(
"update-bundle/src/core.rs",
"EvidenceEntry",
&[
("relative_path", Kept),
("media_type", Kept),
("size_bytes", Kept),
("sha256", Kept),
("blake3", Kept),
("evidence_kind", Kept),
],
),
(
"server/lib/src/scanner/mod.rs",
"Evidence",
&[
("schema", Signed),
("subject", Signed),
("verdict", Signed),
("reasons", Signed),
("records", Signed),
("scanned_at", Signed),
],
),
(
"server/lib/src/scanner/mod.rs",
"EvidenceSubject",
&[
("file_name", Signed),
("ecosystem", Signed),
("name", Signed),
("version", Signed),
("size", Signed),
("inputs", Signed),
],
),
(
"server/lib/src/scanner/mod.rs",
"ScanEvidence",
&[
("scanner_id", Signed),
("scanner_version", Signed),
("kind", Signed),
("verdict", Signed),
("findings", Signed),
("inputs", Signed),
("document", Signed),
("detail", Signed),
("required", Signed),
("intel_sequence", Signed),
("hold", Signed),
],
),
(
"sync/src/lib.rs",
"SyncManifest",
&[
("schema", Signed),
("upstream", Signed),
("generation", Signed),
("issued_at", Signed),
("carrier", Signed),
("added", Signed),
("invalidations", Signed),
],
),
(
"sync/src/lib.rs",
"Invalidation",
&[
("id", Signed),
("coordinate", Signed),
("sha256", Signed),
("reason", Signed),
// One sentence a downstream shows: the CVE, the scanner hit, the notice.
("detail", Signed),
("generation", Signed),
("issued_at", Signed),
("issued_by", Stripped),
],
),
(
"sync/src/lib.rs",
"PackageRef",
&[("coordinate", Signed), ("sha256", Signed)],
),
];
/// What the importer, the admin and the other non-approving actors are called on the
/// airgap side.
pub const ONLINE_ACTOR: &str = "online";
/// Pure: a reference that is already a digest, kept as it is.
fn is_digest_reference(r: &str) -> bool {
r.starts_with("sha256:") || r.starts_with("blake3:") || r.starts_with("scan-evidence ")
}
/// ★ **The line of an intake record the export carries** ([`PROFILE`]'s `IntakeEntry` and
/// `Evidence` rows): the transition, its coordinate and bytes, its evidence slots (free-text
/// references as their SHA-256), the approver on the APPROVED line; no repository, timing,
/// policy words, reason, source or effective policy, and no other actor's name.
pub fn decision_entry(e: &IntakeEntry) -> IntakeEntry {
IntakeEntry {
from: e.from,
to: e.to,
artifact: e.artifact.clone(),
sha256: e.sha256.clone(),
repo: String::new(),
actor: if e.to == ArtifactState::Approved {
e.actor.clone()
} else {
ONLINE_ACTOR.to_string()
},
at: 0,
evidence: e.evidence.iter().map(decision_evidence).collect(),
policy: String::new(),
reason: String::new(),
origin: String::new(),
effective_policy: None,
}
}
fn decision_evidence(ev: &Evidence) -> Evidence {
let mut out = ev.clone();
if !out.reference.is_empty() && !is_digest_reference(&out.reference) {
out.reference = format!(
"sha256:{}",
update_bundle::digest::sha256_hex(out.reference.as_bytes())
);
}
out
}