ADR 0012: Vulnerability-severity and VEX policy
Status: accepted (2026-10-06; exception mechanism refined 2026-10-10; severity decided 2026-10-29)
Context
The first supply-chain scan of DeskOS Core on CentOS Stream 10 (run 37466048646, 2026-10-06) produced 39,012
matches (Syft/Grype, distro centos-10): 0 Critical, and the counts below. Almost all High matches are the kernel
(6.12.0-271.el10 lists the same CVE once per kernel subpackage) and Firefox, mostly backported by the vendor or
unreachable. Failing every build on High would stall the factory without reducing real risk.
Decision
The blocking threshold is Critical. High is tracked and does not block by default. Every exception is written,
owned and dated; nothing is silently ignored.
- The check.
grype --fail-on critical. A digest is not promoted with an unresolved Critical. - High. Recorded in the results, reviewed per release; blocking once shown reachable in this context.
- Vendor-controlled components. The kernel and distribution packages follow the distribution's security
response in the pinned base: tracked, not checked, and a Critical there still blocks. Whose Critical: the
vendor's. For a package the distribution ships, the check uses the severity in the scanner's vendor data
source (
redhat:distro:redhat:10today), because that vendor owns the backport and the reachability judgement. A different NVD score sits beside the match and does not move the check.CVE-2026-98432is the worked example: Red Hat moved it from Moderate 5.9 to Important 8.1 on 2026-10-30 and to Critical 9.8 on 2026-10-31, with EL10 still affected and no fixed build, so the gate blocks until a fixed kernel reaches the base (issue #302). The example shows why the source is named: the vendor revised the score twice and the gate followed each time. Results name the source per match. - Suppressions are explicit. Either an OpenVEX statement (
not_affected,fixed,under_investigation) intests/supply-chain/vex.openvex.json, or an entry intests/supply-chain/exceptions.jsonnaming the owner, the review date, the base image digest and the exact matches (package, type, version, location) it covers. The Grype configuration is generated from it, andtests/supply-chain/check_exceptions.pyfails the build when the review expires, the base moves, or a match falls outside it. An entry with no justification, owner or review date is a bug. - No blanket exclusions. Exceptions are per-CVE, or a scoped package and version, never a whole severity or package family.
- The threshold changes only through a new ADR, not through a workflow edit.
OpenVEX justifications
Use the vocabulary the ecosystem understands: component_not_present, vulnerable_code_not_present,
vulnerable_code_not_in_execute_path, vulnerable_code_cannot_be_controlled_by_adversary and
inline_mitigations_already_exist. Each statement names the component and, where relevant, its window.
Consequences
- The factory moves on Critical while keeping every High visible. Exceptions are auditable: each has an owner and a review date, and an expired entry is removed so the CVE reappears.
- The check lives in CI (ADR 0010) and is the policy DeskOS CI and an organization's factory alike apply.
Baseline (reference)
| Severity | CVEs |
|---|---|
| Critical | 0 |
| High | 1866 |
| Medium | 26735 |
| Low | 10263 |
| Negligible | 24 |
| Unknown | 124 |
Top packages by High: kernel-6.12.0-271.el10 (171, repeated per kernel subpackage),
firefox-140.16.0-1.el10 (61), stdlib-go1.25.7 (55).