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

nordisk/holger

server/lib/src/security_view.rs

//! **`AdminService.SecuritySummary`: the Security page, as the server words it** (lane
//! HOL-SEC, owner 2026-10-27: "Security must show security, not the machinery again").
//!
//! Holger is a security gateway: its product is the intake chain. The console's Security
//! page is a thin layer, so every word on it is decided here, in sections:
//!
//! | key | what it says |
//! |---|---|
//! | `chain` | the steps every package passes, and how many stand at each now |
//! | `sources` | each declared source and what is checked for it (its effective policy), or that it skips the chain |
//! | `scanners` | each scanner slot, what fills it, whether it is required, and where scans run (a container) |
//! | `approvals` | what waits for the two-person approval, with its votes |
//! | `held` | what is quarantined, blocked or refused, and why, by name |
//! | `access` | who may read: the `access` block's words and each repository's visibility (HCON-5) |
//! | `sync` | the key that signs what goes downstream, and each downstream's and upstream's key |
//! | `events` | the last security events of the audit chain |
//!
//! [`Sources::gather`] reads what the server already recorded (it blocks: the intake record,
//! the audit log); [`summary`] is pure and decides every word. Nothing here dials a box.

use std::sync::Arc;

use crate::grpc::holger_proto as pb;

/// The section keys, in the page's order.
pub const SECTION_CHAIN: &str = "chain";
pub const SECTION_SOURCES: &str = "sources";
pub const SECTION_SCANNERS: &str = "scanners";
pub const SECTION_APPROVALS: &str = "approvals";
pub const SECTION_HELD: &str = "held";
pub const SECTION_ACCESS: &str = "access";
pub const SECTION_SYNC: &str = "sync";
pub const SECTION_EVENTS: &str = "events";
/// Where untrusted work runs (lane HOL-SANDBOX-TRAIT): the job runner and what it has.
pub const SECTION_RUNNER: &str = "runner";

pub const TONE_GOOD: &str = "good";
pub const TONE_WARN: &str = "warn";
pub const TONE_BAD: &str = "bad";
pub const TONE_INFO: &str = "info";

/// How many audit events the page shows.
pub const EVENTS_SHOWN: usize = 12;
/// How many refusals the `held` section shows.
pub const REFUSALS_SHOWN: usize = 20;

/// The prefix the intake chain writes before a refusal it audits.
pub const INTAKE_REFUSED: &str = "intake refused: ";

// ── the facts, as gathered ──────────────────────────────────────────────────

/// **Everything the page says, as the server recorded it.** Plain data, so [`summary`] is
/// pure and tested without a server.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Facts {
    /// `None`: this Holger runs no intake chain.
    pub chain: Option<Chain>,
    pub access: Access,
    /// `None`: no `sync:` block.
    pub sync: Option<SyncFacts>,
    /// The audit records, oldest first; `Err` when the sink keeps none (audit off).
    pub events: Result<Vec<Event>, String>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Chain {
    /// How many distinct approvers APPROVED needs.
    pub approvals: u32,
    /// Where scans run.
    pub sandbox: Sandbox,
    /// One per scanner kind, in the fold's order. Empty: the build has no scanners.
    pub scanners: Vec<ScannerSlot>,
    /// Each declared source, in declaration order.
    pub sources: Vec<SourceFact>,
    /// Every artifact with an intake record.
    pub items: Vec<Item>,
    /// The job runner every bundle and scan runs in (lane HOL-SANDBOX-TRAIT).
    pub runner: Option<RunnerFacts>,
}

/// **The job runner, as it states itself** (`JobRunner::features`).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RunnerFacts {
    pub runner: String,
    pub interim: bool,
    /// The pinned per-boot result key's id.
    pub key_id: String,
    /// `(what, has it, the words)`, from `holger_job_runner::feature_rows`.
    pub rows: Vec<(String, bool, String)>,
    /// What the runner got from the kernel (Heimdall's report): `(what, state, good, words)`.
    pub kernel: Vec<(String, String, bool, String)>,
    /// Why the runner is not the one the config asked for (lane HOL-HEIMDALL-02).
    pub fallback: Option<String>,
}

impl RunnerFacts {
    #[cfg(feature = "intake")]
    pub fn of(f: &crate::job_runner::heimdall_core::Features, key_id: &str) -> Self {
        Self {
            runner: f.runner.clone(),
            interim: f.runner != "heimdall",
            key_id: key_id.to_string(),
            rows: holger_job_runner::feature_rows(f)
                .into_iter()
                .filter(|(k, ..)| *k != "runner")
                .map(|(k, has, w)| (k.to_string(), has, w))
                .collect(),
            kernel: Vec::new(),
            fallback: None,
        }
    }

