CADENCE — Architecture
The §2 mission decisions are locked. This document explains how the code realizes them.
The two halves
┌──────────────────────────────┐ signed, static feed ┌──────────────────────────────┐
│ FEED BUILDER (vendor side) │ ── manifest.yaml + .sig + files ──▶ │ ADVISORY CLIENT (customer) │
│ Stoffel-operated CLI │ (HTTPS pull OR air-gap mirror) │ appliance, source-available │
│ │ │ │
│ authors advisories (YAML) │ │ pull → verify sig → check │
│ → validate against schema │ │ file hashes → load feed │
│ → SHA-256 every file │ │ → read LOCAL inventory │
│ → write manifest │ │ → analyze → per-asset verdict│
│ → ed25519-sign the manifest │ │ → notify (email / webhook) │
└──────────────────────────────┘ └──────────────────────────────┘
holds SECRET key ships only the PUBLIC key
Data direction is one-way, outbound-from-vendor only. The client never sends inventory, topology, or versions back. The feed is a static file tree, so a customer can mirror it to an internal server and run fully air-gapped — trust is anchored in the single shipped public key.
Crate layout (Cargo workspace)
| Crate | Role | Status |
|---|---|---|
|
Framework-free core: feed schema, ed25519 sign/verify, manifest integrity, signed-feed pull seam, inventory adapter trait + 4 impls + mock, analysis engine, change-detection, notification formatting, feed linter. Most tests live here. |
DONE |
|
Vendor CLI: keygen, lint, build (hash + manifest), sign, verify, analyze. |
DONE |
|
Client-local SQLite (sqlx): reminder timestamps, last-seen versions, the notify-on-change baseline, and the tamper-evident decision audit trail. |
DONE |
|
Advisory Client appliance: pull→verify→analyze→notify, as a scheduled-refresh daemon with notify-on-change; Leptos read-only dashboard (+ optional Basic auth, freshness bar, audit badge) + a |
DONE |
domain depends on nothing framework-y. Adapters and notifiers are traits with mocks, so the whole workspace compiles and cargo tests offline with no live infrastructure.
The client as a daemon (run #3)
The appliance runs the §6 loop continuously. With CADENCE_REFRESH_INTERVAL set, a Tokio interval task re-runs source → analyze → deliver → persist each tick and hot-swaps the rendered dashboard behind an RwLock<Arc<Dashboard>> (read briefly per request, written briefly per refresh — never across an .await). A failed cycle is logged and skipped; the last good dashboard keeps serving. Notify-on-change (D012): each cycle delivers only the notifications new since the last (current − previous), persisted in SQLite so suppression survives restarts; failures are retried, not dropped. Every advisory decision is appended to a SHA-256 hash chain (D013) — tamper-evident, verified each cycle, surfaced on the dashboard and exported as JSON for auditors. Notifications go to both real channels (webhook via ureq, SMTP via lettre) behind the domain seams; the blocking network I/O runs off the async runtime via spawn_blocking.
Feed integrity model (D001 + D002)
-
A feed is a directory of YAML files:
manifest.yaml, and per-module<module>/advisories.yaml+<module>/service_intervals.yaml. -
manifest.yamlcarriesfeed_version,modules, and afiles[]list where each entry is{ path, sha256, bytes }. -
The Feed Builder writes
manifest.yaml, then ed25519-signs its exact bytes →manifest.yaml.sig(detached, base64). -
The client: a. reads
manifest.yamlbytes, verifiesmanifest.yaml.sigwith the shipped public key; b. for eachfiles[]entry, recomputes SHA-256 of the named file and compares; c. only then parses the advisory/interval YAML. Any failure rejects the whole feed (fail closed). No partial trust.
This is the Linux-repo / Sonatype-Repository-Firewall pattern: central signed intelligence, local enforcement, customer data stays local.
Analysis engine (§6)
Pure function: analyze(snapshot, feed, reminder_state, now) -> Vec<AssetVerdict> + Vec<ReminderDue>.
For each inventoried asset:
-
resolve product + installed version + facts (installed apps, Ceph version, …);
-
select advisories whose
product+versionmatch; -
evaluate optional
conditionsagainst local facts (incl. cross-product facts like "Ceph version") to pick the effective verdict; -
emit
{ asset, verdict, severity, summary, cves, blocking_requirements }; -
service-interval reminders are computed from
reminder_state(last-action timestamps) vsnow— kept as a plain input sodomainneeds no DB.
Output is advice, never action (v1). No write-back to customer systems.
Inventory adapters (§5)
trait InventoryAdapter { fn collect(&self) -> Result<InventorySnapshot, AdapterError>; } v1 real impls (Proxmox API, Nextcloud occ, NetBox, SSH/apt) each ship with the trait; a MockAdapter (in-memory) backs all offline tests. Monitoring systems (Prometheus/Grafana) are not inventory and are never the version spine.