intel/src/media.rs
//! **The intel a Holger medium ships** (row HOL-INTEL-ON-MEDIA, owner 2026-11-03).
//!
//! A fresh airgap Holger had no CVE data until an admin imported a signed bundle, and an empty
//! CVE slot holds every pull (H32-FAILCLOSED). So the install medium carries a fresh
//! `holger.intel/v1` bundle, built when the medium is composed (`cargo xtask estate-media
//! --intel-release-signer`), dated, and signed by **Holger's release signer alone**: the
//! release key already vouches for every binary on the medium, so the intel it ships needs no
//! approvers of its own. The box imports it at first boot against [`release_policy`], through
//! the same verify (an `import-intel` job), anti-rollback floor and audit-first path as every
//! other intel import. Online and airgap alike: an online box's `intel.refresh` then lands
//! newer sets over it, an airgap box takes newer bundles at `POST /-/intel/import`.
//!
//! [`status`] is the console's one line: where the live data came from ([`Source`]), who signed
//! it, its date and age, and whether it is fresh ([`State`]).
//!
//! Never the installtest's throwaway signers (`IntelFeed::mint`, keys dropped after the run): a
//! medium pinned to a key nobody holds would refuse every later bundle.
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use chrono::{DateTime, Duration, Utc};
use holger_update_bundle::digest::sha256_hex;
use holger_update_bundle::policy::{CertificateValidity, ExpiredPosture, TrustMode, TrustPolicy};
use holger_update_bundle::schema::POLICY_SCHEMA;
use serde::{Deserialize, Serialize};
use crate::bundle::{build_bundle, sequence_for, BuildOptions, BuiltBundle, SignerFiles};
use crate::db::LocalIntelDb;
use crate::set::AdvisorySet;
use crate::{DEFAULT_SUPPLIER, INTEL_PRODUCT};
/// How the console names Holger's release signer.
pub const RELEASE_LABEL: &str = "Holger release";
/// The producer a medium's bundle names.
pub const MEDIA_PRODUCER: &str = "holger estate-media";
/// How long a medium's bundle stays importable: as long as a scan accepts its date
/// (`intel.block_after_days`, 30 by default). A box installed later from an old medium finds
/// it expired, imports nothing and holds every pull until newer intel comes (fail closed).
pub const MEDIA_VALID_DAYS: i64 = 30;
/// **What a box with no CVE data says**, word for word, on the console's line, in the status
/// door's `alert` and as the `ALERT` line of its log (HOL-INTEL-ON-MEDIA): an empty CVE slot
/// holds every pull (fail closed), and nobody should have to guess why.
pub const NO_DATA_ALERT: &str = "no signed CVE data: pulls are held until a bundle is imported";
/// The landing record beside `installed.json`: who put the live set there.
pub const LANDED_FILE: &str = "landed.json";
/// **The trust policy a medium's bundle verifies against**: Holger's release signer (by its
/// certificate's SHA-256), alone, for `holger.intel/v1` only, an expiry required.
pub fn release_policy(release_cert_der: &[u8]) -> TrustPolicy {
TrustPolicy {
schema_version: POLICY_SCHEMA.into(),
trust_mode: Some(TrustMode::PinnedSigners),
trusted_roots: vec![],
certificate_validity: CertificateValidity::Enforce,
approved_signers: vec![],
required_approval_roles: vec![],
minimum_distinct_approvers: 0,
trusted_release_signers: vec![sha256_hex(release_cert_der)],
release_signer_must_be_distinct: true,
maximum_bundle_age_secs: None,
require_expiry: true,
on_expired_metadata: Some(ExpiredPosture::Refuse),
require_sbom: false,
require_vex: false,
require_scan: false,
allowed_products: vec![INTEL_PRODUCT.into()],
allowed_models: vec![],
}
}
/// **Sign `set` into a medium's bundle** at `out`, dated `created_at`, by `release` alone.
pub fn build_media_bundle(
set: &AdvisorySet,
release: &SignerFiles,
created_at: DateTime<Utc>,
out: PathBuf,
) -> Result<BuiltBundle> {
build_bundle(
set,
BuildOptions {
sequence: sequence_for(created_at),
created_at,
expires_at: Some(created_at + Duration::days(MEDIA_VALID_DAYS)),
supplier: DEFAULT_SUPPLIER.into(),
producer: MEDIA_PRODUCER.into(),
policy: release_policy(&release.cert_der),
approvals: vec![],
release: SignerFiles {
cert_der: release.cert_der.clone(),
key_pkcs8: release.key_pkcs8.clone(),
},
out,
},
)
}
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// **The bundle as a medium stages it**: a medium's staged files are text, so the bundle rides
/// as one line of standard base64.
pub fn to_staged_text(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
for c in bytes.chunks(3) {
let n = (u32::from(c[0]) << 16)
| (u32::from(*c.get(1).unwrap_or(&0)) << 8)
| u32::from(*c.get(2).unwrap_or(&0));
for (i, shift) in [18u32, 12, 6, 0].into_iter().enumerate() {
if i <= c.len() {
s.push(B64[((n >> shift) & 63) as usize] as char);
} else {
s.push('=');
}
}
}
s
}
/// **The bundle's bytes from a staged file**: base64 text (whitespace ignored). Anything else is
/// refused by name.
pub fn from_staged(staged: &[u8]) -> Result<Vec<u8>> {
let val = |c: u8| B64.iter().position(|b| *b == c).map(|p| p as u32);
let text: Vec<u8> = staged
.iter()
.copied()
.filter(|c| !c.is_ascii_whitespace())
.collect();
if text.is_empty() || !text.len().is_multiple_of(4) {
bail!(
"the staged intel bundle is not base64 (length {})",
text.len()
);
}
let mut out = Vec::with_capacity(text.len() / 4 * 3);
for (i, q) in text.chunks(4).enumerate() {
let last = i == text.len() / 4 - 1;
let pad = q.iter().rev().take_while(|c| **c == b'=').count();
if pad > 2 || (pad > 0 && !last) {
bail!("the staged intel bundle is not base64 (padding inside it)");
}
let mut n = 0u32;
for &c in &q[..4 - pad] {
let v = val(c).with_context(|| {
format!("the staged intel bundle is not base64 (byte {c:#04x})")
})?;
n = (n << 6) | v;
}
n <<= 6 * pad as u32;
let bytes = [(n >> 16) as u8, (n >> 8) as u8, n as u8];
out.extend_from_slice(&bytes[..3 - pad]);
}
Ok(out)
}
/// Where the live set came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
/// The bundle the install medium shipped, imported at first boot.
Bundled,
/// A signed bundle an operator imported (a door, the inbox, the CLI).
Imported,
/// The box's own `intel.refresh` fetch.
Refreshed,
/// Nothing is live.
None,
}
impl Source {
pub fn as_str(self) -> &'static str {
match self {
Source::Bundled => "bundled",
Source::Imported => "imported",
Source::Refreshed => "refreshed",
Source::None => "none",
}
}
}
/// How fresh the live set is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum State {
/// Dated within the warn limit.
Ok,
/// Older than the warn limit (yellow).
Stale,
/// No data: every pull is held (red).
None,
}
/// **Who put the live set there** ([`LANDED_FILE`]), written by the server after each landing.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Landed {
pub source: Source,
pub bundle_id: String,
}
/// The coverage record beside `installed.json`: which popular-names lists the live set lacks
/// (H32-TYPOSQUAT holds every pull of an ecosystem with none), so the console's line can say it
/// without reading the whole set.
pub const COVERAGE_FILE: &str = "coverage.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Coverage {
bundle_id: String,
missing_popular: Vec<String>,
}
/// **The ecosystems whose popular-names list `set` lacks** ([`crate::supply::SUPPLY_ECOSYSTEMS`]
/// spelling, in that order): the look-alike check fails closed, so every pull of those is held.
pub fn missing_popular(set: &AdvisorySet) -> Vec<String> {
crate::supply::SUPPLY_ECOSYSTEMS
.iter()
.filter(|e| set.supply.popular_for(e).is_none())
.map(|e| (*e).to_string())
.collect()
}
/// Write the landing record of the set that just went live, and its coverage record.
pub fn record_landed(db: &LocalIntelDb, landed: &Landed) -> Result<()> {
let p = db.root().join(LANDED_FILE);
let bytes = serde_json::to_vec_pretty(landed)?;
holger_update_bundle::atomic_write(&p, &bytes)
.map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?;
if let Some((i, set)) = db.load()? {
let c = Coverage {
bundle_id: i.bundle_id,
missing_popular: missing_popular(&set),
};
let p = db.root().join(COVERAGE_FILE);
holger_update_bundle::atomic_write(&p, &serde_json::to_vec_pretty(&c)?)
.map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?;
}
Ok(())
}
fn read_coverage(root: &Path, bundle_id: &str) -> Vec<String> {
std::fs::read(root.join(COVERAGE_FILE))
.ok()
.and_then(|b| serde_json::from_slice::<Coverage>(&b).ok())
.filter(|c| c.bundle_id == bundle_id)
.map(|c| c.missing_popular)
.unwrap_or_default()
}
fn read_landed(root: &Path) -> Option<Landed> {
serde_json::from_slice(&std::fs::read(root.join(LANDED_FILE)).ok()?).ok()
}
/// **The console's CVE-data line**, as data: the console renders it and adds nothing.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IntelStatus {
pub source: Source,
/// The date the advisories are as of (`YYYY-MM-DD`), when any are live.
pub date: Option<String>,
/// Who signed the live set: [`RELEASE_LABEL`], the box's refresh signer, or an estate signer
/// by its fingerprint.
pub signer: String,
pub age_days: Option<i64>,
pub state: State,
/// The days past which the line turns yellow.
pub stale_after_days: u32,
pub bundle_id: Option<String>,
pub sequence: Option<u64>,
/// The line, whole: `CVE data: bundled 2026-11-03 (signed by Holger release) · 0 days old`.
pub line: String,
/// [`NO_DATA_ALERT`] when no CVE data is live (every pull is held), else `None`.
pub alert: Option<String>,
/// The ecosystems whose popular-names list the live set lacks ([`missing_popular`]): every
/// pull of those is held. Empty with no data (the alert says it) or full coverage.
pub missing_popular: Vec<String>,
}
/// **What the live intel is**, for the console's one line. `release_signers` are the release
/// fingerprints the box pins ([`release_policy`]); past `stale_after_days` it is [`State::Stale`].
pub fn status(
db: &LocalIntelDb,
release_signers: &[String],
stale_after_days: u32,
now: DateTime<Utc>,
) -> IntelStatus {
let Some(i) = db.installed().ok().flatten() else {
return IntelStatus {
source: Source::None,
date: None,
signer: String::new(),
age_days: None,
state: State::None,
stale_after_days,
bundle_id: None,
sequence: None,
line: format!("CVE data: none — {NO_DATA_ALERT}"),
alert: Some(NO_DATA_ALERT.to_string()),
missing_popular: Vec::new(),
};
};
let source = match read_landed(db.root()) {
Some(l) if l.bundle_id == i.bundle_id => l.source,
_ => Source::Imported,
};
let fp = &i.release_signer_fingerprint;
let signer = if release_signers.iter().any(|r| r.eq_ignore_ascii_case(fp)) {
RELEASE_LABEL.to_string()
} else if source == Source::Refreshed {
"this box's refresh signer".to_string()
} else {
format!("estate signer {}", &fp[..fp.len().min(12)])
};
let age = crate::stale::StalenessRule::age_days(i.snapshot_at, now).max(0);
let date = i.snapshot_at.format("%Y-%m-%d").to_string();
let state = if age > i64::from(stale_after_days) {
State::Stale
} else {
State::Ok
};
let days = if age == 1 { "day" } else { "days" };
let missing = read_coverage(db.root(), &i.bundle_id);
let lists = if missing.is_empty() {
String::new()
} else {
format!(
" · no popular-names list for {}: their pulls are held",
missing.join(", ")
)
};
IntelStatus {
line: format!(
"CVE data: {} {date} (signed by {signer}) · {age} {days} old{lists}",
source.as_str()
),
missing_popular: missing,
source,
date: Some(date),
signer,
age_days: Some(age),
state,
stale_after_days,
bundle_id: Some(i.bundle_id.clone()),
sequence: Some(i.sequence),
alert: None,
}
}