    /// With the kernel report the runner made at its start (Heimdall's): the release and
    /// every feature Heimdall's strict mode asks for, as rows.
    #[cfg(feature = "intake")]
    pub fn with_kernel(
        mut self,
        k: Option<&crate::job_runner::heimdall_core::KernelFeatures>,
    ) -> Self {
        let Some(k) = k else { return self };
        let yes = |b: bool| if b { "yes" } else { "no" }.to_string();
        self.kernel = vec![
            ("kernel".into(), k.release.clone(), true, "The kernel release the runner probed at its start.".into()),
            ("kernel: seccomp filters".into(), yes(k.seccomp_filter), k.seccomp_filter, "Seccomp filters that kill the whole job (SECCOMP_RET_KILL_PROCESS).".into()),
            (
                "kernel: Landlock".into(),
                k.landlock_abi.map(|a| format!("ABI {a}")).unwrap_or_else(|| "no".into()),
                k.landlock_abi.is_some(),
                "Landlock, which limits a job to its own in and out whatever the mounts allow.".into(),
            ),
            (
                "kernel: cgroup v2".into(),
                yes(k.cgroup_v2),
                k.cgroup_v2,
                format!("cgroup v2; the jobs' parent offers {}.", if k.cgroup_controllers.is_empty() { "no controller".to_string() } else { k.cgroup_controllers.join(", ") }),
            ),
            ("kernel: user namespaces".into(), yes(k.user_namespaces), k.user_namespaces, "Unprivileged user namespaces: a job's root is the runner's own user.".into()),
            ("kernel: user notification".into(), yes(k.user_notif && k.notif_addfd), true, "Seccomp user notification, for the syscall broker (reported; nothing uses it yet).".into()),
        ];
        self
    }

