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

nordisk/holger

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,
    }
}