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;