src/security.rs
//! **The security workflow — what its pages show**, pure and host-tested;
//! `pages/security.rs` renders it.
//!
//! Two pages, both read from holger's intake chain and nothing else:
//!
//! * **the dashboard** (`/security`): holger's queues — Incoming, Quarantined,
//! Awaiting approval, Blocked, Approved / available — as tiles with the
//! server's counts, and the rows of one queue (`ListIntakeArtifacts`), under
//! the gateway as holger words it (`AdminService.SecuritySummary`: the chain,
//! the sources and scanners, what waits and what is held, who reads, the sync
//! signatures, the last security events). The estate board is the Overview's;
//! * **the artifact view** (`/security/artifact?ns=&name=&version=`): its state
//! and votes (`GetArtifactState`), its scan evidence pane by pane — Integrity,
//! Malware, Vulnerabilities, License, Provenance, SBOM, Policy
//! (`GetArtifactEvidence`) — and four buttons that each send one
//! `AdvanceArtifact`; and the audit trail of its bytes with the chain's
//! verdict (`AuditService.GetArtifactTrail`).
//!
//! **Thin.** Which states make up a queue, how many votes count, who may press
//! which button: all of it is the server's. The buttons are the same four for
//! every artifact and every visitor; a press the server refuses shows the
//! server's code and words, nothing else. This module only
//! turns the server's answers into lines.
//!
//! **The door is a trait** ([`IntakeDoor`]) so the pages and the tests drive
//! the same loaders: the wasm build talks gRPC-web to holger, the tests talk to
//! a mock server with canned answers.
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
#![allow(async_fn_in_trait)]
#[cfg(not(target_arch = "wasm32"))]
use crate::admin_queue::pb;
#[cfg(target_arch = "wasm32")]
use crate::grpc::pb;
use crate::util::{display_name, enc, Tone};
/// The page of the dashboard.
pub const DASHBOARD: &str = "/security";
/// The queue a dashboard opened without one shows.
pub const DEFAULT_QUEUE: &str = "awaiting-approval";
/// A call the server answered with a refusal: the gRPC code's name and the
/// server's message, verbatim.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Refused {
pub code: String,
pub message: String,
}
/// **holger's intake door**, as the security pages use it.
pub trait IntakeDoor {
async fn list(&self, req: pb::ListIntakeArtifactsRequest) -> Result<pb::ListIntakeArtifactsResponse, Refused>;
async fn state(&self, req: pb::GetArtifactStateRequest) -> Result<pb::GetArtifactStateResponse, Refused>;
async fn evidence(&self, req: pb::GetArtifactEvidenceRequest) -> Result<pb::GetArtifactEvidenceResponse, Refused>;
async fn advance(&self, req: pb::AdvanceArtifactRequest) -> Result<pb::AdvanceArtifactResponse, Refused>;
/// `AdminService.Topology`: the estate board's rows (its line under the drawing).
async fn topology(&self) -> Result<pb::TopologyReply, Refused>;
/// `AdminService.FlowSvg`: the artifact flow, drawn by the server from its RON, with the
/// machines `open` names unfolded (`""` is nothing open, `all` everything).
async fn flow(&self, open: &str) -> Result<pb::FlowSvgReply, Refused>;
/// `AuditService.GetArtifactTrail`: the audit trail of the bytes.
async fn trail(&self, req: pb::GetArtifactTrailRequest) -> Result<pb::GetArtifactTrailResponse, Refused>;
/// `AdminService.SecuritySummary`: the Security page, every word the server's.
async fn security(&self) -> Result<pb::SecuritySummaryReply, Refused>;
}
// ── the server's words, shown ───────────────────────────────────────────────
/// A refusal split into the server's code (the bracketed tag, when its message
/// starts with one in brackets) and the rest of its sentence.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefusalView {
pub code: String,
pub text: String,
}
/// Pure: how a refusal reads. The code is the server's bracketed tag; a
/// message without one shows the gRPC code's name instead.
pub fn refusal(r: &Refused) -> RefusalView {
let m = r.message.trim();
if let Some(rest) = m.strip_prefix('[') {
if let Some((code, text)) = rest.split_once(']') {
if !code.is_empty() && !code.contains(' ') {
return RefusalView { code: code.to_string(), text: text.trim().to_string() };
}
}
}
RefusalView { code: r.code.clone(), text: m.to_string() }
}
/// Pure: the badge tone of a state word the server sent (a word this console
/// does not know is shown as it came, untoned).
pub fn state_tone(state: &str) -> Tone {
match state {
"AVAILABLE" | "APPROVED" | "SEALED" | "PROMOTED" => Tone::Good,
"POLICY_CHECKED" | "QUARANTINED" => Tone::Warn,
"BLOCKED" => Tone::Bad,
_ => Tone::Neutral,
}
}
/// Pure: the badge tone of a verdict word the server sent.
pub fn verdict_tone(verdict: &str) -> Tone {
match verdict {
"pass" => Tone::Good,
"warn" => Tone::Warn,
"block" | "fail" | "error" => Tone::Bad,
_ => Tone::Neutral,
}
}
/// Pure: the server's vote count in words (`1 of 2 approvals`).
pub fn votes_line(approvals: u32, required: u32) -> String {
format!("{approvals} of {required} approval{}", if required == 1 { "" } else { "s" })
}
/// Pure: the link to one artifact's security view.
pub fn artifact_link(id: &pb::ArtifactId) -> String {
format!("{DASHBOARD}/artifact?ns={}&name={}&version={}", enc(&id.namespace), enc(&id.name), enc(&id.version))
}
/// Pure: a coordinate as an operator reads it.
pub fn label(id: &pb::ArtifactId) -> String {
let base = display_name(&id.namespace, &id.name);
if id.version.is_empty() {
base
} else {
format!("{base} {}", id.version)
}
}
/// Pure: a Unix second as `YYYY-MM-DD HH:MM UTC`.
pub fn utc(secs: i64) -> String {
if secs <= 0 {
return "—".into();
}
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
// Civil from days (Howard Hinnant's algorithm).
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + i64::from(m <= 2);
format!("{y:04}-{m:02}-{d:02} {:02}:{:02} UTC", rem / 3_600, rem % 3_600 / 60)
}
fn short(hex: &str) -> String {
hex.chars().take(12).collect()
}
// ── the dashboard ───────────────────────────────────────────────────────────
/// One tile: a queue of the server's, its count, its link.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tile {
pub name: String,
pub title: String,
pub count: u64,
pub href: String,
pub active: bool,
}
/// One row of the open queue.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Row {
pub label: String,
pub href: String,
pub state: String,
pub tone: Tone,
pub sha256: String,
pub importer: String,
/// The server's vote count, on a row awaiting approval; empty otherwise.
pub votes: String,
pub last: String,
pub note: String,
}
/// What the dashboard shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DashboardView {
pub tiles: Vec<Tile>,
/// The title of the open queue.
pub open: String,
pub rows: Vec<Row>,
pub truncated: bool,
}
/// Pure: the dashboard from the server's answer, `queue` open.
pub fn dashboard(resp: &pb::ListIntakeArtifactsResponse, queue: &str) -> DashboardView {
let tiles = resp
.queues
.iter()
.map(|q| Tile {
name: q.name.clone(),
title: q.title.clone(),
count: q.count,
href: format!("{DASHBOARD}?queue={}", enc(&q.name)),
active: q.name == queue,
})
.collect();
let open = resp.queues.iter().find(|q| q.name == queue).map(|q| q.title.clone()).unwrap_or_else(|| queue.to_string());
let rows = resp
.artifacts
.iter()
.map(|a| {
let id = a.artifact.clone().unwrap_or_default();
let mut note = a.reason.clone();
if !a.held_from.is_empty() {
note = format!("held at {}{}", a.held_from, if note.is_empty() { String::new() } else { format!(": {note}") });
}
Row {
label: label(&id),
href: artifact_link(&id),
state: a.state.clone(),
tone: state_tone(&a.state),
sha256: short(&a.sha256),
importer: if a.importer.is_empty() { "—".into() } else { a.importer.clone() },
votes: if a.state == "POLICY_CHECKED" { votes_line(a.approvals, a.approvals_required) } else { String::new() },
last: format!("{} · {}", if a.actor.is_empty() { "—" } else { &a.actor }, utc(a.at)),
note,
}
})
.collect();
DashboardView { tiles, open, rows, truncated: resp.truncated }
}
/// **Load the dashboard** from the door: the counts and the open queue's rows
/// in one call.
pub async fn load_dashboard(door: &impl IntakeDoor, queue: &str) -> Result<DashboardView, RefusalView> {
let queue = if queue.trim().is_empty() { DEFAULT_QUEUE } else { queue.trim() };
let resp = door
.list(pb::ListIntakeArtifactsRequest { queue: queue.to_string(), limit: 0 })
.await
.map_err(|r| refusal(&r))?;
Ok(dashboard(&resp, queue))
}
/// **The artifact flow as the console shows it** (lane HOL-SVG-RON): the drawing holger
/// made of its RON, the estate and the counters (`AdminService.FlowSvg`), shown as an
/// image, and the server's line about the estate under it. The console draws nothing.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Board {
/// The drawing as an image source (`data:image/svg+xml,…`). An image runs
/// no script.
pub src: String,
pub estate: String,
/// The board in words, for the image's text alternative and the line under
/// it: the server's own line (`TopologyReply.summary`), never one computed here.
pub summary: String,
/// Where the drawing is clickable (HOL-FLOW-SMART): holger's places, each laid over
/// the image as a plain link. An image follows no link inside it.
pub links: Vec<BoardLink>,
}
/// One clickable place of the drawing, as holger sent it (`FlowLink`).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BoardLink {
/// Relative, holger's: `flow?open=main`.
pub href: String,
/// What following it does, in holger's words: `unfold main`.
pub label: String,
/// Where it lies over the image: holger's shares, as CSS percentages.
pub style: String,
}
/// A share of the drawing (0..1) as a CSS percentage, without noise digits.
fn percent(share: f64) -> String {
format!("{}%", (share * 100_000.0).round() / 1000.0)
}
/// Pure: **the Flow page's zoom from its address**: `?open=` as given (empty is nothing
/// open), main when the address names none.
pub fn flow_open(query: Option<&str>) -> String {
query.unwrap_or("main").to_string()
}
/// Pure: the board from the server's two answers; `None` when holger drew nothing.
pub fn board(flow: &pb::FlowSvgReply, t: Option<&pb::TopologyReply>) -> Option<Board> {
if flow.svg.trim().is_empty() {
return None;
}
Some(Board {
src: format!("data:image/svg+xml;charset=utf-8,{}", enc(&flow.svg)),
estate: t.map(|t| t.estate.clone()).unwrap_or_default(),
summary: t.map(summary).unwrap_or_default(),
links: flow
.links
.iter()
.map(|l| BoardLink {
href: l.href.clone(),
label: l.label.clone(),
style: format!(
"left:{};top:{};width:{};height:{}",
percent(l.left),
percent(l.top),
percent(l.width),
percent(l.height)
),
})
.collect(),
})
}
/// Pure: the server's board in words — its own line (`TopologyReply.summary`: the
/// estate's boxes, upstreams, downstreams, who used it), rendered as it came (lane A13).
/// No count of the console's own (OX-HOLF): the server counts the rows it sends.
pub fn summary(t: &pb::TopologyReply) -> String {
t.summary.clone()
}
/// **Load the estate board** from the door, at the zoom `open` names. Whether it may be
/// read is the server's call; a refusal comes back as its code and words.
pub async fn load_board(door: &impl IntakeDoor, open: &str) -> Result<Option<Board>, RefusalView> {
let flow = door.flow(open).await.map_err(|r| refusal(&r))?;
// The line under it is the estate board's; without it the drawing still shows.
let topology = door.topology().await.ok();
Ok(board(&flow, topology.as_ref()))
}
// ── the security summary (lane HOL-SEC) ─────────────────────────────────────
/// One row of a section, as the server worded it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SectionRow {
pub name: String,
/// The artifact's security view, when the row is about one.
pub href: Option<String>,
pub state: String,
pub tone: Tone,
pub detail: String,
}
/// One section of the Security page: the chain, the scanners, who reads, …
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Section {
/// The server's stable key (`chain`, `scanners`, …); the markup's `data-section`.
pub key: String,
pub title: String,
pub summary: String,
pub empty: String,
pub rows: Vec<SectionRow>,
}
/// What the top of the Security page shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SummaryView {
pub headline: String,
pub sections: Vec<Section>,
}
/// Pure: the badge tone of the server's tone word.
pub fn row_tone(tone: &str) -> Tone {
match tone {
"good" => Tone::Good,
"warn" => Tone::Warn,
"bad" => Tone::Bad,
_ => Tone::Neutral,
}
}
/// Pure: the Security page's sections, as the server sent them, in its order.
pub fn security_view(r: &pb::SecuritySummaryReply) -> SummaryView {
let sections = r
.sections
.iter()
.map(|s| Section {
key: s.key.clone(),
title: s.title.clone(),
summary: s.summary.clone(),
empty: s.empty.clone(),
rows: s
.rows
.iter()
.map(|row| SectionRow {
name: row.name.clone(),
href: row.artifact.as_ref().map(artifact_link),
state: row.state.clone(),
tone: row_tone(&row.tone),
detail: row.detail.clone(),
})
.collect(),
})
.collect();
SummaryView { headline: r.headline.clone(), sections }
}
/// **Load the Security page's summary** from the door. Who may read it is the server's call.
pub async fn load_security(door: &impl IntakeDoor) -> Result<SummaryView, RefusalView> {
door.security().await.map(|r| security_view(&r)).map_err(|r| refusal(&r))
}
// ── the artifact view ───────────────────────────────────────────────────────
/// One line of a pane: a label and the server's value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Line {
pub label: String,
pub value: String,
}
/// One scanner's evidence, as a pane lists it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScannerView {
pub scanner: String,
pub verdict: String,
pub tone: Tone,
pub detail: String,
/// "required", "not installed", both or neither — the server's flags in words.
pub flags: Vec<&'static str>,
pub findings: Vec<Line>,
}
/// One pane of the artifact view.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pane {
pub title: &'static str,
/// The pane's verdict: the server's, or empty when the pane has none.
pub verdict: String,
pub tone: Tone,
pub lines: Vec<Line>,
pub scanners: Vec<ScannerView>,
/// What the pane says when the server sent nothing for it.
pub empty: String,
}
/// The SBOM document the server holds for the artifact, for a download link.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SbomFile {
pub file_name: String,
pub json: String,
/// One line about it: the scanner's sentence and the component count.
pub summary: String,
}
/// One button. The four are the same for every artifact and every visitor.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Action {
pub label: &'static str,
pub verb: pb::IntakeVerb,
/// The state the press asks for (`APPROVED` for Approve), or empty.
pub to_state: &'static str,
/// The CSS class of the button.
pub class: &'static str,
}
/// The four buttons. The server decides whether a press is allowed.
pub const ACTIONS: [Action; 4] = [
Action { label: "Approve", verb: pb::IntakeVerb::Advance, to_state: "APPROVED", class: "" },
Action { label: "Quarantine", verb: pb::IntakeVerb::Quarantine, to_state: "", class: "quiet" },
Action { label: "Release", verb: pb::IntakeVerb::Release, to_state: "", class: "quiet" },
Action { label: "Block", verb: pb::IntakeVerb::Block, to_state: "", class: "danger" },
];
/// One step of the artifact's intake history.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Step {
pub when: String,
pub hop: String,
pub actor: String,
pub policy: String,
pub reason: String,
pub evidence: Vec<Line>,
}
/// The audit trail as the artifact view shows it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrailView {
/// The server's words on the hash chain, and its verdict.
pub chain: String,
pub intact: bool,
pub entries: Vec<TrailLine>,
}
/// One entry of the audit trail.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrailLine {
pub seq: u64,
pub when: String,
pub kind: String,
pub who: String,
pub repo: String,
pub detail: String,
pub hash: String,
}
/// Pure: the trail from the server's answer.
pub fn trail(t: &pb::GetArtifactTrailResponse) -> TrailView {
TrailView {
chain: t.chain_summary.clone(),
intact: t.chain_intact,
entries: t
.entries
.iter()
.map(|e| TrailLine {
seq: e.seq,
when: utc(e.ts_nanos.div_euclid(1_000_000_000)),
kind: e.kind.clone(),
who: e.ident.clone(),
repo: e.repo.clone(),
detail: e.detail.clone(),
hash: short(&e.hash),
})
.collect(),
}
}
/// What the artifact view shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtifactView {
pub label: String,
pub state: String,
pub tone: Tone,
pub importer: String,
pub votes: String,
/// Whose votes the server recorded short of the quorum.
pub voters: Vec<String>,
pub panes: Vec<Pane>,
pub sbom: Option<SbomFile>,
pub history: Vec<Step>,
pub actions: Vec<Action>,
/// The evidence read's refusal, when the server refused only that.
pub evidence_refused: Option<RefusalView>,
/// The audit trail, or the server's refusal of it.
pub trail: Option<Result<TrailView, RefusalView>>,
}
/// The panes, in order: `(title, the evidence kinds it shows)`. Integrity and
/// Policy are built from the record itself; every scanner kind the server sends
/// lands in exactly one pane, an unknown one in "Other checks".
const KIND_PANES: [(&str, &[&str]); 6] = [
("Malware", &["malware"]),
("Vulnerabilities", &["vulnerability"]),
("License", &["license"]),
("Provenance", &["provenance"]),
("SBOM", &["sbom"]),
("Other checks", &["inspection", "secrets"]),
];
fn scanner_view(r: &pb::EvidenceRecord) -> ScannerView {
let mut flags = Vec::new();
if r.required {
flags.push("required");
}
if r.not_configured {
flags.push("not installed");
}
ScannerView {
scanner: if r.scanner_version.is_empty() { r.scanner_id.clone() } else { format!("{} {}", r.scanner_id, r.scanner_version) },
verdict: r.verdict.clone(),
tone: verdict_tone(&r.verdict),
detail: r.detail.clone(),
flags,
findings: r
.findings
.iter()
.map(|f| Line { label: format!("{} · {}", f.id, f.severity), value: f.summary.clone() })
.collect(),
}
}
/// The verdict a pane shows: the worst of its records, in the server's words.
fn pane_verdict(records: &[&pb::EvidenceRecord]) -> String {
const ORDER: [&str; 4] = ["block", "error", "warn", "pass"];
ORDER
.iter()
.find(|v| records.iter().any(|r| r.verdict == **v))
.map(|v| v.to_string())
.or_else(|| records.first().map(|r| r.verdict.clone()))
.unwrap_or_default()
}
fn sbom_file(ev: &pb::GetArtifactEvidenceResponse) -> Option<SbomFile> {
let r = ev.records.iter().find(|r| r.kind == "sbom" && !r.document_json.is_empty())?;
let components = serde_json::from_str::<serde_json::Value>(&r.document_json)
.ok()
.and_then(|v| v.get("components").and_then(|c| c.as_array()).map(|c| c.len()));
let stem = ev.file_name.rsplit('/').next().filter(|s| !s.is_empty()).unwrap_or("artifact");
let mut summary = r.detail.clone();
if let Some(n) = components {
summary = format!("{summary}{}{n} component{}", if summary.is_empty() { "" } else { " · " }, if n == 1 { "" } else { "s" });
}
Some(SbomFile { file_name: format!("{stem}.cdx.json"), json: r.document_json.clone(), summary })
}
/// Pure: the panes from the server's evidence answer.
pub fn panes(ev: &pb::GetArtifactEvidenceResponse) -> Vec<Pane> {
let not_scanned = "no scan has recorded evidence for these bytes yet".to_string();
let mut out = Vec::new();
let mut integrity = vec![Line { label: "sha256 at ingest".into(), value: ev.sha256.clone() }];
integrity.extend(ev.hashes.iter().map(|h| Line { label: format!("{} scanned", h.alg), value: h.hex.clone() }));
if ev.scanned {
integrity.push(Line { label: "file".into(), value: ev.file_name.clone() });
integrity.push(Line { label: "size".into(), value: crate::util::fmt_bytes(ev.size as i64) });
integrity.push(Line { label: "ecosystem".into(), value: ev.ecosystem.clone() });
integrity.push(Line { label: "scanned".into(), value: utc(ev.scanned_at) });
}
out.push(Pane { title: "Integrity", verdict: String::new(), tone: Tone::Neutral, lines: integrity, scanners: vec![], empty: String::new() });
let known: Vec<&str> = KIND_PANES.iter().flat_map(|(_, k)| k.iter().copied()).collect();
for (title, kinds) in KIND_PANES {
let records: Vec<&pb::EvidenceRecord> = ev
.records
.iter()
.filter(|r| kinds.contains(&r.kind.as_str()) || (title == "Other checks" && !known.contains(&r.kind.as_str())))
.collect();
if title == "Other checks" && records.is_empty() {
continue;
}
let verdict = pane_verdict(&records);
let mut lines = Vec::new();
if title == "SBOM" {
if let Some(f) = sbom_file(ev) {
lines.push(Line { label: "summary".into(), value: f.summary });
}
}
out.push(Pane {
title,
tone: verdict_tone(&verdict),
verdict,
lines,
scanners: records.iter().map(|r| scanner_view(r)).collect(),
empty: if ev.scanned { "the server sent no evidence of this kind".into() } else { not_scanned.clone() },
});
}
let mut policy = Vec::new();
if !ev.policy.is_empty() {
policy.push(Line { label: "last transition".into(), value: ev.policy.clone() });
}
policy.extend(ev.reasons.iter().map(|r| Line { label: "reason".into(), value: r.clone() }));
out.push(Pane {
title: "Policy",
verdict: ev.verdict.clone(),
tone: verdict_tone(&ev.verdict),
lines: policy,
scanners: vec![],
empty: if ev.scanned { "the policy recorded no words".into() } else { not_scanned },
});
out
}
/// Pure: the history from the server's state answer.
pub fn history(st: &pb::GetArtifactStateResponse) -> Vec<Step> {
st.history
.iter()
.map(|t| Step {
when: utc(t.at),
hop: format!("{} → {}", t.from_state, t.to_state),
actor: t.actor.clone(),
policy: t.policy.clone(),
reason: t.reason.clone(),
evidence: t
.evidence
.iter()
.map(|e| Line { label: format!("{} · {}", e.kind, e.verdict), value: format!("{} {}", e.source, e.reference).trim().to_string() })
.collect(),
})
.collect()
}
/// Pure: the artifact view from the server's two answers. The evidence read may
/// be refused on its own; the state read may not (the page then shows that
/// refusal instead).
pub fn artifact(id: &pb::ArtifactId, st: &pb::GetArtifactStateResponse, ev: Result<&pb::GetArtifactEvidenceResponse, &Refused>) -> ArtifactView {
let (panes, sbom, evidence_refused) = match ev {
Ok(ev) => (panes(ev), sbom_file(ev), None),
Err(r) => (Vec::new(), None, Some(refusal(r))),
};
ArtifactView {
label: label(id),
state: st.state.clone(),
tone: state_tone(&st.state),
importer: if st.importer.is_empty() { "—".into() } else { st.importer.clone() },
votes: votes_line(st.approvals, st.approvals_required),
voters: st.pending_approvals.iter().map(|v| v.source.clone()).collect(),
panes,
sbom,
history: history(st),
actions: ACTIONS.to_vec(),
evidence_refused,
trail: None,
}
}
/// **Load the artifact view** from the door: its state, then its evidence.
pub async fn load_artifact(door: &impl IntakeDoor, id: &pb::ArtifactId) -> Result<ArtifactView, RefusalView> {
let st = door
.state(pb::GetArtifactStateRequest { artifact: Some(id.clone()) })
.await
.map_err(|r| refusal(&r))?;
let ev = door.evidence(pb::GetArtifactEvidenceRequest { artifact: Some(id.clone()) }).await;
let mut v = artifact(id, &st, ev.as_ref());
if !st.sha256.is_empty() {
let t = door.trail(pb::GetArtifactTrailRequest { sha256: st.sha256.clone() }).await;
v.trail = Some(t.map(|t| trail(&t)).map_err(|r| refusal(&r)));
}
Ok(v)
}
/// What a press shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Pressed {
/// The server moved it, or recorded a vote: its sentence and its vote count.
Done { message: String, state: String, votes: String },
/// The server refused: its code and its words.
Refused(RefusalView),
}
/// **Press one button**: one `AdvanceArtifact`, and the server's answer.
pub async fn press(door: &impl IntakeDoor, id: &pb::ArtifactId, action: Action, reason: &str) -> Pressed {
let req = pb::AdvanceArtifactRequest {
artifact: Some(id.clone()),
verb: action.verb as i32,
to_state: action.to_state.to_string(),
reason: reason.trim().to_string(),
evidence: Vec::new(),
};
match door.advance(req).await {
Ok(a) => Pressed::Done { message: a.message, state: a.state, votes: votes_line(a.approvals, a.approvals_required) },
Err(r) => Pressed::Refused(refusal(&r)),
}
}
/// **The wasm door**: holger's PromotionService over gRPC-web.
#[cfg(target_arch = "wasm32")]
pub struct GrpcDoor;
#[cfg(target_arch = "wasm32")]
fn refused(s: tonic::Status) -> Refused {
Refused { code: format!("{:?}", s.code()), message: crate::grpc::status_text(&s) }
}
#[cfg(target_arch = "wasm32")]
impl IntakeDoor for GrpcDoor {
async fn list(&self, req: pb::ListIntakeArtifactsRequest) -> Result<pb::ListIntakeArtifactsResponse, Refused> {
crate::grpc::promotion().list_intake_artifacts(req).await.map(|r| r.into_inner()).map_err(refused)
}
async fn state(&self, req: pb::GetArtifactStateRequest) -> Result<pb::GetArtifactStateResponse, Refused> {
crate::grpc::promotion().get_artifact_state(req).await.map(|r| r.into_inner()).map_err(refused)
}
async fn evidence(&self, req: pb::GetArtifactEvidenceRequest) -> Result<pb::GetArtifactEvidenceResponse, Refused> {
crate::grpc::promotion().get_artifact_evidence(req).await.map(|r| r.into_inner()).map_err(refused)
}
async fn advance(&self, req: pb::AdvanceArtifactRequest) -> Result<pb::AdvanceArtifactResponse, Refused> {
crate::grpc::promotion().advance_artifact(req).await.map(|r| r.into_inner()).map_err(refused)
}
async fn topology(&self) -> Result<pb::TopologyReply, Refused> {
crate::grpc::admin().topology(pb::TopologyRequest {}).await.map(|r| r.into_inner()).map_err(refused)
}
async fn flow(&self, open: &str) -> Result<pb::FlowSvgReply, Refused> {
crate::grpc::admin().flow_svg(pb::FlowSvgRequest { open: open.to_string() }).await.map(|r| r.into_inner()).map_err(refused)
}
async fn trail(&self, req: pb::GetArtifactTrailRequest) -> Result<pb::GetArtifactTrailResponse, Refused> {
crate::grpc::audit().get_artifact_trail(req).await.map(|r| r.into_inner()).map_err(refused)
}
async fn security(&self) -> Result<pb::SecuritySummaryReply, Refused> {
crate::grpc::admin().security_summary(pb::SecuritySummaryRequest {}).await.map(|r| r.into_inner()).map_err(refused)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
/// Run a future that never waits on anything outside itself (the mock
/// server answers at once).
fn block_on<F: std::future::Future>(f: F) -> F::Output {
let mut f = std::pin::pin!(f);
let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
loop {
if let std::task::Poll::Ready(v) = f.as_mut().poll(&mut cx) {
return v;
}
}
}
/// **The mock server**: canned answers, and every request it was sent.
#[derive(Default)]
struct Mock {
list: Option<Result<pb::ListIntakeArtifactsResponse, Refused>>,
state: Option<Result<pb::GetArtifactStateResponse, Refused>>,
evidence: Option<Result<pb::GetArtifactEvidenceResponse, Refused>>,
advance: Option<Result<pb::AdvanceArtifactResponse, Refused>>,
sent_list: RefCell<Vec<pb::ListIntakeArtifactsRequest>>,
sent_advance: RefCell<Vec<pb::AdvanceArtifactRequest>>,
topology: Option<Result<pb::TopologyReply, Refused>>,
flow: Option<Result<pb::FlowSvgReply, Refused>>,
sent_flow: RefCell<Vec<String>>,
trail: Option<Result<pb::GetArtifactTrailResponse, Refused>>,
sent_trail: RefCell<Vec<pb::GetArtifactTrailRequest>>,
security: Option<Result<pb::SecuritySummaryReply, Refused>>,
}
fn unanswered<T>() -> Result<T, Refused> {
Err(Refused { code: "Unimplemented".into(), message: "the mock was not given this answer".into() })
}
impl IntakeDoor for Mock {
async fn list(&self, req: pb::ListIntakeArtifactsRequest) -> Result<pb::ListIntakeArtifactsResponse, Refused> {
self.sent_list.borrow_mut().push(req);
self.list.clone().unwrap_or_else(unanswered)
}
async fn state(&self, _req: pb::GetArtifactStateRequest) -> Result<pb::GetArtifactStateResponse, Refused> {
self.state.clone().unwrap_or_else(unanswered)
}
async fn evidence(&self, _req: pb::GetArtifactEvidenceRequest) -> Result<pb::GetArtifactEvidenceResponse, Refused> {
self.evidence.clone().unwrap_or_else(unanswered)
}
async fn advance(&self, req: pb::AdvanceArtifactRequest) -> Result<pb::AdvanceArtifactResponse, Refused> {
self.sent_advance.borrow_mut().push(req);
self.advance.clone().unwrap_or_else(unanswered)
}
async fn topology(&self) -> Result<pb::TopologyReply, Refused> {
self.topology.clone().unwrap_or_else(unanswered)
}
async fn flow(&self, open: &str) -> Result<pb::FlowSvgReply, Refused> {
self.sent_flow.borrow_mut().push(open.to_string());
self.flow.clone().unwrap_or_else(unanswered)
}
async fn trail(&self, req: pb::GetArtifactTrailRequest) -> Result<pb::GetArtifactTrailResponse, Refused> {
self.sent_trail.borrow_mut().push(req);
self.trail.clone().unwrap_or_else(unanswered)
}
async fn security(&self) -> Result<pb::SecuritySummaryReply, Refused> {
self.security.clone().unwrap_or_else(unanswered)
}
}
/// ★ **The smart flow's zoom and its clickable places are holger's** (HOL-FLOW-SMART):
/// the console asks for the machines the page names (`open`) and lays a plain link over
/// each place holger says is clickable, where holger says it is, as holger worded it.
/// The image still runs nothing; the console computes no layout.
#[test]
fn the_board_asks_holger_for_the_zoom_and_lays_its_links_over_the_drawing() {
let mock = Mock {
flow: Some(Ok(pb::FlowSvgReply {
svg: "<svg/>".into(),
graph_json: "{}".into(),
links: vec![
pb::FlowLink { href: "flow?open=main".into(), label: "unfold main".into(), left: 0.25, top: 0.5, width: 0.125, height: 0.2 },
pb::FlowLink { href: "flow?open=all".into(), label: "show the full RON".into(), left: 0.9, top: 0.0, width: 0.1, height: 0.02 },
],
})),
..Default::default()
};
let b = block_on(load_board(&mock, "main,lab-1")).unwrap().unwrap();
assert_eq!(mock.sent_flow.borrow().as_slice(), ["main,lab-1"], "the page's zoom, as it named it");
assert_eq!(b.links.len(), 2);
assert_eq!(b.links[0].href, "flow?open=main");
assert_eq!(b.links[0].label, "unfold main");
assert_eq!(b.links[0].style, "left:25%;top:50%;width:12.5%;height:20%", "where holger says, as a share of the drawing");
assert_eq!(b.links[1].href, "flow?open=all");
block_on(load_board(&mock, "")).unwrap();
assert_eq!(mock.sent_flow.borrow().last().map(String::as_str), Some(""), "nothing open: level 1");
}
/// The Flow page's zoom from its address: `?open=` as given (empty is nothing open),
/// main when it names none.
#[test]
fn the_flow_page_asks_for_what_its_address_names_and_main_by_default() {
assert_eq!(flow_open(None), "main");
assert_eq!(flow_open(Some("")), "");
assert_eq!(flow_open(Some("main,lab-1")), "main,lab-1");
assert_eq!(flow_open(Some("all")), "all");
}
// ── the security summary (lane HOL-SEC) ─────────────────────────────────
#[test]
fn the_security_summary_is_the_servers_sections_as_sent() {
let held = pb::SecurityRow {
name: "evil 1.0".into(),
state: "quarantined".into(),
tone: "warn".into(),
detail: "invalidated upstream: malware. By security.".into(),
artifact: Some(pb::ArtifactId { namespace: String::new(), name: "evil.whl".into(), version: "1.0".into() }),
};
let reply = pb::SecuritySummaryReply {
headline: "Holger checks every package before it serves it.".into(),
sections: vec![
pb::SecuritySection { key: "chain".into(), title: "Intake chain".into(), summary: "Every package passes these steps.".into(), empty: String::new(), rows: vec![] },
pb::SecuritySection { key: "held".into(), title: "Held or refused".into(), summary: String::new(), empty: "Nothing is held.".into(), rows: vec![held] },
],
};
let mock = Mock { security: Some(Ok(reply)), ..Default::default() };
let v = block_on(load_security(&mock)).expect("the summary");
assert_eq!(v.headline, "Holger checks every package before it serves it.");
let keys: Vec<&str> = v.sections.iter().map(|s| s.key.as_str()).collect();
assert_eq!(keys, ["chain", "held"], "the server's order");
let r = &v.sections[1].rows[0];
assert_eq!((r.state.as_str(), r.tone), ("quarantined", Tone::Warn));
assert_eq!(r.href.as_deref(), Some("/security/artifact?ns=&name=evil.whl&version=1.0"));
assert_eq!(r.detail, "invalidated upstream: malware. By security.");
}
#[test]
fn a_refused_security_summary_shows_the_servers_code_and_words() {
let mock = Mock {
security: Some(Err(Refused { code: "PermissionDenied".into(), message: "[holger-auth-2] 'dev' may not read the Security page".into() })),
..Default::default()
};
let r = block_on(load_security(&mock)).unwrap_err();
assert_eq!((r.code.as_str(), r.text.as_str()), ("holger-auth-2", "'dev' may not read the Security page"));
}
fn queue(name: &str, title: &str, count: u64) -> pb::IntakeQueue {
pb::IntakeQueue { name: name.into(), title: title.into(), states: vec![], count }
}
fn wheel() -> pb::ArtifactId {
pb::ArtifactId { namespace: String::new(), name: "demo_pkg-1.0.0-py3-none-any.whl".into(), version: String::new() }
}
fn listing() -> pb::ListIntakeArtifactsResponse {
pb::ListIntakeArtifactsResponse {
queues: vec![
queue("incoming", "Incoming", 3),
queue("quarantined", "Quarantined", 1),
queue("awaiting-approval", "Awaiting approval", 2),
queue("blocked", "Blocked", 0),
queue("available", "Approved / available", 7),
],
artifacts: vec![
pb::IntakeArtifactRow {
artifact: Some(wheel()),
state: "POLICY_CHECKED".into(),
sha256: "ab".repeat(32),
repository: "quarantine".into(),
actor: "ida".into(),
at: 1_800_000_000,
importer: "ida".into(),
approvals: 1,
approvals_required: 2,
..Default::default()
},
pb::IntakeArtifactRow {
artifact: Some(pb::ArtifactId { namespace: "acme".into(), name: "tool".into(), version: "2.0".into() }),
state: "POLICY_CHECKED".into(),
approvals: 0,
approvals_required: 2,
..Default::default()
},
],
truncated: false,
}
}
fn evidence() -> pb::GetArtifactEvidenceResponse {
let rec = |kind: &str, verdict: &str| pb::EvidenceRecord {
scanner_id: format!("{kind}-scanner"),
scanner_version: "1".into(),
kind: kind.into(),
verdict: verdict.into(),
detail: format!("{kind} said {verdict}"),
..Default::default()
};
let mut vuln = rec("vulnerability", "warn");
vuln.findings = vec![pb::EvidenceFinding { id: "PYSEC-1".into(), severity: "low".into(), summary: "a low one".into() }];
let mut malware = rec("malware", "error");
malware.scanner_id = "not-configured".into();
malware.scanner_version = "0".into();
malware.not_configured = true;
malware.required = true;
let mut sbom = rec("sbom", "pass");
sbom.detail = "CycloneDX SBOM for demo-pkg 1.0.0".into();
sbom.document_json = r#"{"bomFormat":"CycloneDX","components":[{"name":"demo-pkg"}]}"#.into();
pb::GetArtifactEvidenceResponse {
state: "POLICY_CHECKED".into(),
sha256: "ab".repeat(32),
scanned: true,
file_name: "demo_pkg-1.0.0-py3-none-any.whl".into(),
ecosystem: "pip".into(),
size: 2_048,
hashes: vec![
pb::EvidenceHash { alg: "sha256".into(), hex: "ab".repeat(32) },
pb::EvidenceHash { alg: "blake3".into(), hex: "cd".repeat(32) },
],
verdict: "block".into(),
reasons: vec!["malware: required, not configured".into()],
records: vec![rec("inspection", "pass"), malware, vuln, rec("license", "pass"), rec("provenance", "error"), sbom],
scanned_at: 1_800_000_000,
policy: "structural check only".into(),
// W5's stored records, signer and SBOM link (holger 0cb5f11f): not shown here.
..Default::default()
}
}
fn state() -> pb::GetArtifactStateResponse {
pb::GetArtifactStateResponse {
state: "POLICY_CHECKED".into(),
sha256: "ab".repeat(32),
importer: "ida".into(),
approvals: 1,
approvals_required: 2,
history: vec![pb::IntakeTransition {
from_state: "UNTRUSTED".into(),
to_state: "INGESTED".into(),
actor: "ida".into(),
at: 1_800_000_000,
..Default::default()
}],
pending_approvals: vec![pb::IntakeEvidence { kind: "approval".into(), source: "bob".into(), verdict: "pass".into(), reference: "sha256:x".into() }],
// holger f86b9627 (tier 1): the artifact's source and its effective policy; not shown here.
source: String::new(),
effective_policy: String::new(),
}
}
/// ★ **The dashboard renders the server's five queues and counts**, and the
/// rows of the open queue with the server's vote count.
#[test]
fn the_dashboard_renders_the_servers_queues_counts_and_rows() {
let mock = Mock { list: Some(Ok(listing())), ..Default::default() };
let v = block_on(load_dashboard(&mock, "")).expect("the mock answers");
assert_eq!(mock.sent_list.borrow()[0].queue, DEFAULT_QUEUE, "a bare dashboard opens awaiting approval");
let tiles: Vec<(&str, u64, bool)> = v.tiles.iter().map(|t| (t.title.as_str(), t.count, t.active)).collect();
assert_eq!(
tiles,
vec![
("Incoming", 3, false),
("Quarantined", 1, false),
("Awaiting approval", 2, true),
("Blocked", 0, false),
("Approved / available", 7, false),
]
);
assert_eq!(v.tiles[1].href, "/security?queue=quarantined");
assert_eq!(v.open, "Awaiting approval");
assert_eq!(v.rows.len(), 2);
let r = &v.rows[0];
assert_eq!((r.label.as_str(), r.state.as_str(), r.tone), ("demo_pkg-1.0.0-py3-none-any.whl", "POLICY_CHECKED", Tone::Warn));
assert_eq!(r.votes, "1 of 2 approvals");
assert_eq!(r.sha256, "abababababab");
assert_eq!(r.last, "ida · 2027-01-15 08:00 UTC");
assert_eq!(r.href, "/security/artifact?ns=&name=demo_pkg-1.0.0-py3-none-any.whl&version=");
assert_eq!(v.rows[1].label, "acme/tool 2.0");
assert_eq!(v.rows[1].votes, "0 of 2 approvals");
// Another queue is asked for by its server name.
block_on(load_dashboard(&mock, "blocked")).unwrap();
assert_eq!(mock.sent_list.borrow()[1].queue, "blocked");
}
#[test]
fn a_dashboard_the_server_refuses_shows_its_code_and_words() {
let mock = Mock {
list: Some(Err(Refused { code: "FailedPrecondition".into(), message: "this server declares no intake chain".into() })),
..Default::default()
};
let e = block_on(load_dashboard(&mock, "incoming")).expect_err("refused");
assert_eq!(e, RefusalView { code: "FailedPrecondition".into(), text: "this server declares no intake chain".into() });
}
/// ★ **The artifact view renders every pane from the server's evidence**:
/// Integrity (the hashes), Malware, Vulnerabilities, License, Provenance,
/// SBOM (a download and a summary), Policy, and the vote count.
#[test]
fn the_artifact_view_renders_integrity_scans_sbom_policy_and_votes() {
let mock = Mock { state: Some(Ok(state())), evidence: Some(Ok(evidence())), ..Default::default() };
let v = block_on(load_artifact(&mock, &wheel())).expect("the mock answers");
let titles: Vec<&str> = v.panes.iter().map(|p| p.title).collect();
assert_eq!(titles, ["Integrity", "Malware", "Vulnerabilities", "License", "Provenance", "SBOM", "Other checks", "Policy"]);
let pane = |t: &str| v.panes.iter().find(|p| p.title == t).unwrap();
let integrity: Vec<(&str, &str)> = pane("Integrity").lines.iter().map(|l| (l.label.as_str(), l.value.as_str())).collect();
assert_eq!(integrity[1], ("sha256 scanned", "ab".repeat(32).as_str()));
assert_eq!(integrity[2], ("blake3 scanned", "cd".repeat(32).as_str()));
assert!(integrity.contains(&("size", "2.0 kB")), "{integrity:?}");
let malware = pane("Malware");
assert_eq!((malware.verdict.as_str(), malware.tone), ("error", Tone::Bad));
assert_eq!(malware.scanners[0].flags, vec!["required", "not installed"]);
let vulns = pane("Vulnerabilities");
assert_eq!((vulns.verdict.as_str(), vulns.tone), ("warn", Tone::Warn));
assert_eq!(vulns.scanners[0].findings, vec![Line { label: "PYSEC-1 · low".into(), value: "a low one".into() }]);
assert_eq!(pane("License").tone, Tone::Good);
assert_eq!(pane("Provenance").verdict, "error");
assert_eq!(pane("SBOM").lines[0].value, "CycloneDX SBOM for demo-pkg 1.0.0 · 1 component");
let sbom = v.sbom.as_ref().expect("an SBOM to download");
assert_eq!(sbom.file_name, "demo_pkg-1.0.0-py3-none-any.whl.cdx.json");
assert!(sbom.json.contains("CycloneDX"));
let policy = pane("Policy");
assert_eq!((policy.verdict.as_str(), policy.tone), ("block", Tone::Bad));
assert!(policy.lines.iter().any(|l| l.value == "malware: required, not configured"));
// The two-person approval: the server's count, and whose vote it holds.
assert_eq!(v.votes, "1 of 2 approvals");
assert_eq!(v.voters, vec!["bob"]);
assert_eq!(v.history[0].hop, "UNTRUSTED → INGESTED");
assert_eq!((v.state.as_str(), v.importer.as_str()), ("POLICY_CHECKED", "ida"));
}
/// ★ **The trail is the server's**: asked by the bytes' sha256, shown with
/// the chain's verdict; a reader the server refuses sees its refusal.
#[test]
fn the_trail_is_asked_by_sha256_and_shown_or_refused() {
let mock = Mock {
state: Some(Ok(state())),
evidence: Some(Ok(evidence())),
trail: Some(Ok(pb::GetArtifactTrailResponse {
sha256: "ab".repeat(32),
entries: vec![pb::ArtifactTrailEntry {
seq: 7,
hash: "f0".repeat(32),
ts_nanos: 1_800_000_000_000_000_000,
kind: "imported".into(),
ident: "ida".into(),
repo: "incoming".into(),
detail: "intake UNTRUSTED → INGESTED".into(),
..Default::default()
}],
chain_intact: true,
chain_summary: "12 records, chain intact".into(),
})),
..Default::default()
};
let v = block_on(load_artifact(&mock, &wheel())).unwrap();
assert_eq!(mock.sent_trail.borrow()[0].sha256, "ab".repeat(32));
let t = v.trail.expect("asked").expect("answered");
assert!(t.intact);
assert_eq!(t.chain, "12 records, chain intact");
let e = &t.entries[0];
assert_eq!((e.seq, e.when.as_str(), e.kind.as_str(), e.who.as_str(), e.hash.as_str()), (7, "2027-01-15 08:00 UTC", "imported", "ida", "f0f0f0f0f0f0"));
let refused = Mock {
state: Some(Ok(state())),
evidence: Some(Ok(evidence())),
trail: Some(Err(Refused { code: "PermissionDenied".into(), message: "[holger-audit-10] only an Auditor or an Admin reads the trail".into() })),
..Default::default()
};
let v = block_on(load_artifact(&refused, &wheel())).unwrap();
assert_eq!(v.trail.unwrap().unwrap_err().code, "holger-audit-10");
}
#[test]
fn an_artifact_not_scanned_yet_says_so_and_a_refused_evidence_read_shows_the_refusal() {
let mut ev = pb::GetArtifactEvidenceResponse { state: "INGESTED".into(), sha256: "ef".repeat(32), ..Default::default() };
ev.scanned = false;
let v = artifact(&wheel(), &state(), Ok(&ev));
let malware = v.panes.iter().find(|p| p.title == "Malware").unwrap();
assert!(malware.scanners.is_empty());
assert_eq!(malware.empty, "no scan has recorded evidence for these bytes yet");
assert!(v.sbom.is_none());
let r = Refused { code: "PermissionDenied".into(), message: "[holger-duty-6] Developer reads only AVAILABLE".into() };
let v = artifact(&wheel(), &state(), Err(&r));
assert!(v.panes.is_empty());
assert_eq!(v.evidence_refused.unwrap().code, "holger-duty-6");
}
/// ★ **A refusal from the server is shown, code and words**, and the press
/// sent exactly what the button says: no client-side decision in between.
#[test]
fn a_press_the_server_refuses_shows_the_servers_code_and_words() {
let mock = Mock {
advance: Some(Err(Refused {
code: "PermissionDenied".into(),
message: "[holger-duty-1] ida imported demo_pkg-1.0.0-py3-none-any.whl@ and may never approve it".into(),
})),
..Default::default()
};
let got = block_on(press(&mock, &wheel(), ACTIONS[0], " looks fine "));
assert_eq!(
got,
Pressed::Refused(RefusalView {
code: "holger-duty-1".into(),
text: "ida imported demo_pkg-1.0.0-py3-none-any.whl@ and may never approve it".into(),
})
);
let sent = mock.sent_advance.borrow();
assert_eq!(sent.len(), 1, "one press, one call");
assert_eq!((sent[0].verb, sent[0].to_state.as_str(), sent[0].reason.as_str()), (pb::IntakeVerb::Advance as i32, "APPROVED", "looks fine"));
assert!(sent[0].evidence.is_empty(), "the console never supplies evidence");
}
#[test]
fn a_vote_short_of_the_quorum_shows_the_servers_count() {
let mock = Mock {
advance: Some(Ok(pb::AdvanceArtifactResponse {
from_state: "POLICY_CHECKED".into(),
state: "POLICY_CHECKED".into(),
approvals: 1,
approvals_required: 2,
message: "approval 1 of 2 recorded".into(),
..Default::default()
})),
..Default::default()
};
assert_eq!(
block_on(press(&mock, &wheel(), ACTIONS[0], "")),
Pressed::Done { message: "approval 1 of 2 recorded".into(), state: "POLICY_CHECKED".into(), votes: "1 of 2 approvals".into() }
);
let verbs: Vec<(&str, pb::IntakeVerb)> = ACTIONS.iter().map(|a| (a.label, a.verb)).collect();
assert_eq!(
verbs,
vec![
("Approve", pb::IntakeVerb::Advance),
("Quarantine", pb::IntakeVerb::Quarantine),
("Release", pb::IntakeVerb::Release),
("Block", pb::IntakeVerb::Block),
]
);
}
/// ★ **No client-side role logic.** Every artifact, in every state, gets the
/// same four buttons; and neither the module nor its page reads who the
/// visitor is to decide anything.
#[test]
fn there_is_no_client_side_role_logic() {
for s in ["INGESTED", "SCANNED", "POLICY_CHECKED", "APPROVED", "AVAILABLE", "QUARANTINED", "BLOCKED"] {
let st = pb::GetArtifactStateResponse { state: s.into(), ..state() };
assert_eq!(artifact(&wheel(), &st, Ok(&evidence())).actions, ACTIONS.to_vec(), "{s}");
}
let code = |src: &'static str| src.split("#[cfg(test)]").next().unwrap();
for (file, src) in [
("src/security.rs", code(include_str!("security.rs"))),
("src/pages/security.rs", code(include_str!("pages/security.rs"))),
] {
// An ARIA `role="alert"` attribute is markup, not a role check.
let lower = src.to_lowercase().replace("role=\"", "aria=\"");
for word in ["role", "duty", "duties", "approver", "is_admin", "admin_visible", "who_am_i", "session", "token"] {
assert!(!lower.contains(word), "{file} mentions `{word}`: the server decides, the console renders");
}
}
}
/// ★ **The board is holger's flow drawing, shown as sent** (lane HOL-SVG-RON, owner
/// 2026-10-30: the SVG is the RON's graph, drawn by the server from its RON; the console
/// draws nothing): the image is `AdminService.FlowSvg`'s bytes, every one escaped; the
/// line under it is the estate board's; a refusal shows the server's code and words.
#[test]
fn the_board_is_holgers_flow_drawing_shown_as_sent_and_its_refusal_is_shown() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><g data-node="source:team" data-kind="source"></g><g data-node="downstream:lab-1" data-kind="downstream"></g></svg>"#;
let mock = Mock {
flow: Some(Ok(pb::FlowSvgReply { svg: svg.into(), graph_json: "{}".into(), links: vec![] })),
topology: Some(Ok(pb::TopologyReply { estate: "acme".into(), summary: "3 boxes: front, main, twin".into(), ..Default::default() })),
..Default::default()
};
let b = block_on(load_board(&mock, "")).expect("answered").expect("a drawing");
assert_eq!(b.src, format!("data:image/svg+xml;charset=utf-8,{}", enc(svg)), "holger's bytes, as sent");
assert!(!b.src.contains('<') && !b.src.contains('"') && !b.src.contains('#'), "every byte escaped: {}", b.src);
assert!(b.src.contains("data-node%3D%22source%3Ateam%22") && b.src.contains("downstream%3Alab-1"), "the RON's nodes, from the server: {}", b.src);
assert_eq!((b.estate.as_str(), b.summary.as_str()), ("acme", "3 boxes: front, main, twin"), "the server's line under it");
// The drawing without the line: the topology refused, the drawing still shows.
let no_line = Mock { flow: Some(Ok(pb::FlowSvgReply { svg: svg.into(), graph_json: String::new(), links: vec![] })), ..Default::default() };
assert_eq!(block_on(load_board(&no_line, "")).unwrap().unwrap().summary, "");
let empty = Mock { flow: Some(Ok(pb::FlowSvgReply::default())), ..Default::default() };
assert_eq!(block_on(load_board(&empty, "")), Ok(None), "holger drew nothing: nothing is shown, never a sample");
let refused = Mock {
flow: Some(Err(Refused { code: "PermissionDenied".into(), message: "[holger-auth-3] the artifact flow is admin-read".into() })),
..Default::default()
};
assert_eq!(
block_on(load_board(&refused, "")),
Err(RefusalView { code: "holger-auth-3".into(), text: "the artifact flow is admin-read".into() })
);
}
#[test]
fn words_and_times_read_plainly() {
assert_eq!(utc(0), "—");
assert_eq!(utc(86_400 + 3_660), "1970-01-02 01:01 UTC");
assert_eq!(votes_line(2, 2), "2 of 2 approvals");
assert_eq!(votes_line(0, 1), "0 of 1 approval");
assert_eq!(state_tone("RESYNCING"), Tone::Neutral);
assert_eq!(verdict_tone("block"), Tone::Bad);
assert_eq!(
refusal(&Refused { code: "Internal".into(), message: "boom".into() }),
RefusalView { code: "Internal".into(), text: "boom".into() }
);
}
/// ★ **The board's words are the server's** (lane A13): its line, as it came. The console
/// adds no count of its own (OX-HOLF).
#[test]
fn the_board_says_what_the_server_wrote() {
let line = "3 boxes: front, main, twin · no upstream (uploads only) · no downstream · 1 person used it in 30 days";
let t = pb::TopologyReply { summary: line.into(), ..Default::default() };
let f = pb::FlowSvgReply { svg: "<svg/>".into(), graph_json: String::new(), links: vec![] };
assert_eq!(board(&f, Some(&t)).unwrap().summary, line);
}
}