    /// With the reason the runner in use is a fallback.
    pub fn with_fallback(mut self, why: Option<&str>) -> Self {
        self.fallback = why.map(str::to_string);
        self
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Sandbox {
    /// Each scan in its own container; the engine's words.
    Container(String),
    /// The worker runs in this process (`sandbox: off`, a test build).
    InProcess,
    /// No scan worker: the scanners run in this process.
    #[default]
    None,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ScannerSlot {
    /// The wire label (`provenance`, `malware`, …).
    pub kind: String,
    pub required: bool,
    /// `<id> <version>` of each installed scanner.
    pub installed: Vec<String>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SourceFact {
    pub name: String,
    /// `upload` | `pull` | `mirror` | `proxy` (`crate::upstreams::KIND_*`).
    pub kind: String,
    /// The source kind as declared (`python`, `oci`, `crates`, …).
    pub speaks: String,
    /// What is checked for it, in words; `None` for a source whose bytes skip the chain.
    pub checks: Option<String>,
    /// What the console warns about it (a fetch source's `plaintext`, `internal`; lane cds4).
    pub warnings: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Item {
    pub id: traits::ArtifactId,
    /// The state's operator name (`POLICY_CHECKED`, `QUARANTINED`, `BLOCKED`, …).
    pub state: String,
    /// The state a QUARANTINED artifact was held at.
    pub held_from: Option<String>,
    /// Who made the last step, and why.
    pub actor: String,
    pub reason: String,
    pub importer: String,
    /// The approvals that count now.
    pub votes: u32,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Access {
    pub gated: bool,
    pub front_open: bool,
    pub public: Vec<String>,
    pub repos: Vec<Repo>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Repo {
    pub name: String,
    pub public: bool,
    /// An owner-only (sold) repository.
    pub owner_only: bool,
    /// A `ron_require_role` floor, lowercase.
    pub floor: Option<String>,
    /// `ron_require_auth` (or made private at run time).
    pub gated: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SyncFacts {
    pub name: String,
    /// This Holger's sync public key, hex.
    pub key: String,
    pub downstreams: Vec<SyncPeer>,
    pub upstreams: Vec<SyncPeer>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SyncPeer {
    pub name: String,
    pub key: String,
    /// The sync board's state and headline (downstreams only).
    pub state: String,
    pub headline: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Event {
    pub ts_nanos: i64,
    pub ident: String,
    /// The action's wire word (`promote`, `scan`, …).
    pub action: String,
    /// The trail step's wire word, when the event is one.
    pub trail: String,
    pub repo: String,
    pub artifact: String,
    pub status: u16,
    pub detail: String,
}

// ── who may read it ─────────────────────────────────────────────────────────

/// **May this caller read the Security page?** An Admin (the role, or the Repository Admin
/// duty), a Security Approver or an Auditor. With RBAC off every caller may, as every
/// other gate of this server.
pub fn authorize_read(
    auth: &crate::auth::AuthConfig,
    identity: Option<&crate::auth::AuthIdentity>,
) -> Result<(), String> {
    use crate::auth::Duty;
    if !auth.rbac_enabled() {
        return Ok(());
    }
    let Some(id) = identity else {
        return Err("the Security page is read by an Admin, a Security Approver or an Auditor; this request carries no sign-in".into());
    };
    let held = auth.duties_for_identity(id);
    if auth.role_for_identity(id) >= crate::auth::Role::Admin
        || held.contains(&Duty::RepositoryAdmin)
        || held.contains(&Duty::SecurityApprover)
        || held.contains(&Duty::Auditor)
    {
        return Ok(());
    }
    Err(format!("'{}' may not read the Security page: it is read by an Admin, a Security Approver or an Auditor", id.subject))
}

// ── the words ───────────────────────────────────────────────────────────────

fn row(
    name: impl Into<String>,
    state: impl Into<String>,
    tone: &str,
    detail: impl Into<String>,
) -> pb::SecurityRow {
    pb::SecurityRow {
        name: name.into(),
        state: state.into(),
        tone: tone.to_string(),
        detail: detail.into(),
        artifact: None,
    }
}

/// Pure: an audit record's artifact as the page names it: the doors record `name@version`,
/// and a file with no version as `name@`, which the page shows as `name`.
fn shown(artifact: &str) -> &str {
    artifact.strip_suffix('@').unwrap_or(artifact)
}

fn wire_id(id: &traits::ArtifactId) -> pb::ArtifactId {
    pb::ArtifactId {
        namespace: id.namespace.clone().unwrap_or_default(),
        name: id.name.clone(),
        version: id.version.clone(),
    }
}

fn label(id: &traits::ArtifactId) -> String {
    let base = match id.namespace.as_deref() {
        Some(ns) if !ns.is_empty() => format!("{ns}/{}", id.name),
        _ => id.name.clone(),
    };
    if id.version.is_empty() {
        base
    } else {
        format!("{base}@{}", id.version)
    }
}

fn plural(n: usize, one: &str, many: &str) -> String {
    if n == 1 {
        format!("1 {one}")
    } else {
        format!("{n} {many}")
    }
}

/// A key, short enough to read and compare: its first 16 hex digits.
pub fn short_key(hex: &str) -> String {
    let h = hex.trim();
    if h.len() > 16 {
        format!("{}…", &h[..16])
    } else {
        h.to_string()
    }
}

/// `YYYY-MM-DD HH:MM UTC`.
pub fn when(ts_nanos: i64) -> String {
    let s = crate::audit::siem::rfc3339_micros(ts_nanos);
    format!("{} UTC", s.get(..16).unwrap_or(&s).replace('T', " "))
}

/// One step of the chain: its state names, its title and what it does.
const STEPS: [(&[&str], &str, &str); 6] = [
    (
        &["UNTRUSTED", "INGESTED"],
        "In",
        "The bytes land in the incoming repository. The ingest gates run first.",
    ),
    (
        &["SCANNED"],
        "Scanned",
        "Every scanner below looks at the package.",
    ),
    (
        &["POLICY_CHECKED"],
        "Policy checked",
        "Its source's rules decide. A refusal names the rule.",
    ),
    (&["APPROVED"], "Approved", ""),
    (
        &["SEALED", "PROMOTED"],
        "Sealed",
        "Stored for good under its sha256. It never changes.",
    ),
    (&["AVAILABLE"], "Served", "Only now does Holger serve it."),
];

fn approvals_words(n: u32) -> String {
    if n <= 1 {
        "One person other than the importer approves it.".into()
    } else {
        format!("{n} different people approve it. The importer cannot approve.")
    }
}

/// What one scanner kind does, in plain words.
pub fn scanner_words(kind: &str) -> (&'static str, &'static str) {
    match kind {
        "inspection" => ("Inspection", "Opens the archive safely: bombs, deep nesting, path tricks, hidden programs."),
        "model" => ("AI model files", "Checks GGUF, Safetensors and ONNX files. Pickle files are blocked."),
        "malware" => ("Malware", "Looks for malware with ClamAV and YARA rules."),
        "vulnerability" => ("CVEs", "Matches packages and container images (their SBOM or their layers) against the local CVE database."),
        "license" => ("Licenses", "Checks each license against the policy."),
        "secrets" => ("Secrets", "Looks for keys and tokens left inside."),
        "provenance" => ("Provenance", "Verifies signatures and build provenance offline: cosign keyless, DSSE and SLSA attestations, as Chainguard ships them."),
        "sbom" => ("SBOM", "Writes the SBOM: every component and its version."),
        _ => ("Scanner", ""),
    }
}

fn chain_section(c: &Chain) -> pb::SecuritySection {
    let rows = STEPS
        .iter()
        .map(|(states, title, does)| {
            let n = c
                .items
                .iter()
                .filter(|i| states.contains(&i.state.as_str()))
                .count();
            let does = if *title == "Approved" {
                approvals_words(c.approvals)
            } else {
                does.to_string()
            };
            row(*title, n.to_string(), TONE_INFO, does)
        })
        .collect();
    pb::SecuritySection {
        key: SECTION_CHAIN.into(),
        title: "Intake chain".into(),
        summary: "Every package passes these steps before Holger serves it. A quarantined or blocked package is never served.".into(),
        empty: String::new(),
        rows,
    }
}

fn runner_section(r: &RunnerFacts) -> pb::SecuritySection {
    let summary = format!(
        "Every bundle that comes in and every scan run as jobs in {}{}, never inside Holger; Holger believes only a job's result signed with the key it pinned at boot ({}).",
        r.runner,
        if r.interim { ", the interim runner, not Heimdall" } else { "" },
        r.key_id
    );
    let summary = match &r.fallback {
        Some(why) => {
            format!("{summary} The configured runner is Heimdall, and it did not start: {why}.")
        }
        None => summary,
    };
    let rows = r
        .rows
        .iter()
        .map(|(k, has, words)| {
            row(
                k.as_str(),
                if *has { "yes" } else { "no" },
                if *has { TONE_GOOD } else { TONE_WARN },
                format!("{words}."),
            )
        })
        .chain(r.kernel.iter().map(|(k, state, good, words)| {
            row(
                k.as_str(),
                state.as_str(),
                if *good { TONE_GOOD } else { TONE_WARN },
                words.as_str(),
            )
        }))
        .collect();
    pb::SecuritySection {
        key: SECTION_RUNNER.into(),
        title: "Where untrusted work runs".into(),
        summary,
        empty: String::new(),
        rows,
    }
}

fn sources_section(c: &Chain) -> pb::SecuritySection {
    let rows = c
        .sources
        .iter()
        .map(|s| match &s.checks {
            Some(checks) if !s.warnings.is_empty() => row(
                &s.name,
                s.kind.clone(),
                TONE_WARN,
                format!("{} source. {checks} Warning: {}.", s.speaks, s.warnings.join("; ")),
            ),
            Some(checks) => row(&s.name, s.kind.clone(), TONE_GOOD, format!("{} source. {checks}", s.speaks)),
            None => row(&s.name, s.kind.clone(), TONE_WARN, format!("{} source. Served as fetched: it skips the intake chain, so no scanner sees it.", s.speaks)),
        })
        .collect();
    pb::SecuritySection {
        key: SECTION_SOURCES.into(),
        title: "Sources".into(),
        summary: "Where packages come from, and what is checked for each.".into(),
        empty: "No source is declared. Uploads go through the intake chain.".into(),
        rows,
    }
}

fn scanners_section(c: &Chain) -> pb::SecuritySection {
    let summary = match &c.sandbox {
        Sandbox::Container(engine) => {
            format!("Every scan runs in its own container ({engine}), never inside Holger.")
        }
        Sandbox::InProcess => {
            "Scans run inside Holger: this is a test build (sandbox: off).".to_string()
        }
        Sandbox::None => "Scans run inside Holger.".to_string(),
    };
    let rows = c
        .scanners
        .iter()
        .map(|s| {
            let (title, does) = scanner_words(&s.kind);
            let (state, tone) = match (s.installed.is_empty(), s.required) {
                (false, true) => ("required", TONE_GOOD),
                (false, false) => ("on", TONE_GOOD),
                (true, true) => ("missing", TONE_BAD),
                (true, false) => ("off", TONE_INFO),
            };
            let detail = if s.installed.is_empty() {
                if s.required {
                    format!("{does} Required, but not installed: every package is refused at this step.")
                } else {
                    format!("{does} Not installed.")
                }
            } else {
                format!("{does} Runs {}.", s.installed.join(", "))
            };
            row(title, state, tone, detail)
        })
        .collect();
    pb::SecuritySection {
        key: SECTION_SCANNERS.into(),
        title: "Scanners".into(),
        summary,
        empty: "This build has no scanners.".into(),
        rows,
    }
}

fn approvals_section(c: &Chain) -> pb::SecuritySection {
    let rows = c
        .items
        .iter()
        .filter(|i| i.state == "POLICY_CHECKED")
        .map(|i| {
            let mut r = row(
                label(&i.id),
                format!("{} of {}", i.votes, c.approvals.max(1)),
                TONE_WARN,
                if i.importer.is_empty() {
                    "Passed every check.".to_string()
                } else {
                    format!("Passed every check. Imported by {}.", i.importer)
                },
            );
            r.artifact = Some(wire_id(&i.id));
            r
        })
        .collect();
    pb::SecuritySection {
        key: SECTION_APPROVALS.into(),
        title: "Waiting for approval".into(),
        summary: approvals_words(c.approvals),
        empty: "Nothing waits for approval.".into(),
        rows,
    }
}

fn held_section(c: Option<&Chain>, events: &[Event]) -> pb::SecuritySection {
    let mut rows: Vec<pb::SecurityRow> = Vec::new();
    for i in c.map(|c| c.items.as_slice()).unwrap_or_default() {
        let (state, tone) = match i.state.as_str() {
            "QUARANTINED" => ("quarantined", TONE_WARN),
            "BLOCKED" => ("blocked", TONE_BAD),
            _ => continue,
        };
        let why = if i.reason.is_empty() {
            "no reason was given".to_string()
        } else {
            i.reason.clone()
        };
        let mut detail = format!(
            "{why}. By {}",
            if i.actor.is_empty() {
                "holger"
            } else {
                i.actor.as_str()
            }
        );
        if let Some(from) = &i.held_from {
            detail.push_str(&format!(
                ", held at {}",
                from.to_lowercase().replace('_', " ")
            ));
        }
        detail.push('.');
        let mut r = row(label(&i.id), state, tone, detail);
        r.artifact = Some(wire_id(&i.id));
        rows.push(r);
    }
    for e in events
        .iter()
        .rev()
        .filter(|e| e.detail.starts_with(INTAKE_REFUSED))
        .take(REFUSALS_SHOWN)
    {
        let name = match (e.repo.is_empty(), e.artifact.is_empty()) {
            (_, false) => shown(&e.artifact).to_string(),
            (false, true) => e.repo.clone(),
            (true, true) => "a package".to_string(),
        };
        let why = e.detail.trim_start_matches(INTAKE_REFUSED);
        rows.push(row(
            name,
            "refused",
            TONE_BAD,
            format!("{why} ({}, {}).", e.ident, when(e.ts_nanos)),
        ));
    }
    pb::SecuritySection {
        key: SECTION_HELD.into(),
        title: "Held or refused".into(),
        summary: "Quarantined waits for a person. Blocked and refused are never served.".into(),
        empty: "Nothing is held or refused.".into(),
        rows,
    }
}

fn access_section(a: &Access) -> pb::SecuritySection {
    let summary = crate::posture::words(&crate::posture::Posture {
        gated: a.gated,
        front_open: a.front_open,
        public: a.public.clone(),
        why: String::new(),
    });
    let rows = a
        .repos
        .iter()
        .map(|r| {
            if r.public {
                row(
                    &r.name,
                    "public",
                    TONE_WARN,
                    "Anybody reads it, with no sign-in.",
                )
            } else if r.owner_only {
                row(
                    &r.name,
                    "owner only",
                    TONE_GOOD,
                    "Its owner and the people they let in read it.",
                )
            } else if let Some(floor) = &r.floor {
                row(
                    &r.name,
                    format!("{floor} only"),
                    TONE_GOOD,
                    format!("Read by a signed-in {floor} or higher."),
                )
            } else if r.gated || a.gated {
                row(&r.name, "sign-in", TONE_GOOD, "Reading needs a sign-in.")
            } else {
                row(
                    &r.name,
                    "open",
                    TONE_WARN,
                    "Anybody who reaches this Holger reads it.",
                )
            }
        })
        .collect();
    pb::SecuritySection {
        key: SECTION_ACCESS.into(),
        title: "Who can read".into(),
        summary,
        empty: "No repository.".into(),
        rows,
    }
}

fn sync_section(s: Option<&SyncFacts>) -> pb::SecuritySection {
    let Some(s) = s else {
        return pb::SecuritySection {
            key: SECTION_SYNC.into(),
            title: "Signed sync".into(),
            summary: "This Holger syncs with no other Holger.".into(),
            empty: "No downstream and no upstream.".into(),
            rows: Vec::new(),
        };
    };
    let mut rows: Vec<pb::SecurityRow> = s
        .downstreams
        .iter()
        .map(|d| {
            let tone = match d.state.as_str() {
                "current" => TONE_GOOD,
                "alarm" => TONE_BAD,
                _ => TONE_WARN,
            };
            let state = if d.state.is_empty() {
                "downstream".to_string()
            } else {
                d.state.clone()
            };
            let mut detail = format!(
                "Downstream. Its signed acknowledgements are checked against its key {}.",
                short_key(&d.key)
            );
            if !d.headline.is_empty() {
                detail = format!("{}. {detail}", d.headline.trim_end_matches('.'));
            }
            row(&d.name, state, tone, detail)
        })
        .collect();
    rows.extend(s.upstreams.iter().map(|u| {
        row(
            &u.name,
            "trusted",
            TONE_GOOD,
            format!(
                "Upstream. Holger applies what it sends only when it is signed by key {}.",
                short_key(&u.key)
            ),
        )
    }));
    pb::SecuritySection {
        key: SECTION_SYNC.into(),
        title: "Signed sync".into(),
        summary: format!(
            "Every manifest and airgap bundle this Holger sends downstream is signed with its sync key {}. A downstream applies it only under that key.",
            short_key(&s.key)
        ),
        empty: "No downstream and no upstream.".into(),
        rows,
    }
}

/// Is this audit event about security? Reads and listings are not; every intake step, scan,
/// approval, refusal, token, deletion and setting change is.
pub fn is_security_event(e: &Event) -> bool {
    !e.trail.is_empty() || !matches!(e.action.as_str(), "download" | "list" | "upload")
}

fn events_section(events: &Result<Vec<Event>, String>) -> pb::SecuritySection {
    let (summary, rows) = match events {
        Err(why) => (
            format!("The audit log cannot be read here: {why}."),
            Vec::new(),
        ),
        Ok(events) => (
            "The newest first. Every event is in the audit chain, where it cannot be changed."
                .to_string(),
            events
                .iter()
                .rev()
                .filter(|e| is_security_event(e))
                .take(EVENTS_SHOWN)
                .map(|e| {
                    let what = if e.trail.is_empty() {
                        e.action.replace('_', " ")
                    } else {
                        e.trail.replace('_', " ")
                    };
                    let (state, tone) = if e.status >= 400 {
                        ("refused", TONE_BAD)
                    } else {
                        ("done", TONE_GOOD)
                    };
                    let on = match (e.repo.is_empty(), e.artifact.is_empty()) {
                        (false, false) => format!(" on {}/{}", e.repo, shown(&e.artifact)),
                        (false, true) => format!(" on {}", e.repo),
                        (true, false) => format!(" on {}", shown(&e.artifact)),
                        (true, true) => String::new(),
                    };
                    let mut detail = format!(
                        "{} by {}{on}.",
                        when(e.ts_nanos),
                        if e.ident.is_empty() {
                            "holger"
                        } else {
                            e.ident.as_str()
                        }
                    );
                    if !e.detail.is_empty() {
                        detail.push(' ');
                        detail.push_str(&e.detail);
                    }
                    row(what, state, tone, detail)
                })
                .collect(),
        ),
    };
    pb::SecuritySection {
        key: SECTION_EVENTS.into(),
        title: "Recent security events".into(),
        summary,
        empty: "No security event yet.".into(),
        rows,
    }
}

/// Pure: **the Security page**, every word decided here.
pub fn summary(f: &Facts) -> pb::SecuritySummaryReply {
    let no_events: Vec<Event> = Vec::new();
    let events = f.events.as_ref().unwrap_or(&no_events);
    let mut sections = Vec::new();
    let headline = match &f.chain {
        None => "This Holger runs no intake chain: it checks nothing before it serves.".to_string(),
        Some(c) => {
            sections.push(chain_section(c));
            if let Some(r) = &c.runner {
                sections.push(runner_section(r));
            }
            sections.push(sources_section(c));
            sections.push(scanners_section(c));
            sections.push(approvals_section(c));
            let waiting = c
                .items
                .iter()
                .filter(|i| i.state == "POLICY_CHECKED")
                .count();
            let held = c
                .items
                .iter()
                .filter(|i| i.state == "QUARANTINED" || i.state == "BLOCKED")
                .count();
            let mut h = format!(
                "Holger checks every package before it serves it. {} for approval, {} held.",
                plural(waiting, "package waits", "packages wait"),
                held
            );
            if let Sandbox::Container(_) = c.sandbox {
                h.push_str(" Scans run in containers.");
            }
            h
        }
    };
    sections.push(held_section(f.chain.as_ref(), events));
    sections.push(access_section(&f.access));
    sections.push(sync_section(f.sync.as_ref()));
    sections.push(events_section(&f.events));
    pb::SecuritySummaryReply { headline, sections }
}

// ── what a source is checked for ────────────────────────────────────────────

/// Pure: **what is checked for a source**, from its effective policy, in words.
#[cfg(feature = "intake")]
pub fn policy_words(p: &crate::sources::EffectivePolicy, approvals: u32) -> String {
    use crate::scanner::FindingSeverity;
    use crate::sources::{ProvenanceMode, SecretsMode};
    let sev = |s: FindingSeverity| match s {
        FindingSeverity::Info => "info",
        FindingSeverity::Low => "low",
        FindingSeverity::Medium => "medium",
        FindingSeverity::High => "high",
        FindingSeverity::Critical => "critical",
    };
    let mut w: Vec<String> = Vec::new();
    w.push(match p.provenance.value {
        ProvenanceMode::Require => {
            let mut s = "Signature required".to_string();
            if !p.identities.is_empty() {
                let who: Vec<String> = p
                    .identities
                    .iter()
                    .map(|i| {
                        let subject = if i.subject.is_empty() {
                            &i.subject_regex
                        } else {
                            &i.subject
                        };
                        if subject.is_empty() {
                            i.issuer.clone()
                        } else {
                            format!("{subject} ({})", i.issuer)
                        }
                    })
                    .collect();
                s.push_str(&format!(", signed by {}", who.join(" or ")));
            }
            s
        }
        ProvenanceMode::Prefer => "Signature checked when there is one".to_string(),
        ProvenanceMode::Off => "No signature check".to_string(),
    });
    if p.slsa_min.value > 0 {
        w.push(format!("SLSA level {} or higher", p.slsa_min.value));
    }
    w.push(if p.malware.value {
        "malware scan".into()
    } else {
        "no malware scan".into()
    });
    w.push(format!("refuses a CVE above {}", sev(p.cve_max.value)));
    if p.sbom.value {
        w.push("SBOM required".into());
    }
    match p.secrets.value {
        SecretsMode::Off => {}
        SecretsMode::Warn => w.push("secrets reported".into()),
        SecretsMode::Block => w.push("secrets refused".into()),
    }
    if !p.deny_licenses.is_empty() {
        let l: Vec<&str> = p.deny_licenses.iter().map(|b| b.value.as_str()).collect();
        w.push(format!("denies licenses {}", l.join(", ")));
    }
    if !p.block_formats.is_empty() {
        let l: Vec<&str> = p.block_formats.iter().map(|b| b.value.as_str()).collect();
        w.push(format!("blocks {}", l.join(", ")));
    }
    let approval = match &p.auto_approve {
        Some(a) => format!(
            "approved by itself when {} and no CVE above {}",
            match a.provenance {
                ProvenanceMode::Require => "signed",
                ProvenanceMode::Prefer => "any signature verifies",
                ProvenanceMode::Off => "it passed every check",
            },
            sev(a.cve_max)
        ),
        None if approvals > 1 => format!("{approvals} people approve"),
        None => "one person approves".to_string(),
    };
    w.push(approval);
    let mut s = w.join("; ");
    s.push('.');
    s
}

// ── the live server's facts ─────────────────────────────────────────────────

/// **The live server's evidence**: the handles [`Sources::gather`] reads.
#[derive(Clone)]
pub struct Sources {
    #[cfg(feature = "intake")]
    pub intake: Option<Arc<crate::promote::intake::IntakeChain>>,
    pub declared: Arc<crate::topology::Declared>,
    pub read_gate: Arc<crate::auth::ReadGate>,
    pub gated: bool,
    /// Every repository this server serves, by name.
    pub repos: Vec<String>,
    pub audit: Arc<dyn crate::audit::AuditLog>,
}

impl Sources {
    /// Read what the server recorded. Blocks (the intake record, the audit log).
    pub fn gather(&self) -> Facts {
        let g = &self.read_gate;
        let access = Access {
            gated: self.gated,
            front_open: g.front_open(),
            public: g.public_names(),
            repos: self
                .repos
                .iter()
                .map(|name| Repo {
                    name: name.clone(),
                    public: g.is_public(name),
                    owner_only: g.sold_owner(name).is_some(),
                    floor: g.floor(name).map(|r| format!("{r:?}").to_lowercase()),
                    gated: g.repo_requires_auth(name),
                })
                .collect(),
        };
        let events = match self.audit.query_source() {
            None => Err("audit is off on this Holger".to_string()),
            Some(src) => src
                .events()
                .map(|evs| {
                    evs.into_iter()
                        .map(|e| Event {
                            ts_nanos: e.ts_nanos,
                            ident: e.ident,
                            action: e.action.as_str().to_string(),
                            trail: e.trail.map(|t| t.as_str().to_string()).unwrap_or_default(),
                            repo: e.repo,
                            artifact: e.artifact,
                            status: e.status,
                            detail: e.detail,
                        })
                        .collect()
                })
                .map_err(|e| e.to_string()),
        };
        Facts {
            chain: self.chain(),
            access,
            sync: self.sync(),
            events,
        }
    }

    #[cfg(not(feature = "intake"))]
    fn chain(&self) -> Option<Chain> {
        None
    }

    #[cfg(feature = "intake")]
    fn chain(&self) -> Option<Chain> {
        use crate::upstreams::{KIND_PULL, KIND_UPLOAD};
        let chain = self.intake.as_ref()?;
        let approvals = chain.approvals_required();
        #[cfg(feature = "scanners")]
        let sandbox = match chain.worker() {
            Some(w) if w.is_isolated() => Sandbox::Container(w.engine_words().to_string()),
            Some(_) => Sandbox::InProcess,
            None => Sandbox::None,
        };
        #[cfg(not(feature = "scanners"))]
        let sandbox = Sandbox::None;
        let scanners = match chain.scanner() {
            None => Vec::new(),
            Some(p) => {
                let set = p.scanners();
                crate::scanner::ScannerKind::ALL
                    .iter()
                    .map(|k| ScannerSlot {
                        kind: k.as_str().to_string(),
                        required: set.required().contains(k),
                        installed: set.installed(*k),
                    })
                    .collect()
            }
        };
        let policies = chain.sources();
        let mut sources: Vec<SourceFact> = self
            .declared
            .upstreams
            .iter()
            .map(|u| {
                let through = u.kind == KIND_PULL || u.kind == KIND_UPLOAD;
                let checks = through.then(|| match policies.and_then(|p| p.named(&u.name)) {
                    Some(p) => policy_words(p, approvals),
                    None => "Checked by every step of the intake chain.".to_string(),
                });
                SourceFact {
                    name: u.name.clone(),
                    kind: u.kind.clone(),
                    speaks: u.speaks.clone(),
                    checks,
                    warnings: Vec::new(),
                }
            })
            .collect();
        // Lane cds4: the fetch sources, each fetched only in Heimdall jobs; plaintext and
        // internal are warnings.
        sources.extend(self.declared.fetch.iter().map(|f| SourceFact {
            name: f.name.clone(),
            kind: "fetch".into(),
            speaks: "fetch".into(),
            checks: Some(format!(
                "Fetched only in Heimdall jobs, from {}.",
                f.hosts.join(", ")
            )),
            warnings: f.warnings.clone(),
        }));
        let items = chain
            .list()
            .into_iter()
            .map(|(id, st)| {
                let votes = if st.state == crate::promote::intake::ArtifactState::PolicyChecked {
                    chain.pending_approvals(&id).len() as u32
                } else {
                    0
                };
                Item {
                    importer: chain.importer(&id).unwrap_or_default(),
                    state: st.state.name().to_string(),
                    held_from: st.held_from.map(|s| s.name().to_string()),
                    actor: st.last.actor.clone(),
                    reason: st.last.reason.clone(),
                    votes,
                    id,
                }
            })
            .collect();
        let runner = chain.jobs().map(|j| {
            RunnerFacts::of(&j.features(), &j.key_id())
                .with_kernel(j.kernel())
                .with_fallback(j.fallback())
        });
        Some(Chain {
            approvals,
            sandbox,
            scanners,
            sources,
            items,
            runner,
        })
    }

    #[cfg(not(feature = "sync"))]
    fn sync(&self) -> Option<SyncFacts> {
        None
    }

    #[cfg(feature = "sync")]
    fn sync(&self) -> Option<SyncFacts> {
        let hub = self.intake.as_ref()?.sync()?;
        let board = hub.board(crate::sync::protocol::now_secs()).ok();
        let cfg = hub.config();
        let downstreams = cfg
            .downstreams
            .iter()
            .map(|d| {
                let b = board
                    .as_ref()
                    .and_then(|b| b.downstreams.iter().find(|r| r.name == d.name));
                SyncPeer {
                    name: d.name.clone(),
                    key: d.key.clone(),
                    state: b.map(|r| r.state.clone()).unwrap_or_default(),
                    headline: b.map(|r| r.headline.clone()).unwrap_or_default(),
                }
            })
            .collect();
        let upstreams = cfg
            .upstreams
            .iter()
            .map(|u| SyncPeer {
                name: u.name.clone(),
                key: u.key.clone(),
                ..Default::default()
            })
            .collect();
        Some(SyncFacts {
            name: cfg.name.clone(),
            key: hub.public_key(),
            downstreams,
            upstreams,
        })
    }
}

#[cfg(test)]
mod tests;