From 5408b886d6c1f5b5265e144281c02705153f2177 Mon Sep 17 00:00:00 2001 From: Romamo Date: Mon, 2 Nov 2026 14:41:48 +0200 Subject: [PATCH] Propose S-010: track open CVEs from the fleet's package evidence Match each host's Debian source packages against the Debian Security Tracker and CISA KEV, alert on what stays open, and close findings with an apply-security-updates operation whose verify re-runs the match. Co-Authored-By: Claude Opus 5.5 --- docs/specs/010-cve-tracking.md | 150 +++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/specs/010-cve-tracking.md diff --git a/docs/specs/010-cve-tracking.md b/docs/specs/010-cve-tracking.md new file mode 100644 index 0000000..671070b --- /dev/null +++ b/docs/specs/010-cve-tracking.md @@ -0,0 +1,150 @@ +# S-010: Track open CVEs from the fleet's own package evidence + +status: proposed +serves: G-2 + +## Problem + +The baseline installs `unattended-upgrades` (`engine/ansible/roles/cloudfall_unattended_upgrades`), +so most Debian security fixes land on their own. Nothing tells the team whether they did: + +- An upgrade that was held back, failed, or needs a package from outside `-security` leaves the + CVE open, and nothing reports it +- A fixed library on disk does not help a service still running the old one, and a fixed kernel + does not help until the host reboots. "Package upgraded" is not "CVE closed" +- A host read from a brownfield inventory (G-4) may have no `unattended-upgrades` at all +- A managed service provider is asked by its customers "were we exposed to CVE X, and since + when". Today the answer is a manual `apt list --upgradable` on each host, with no record + +Generic CVE feeds do not answer this: they list about a hundred new CVEs a day, most for +products the fleet does not run. The fleet's own package evidence does. + +`cloudfall_inspect` already reads the package database with `dpkg-query` and writes one +snapshot per host (`config/schemas/v1/observed-server.schema.json`) with `os` (distribution, +version, release, running kernel) and `packages` (`name`, `version`, `arch`). Its +`packages[].source` is the literal string `"dpkg-query"` (the collector), not the Debian +source package. Debian tracks security status per source package and source version, so the +snapshot cannot be matched as it is. + +## Behaviour + +**Collect what matching needs** + +`cloudfall_inspect` extends its `dpkg-query` format with `${source:Package}` and +`${source:Version}`, and the snapshot schema gains two required fields per package: + +- `sourcePackage`: the Debian source package name +- `sourceVersion`: the source version, which differs from `version` for binNMUs and some + multi-binary sources + +The existing `source` field keeps its meaning (the collector). A snapshot without the new +fields is reported as not matchable, never matched by binary name. + +The role also records, read-only: + +- `rebootRequired`: whether `/run/reboot-required` exists, and the packages listed in + `/run/reboot-required.pkgs` +- `servicesNeedingRestart`: the services `needrestart -b` reports, when `needrestart` is + installed; `null`, not `[]`, when it is not, so "unknown" never reads as "none" + +**Match on the management host** + +A new read command, `cloudfall security observe` (name open), runs on the management host and +never on the nodes: + +1. It fetches the Debian Security Tracker JSON (`https://security-tracker.debian.org/tracker/data/json`) + and CISA's Known Exploited Vulnerabilities catalog, and keeps each with its fetch time and + sha256, so every finding names the exact data it came from +2. For each host snapshot and each `(sourcePackage, sourceVersion)`, it looks up the host's + release (`os.release`, e.g. `trixie`) and classifies the CVE: + - `open`: unfixed in that release, or fixed in a version newer than the installed one + - `fixed_not_active`: the installed version is fixed, but the host needs a reboot for that + package or a service using it needs a restart + - `not_affected`: the installed version is at or past the fixed version +3. It compares versions by Debian's rules (`apt_pkg.version_compare` or `python-debian`), + never as strings +4. It keeps the tracker's own triage: `urgency` and the ``, `` and + `` marks travel with the finding, so a team can tell "Debian will not ship a fix" + from "a fix exists and we have not installed it" + +Each finding carries the host, source package, installed version, fixed version (or `null`), +CVE id, state, Debian urgency, whether CISA lists it as exploited and since when, and the first +time Cloudfall saw it open on that host. That first-seen time is what answers "since when" +later, so findings are kept as history, not overwritten. + +Output follows the existing read commands: a JSON envelope for agents and a short table for +people, ordered exploited first, then by urgency, then by age. + +**Alert on what matters** + +A declared policy, shaped like `AlertRule`, turns findings into alerts: + +- an exploited CVE open on a host longer than a declared grace (default 1 day) +- a `high` urgency CVE open longer than a declared grace (default 7 days) +- a `fixed_not_active` finding older than a declared grace (default 3 days) + +`` and `` findings never alert by default; the team can opt in. + +**Close it as an operation with a verify step** + +A catalog operation, `apply-security-updates`, runs on one host: + +- Precondition: the host has open findings that a package in its configured repositories fixes +- Action: an upgrade limited to the fixing packages, not a full `dist-upgrade`, then a restart + of the services `needrestart` names. A reboot is a separate operation that always asks +- Verify: re-inspect the host and re-run the match; the operation passes only when every + finding it targeted is `not_affected`. A finding left `fixed_not_active` fails verify and + names the reboot or restart that is missing +- Risk: write. The audit entry holds the findings before, the package diff and the findings + after + +With a clear verify, it is a candidate for unattended runs once its receipt history earns it, +under the usual policy. A reboot never is. + +## Acceptance criteria + +- S-010-1: A host with a package below its fixed version shows that CVE as `open`, with the + installed and fixed versions and the tracker's urgency +- S-010-2: A host with a fixed library and a service still running the old one shows + `fixed_not_active` and names the service; without `needrestart` it says the restart state is + unknown +- S-010-3: A binNMU package (binary version `1.2-3+b1`, source version `1.2-3`) matches by + source version and is classified correctly +- S-010-4: A CVE in CISA's KEV catalog is flagged exploited, with its `dateAdded` +- S-010-5: A snapshot without `sourcePackage` is reported as not matchable and produces no + findings +- S-010-6: A CVE first seen open in one run keeps its first-seen time in later runs +- S-010-7: `apply-security-updates` fails verify, and says why, when a reboot is still needed +- S-010-8: Every finding names the tracker and KEV data it was matched against (fetch time, + sha256) + +## Out of scope + +- Packages outside dpkg: pip, npm, gem, Go modules vendored into applications, container images +- Hosts other than Debian; the inspect role already refuses them +- Firmware, network devices and the management host's own tooling +- A general CVE feed; this reports only what is installed on the fleet +- EPSS or other exploit-likelihood scores, which could rank findings later + +## Open questions + +- Command name: `cloudfall security observe`, `cloudfall cves`, or part of `cloudfall inspect` +- The tracker JSON is tens of MB: cache it on the management host with conditional requests, + and decide how stale it may be before findings say so +- Whether the baseline installs `needrestart`, so restart state is known on every host + Cloudfall builds, and how brownfield hosts without it are reported +- Whether findings live in their own history file per host or in the decision record +- The tracker's JSON fields and triage marks above are to be checked against a live copy + before this spec is approved + +## Decisions relied on + +- none + +## Issues + +- none yet + +## Verification + +Not built.