Skip to main content

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.

  1. The check. grype --fail-on critical. A digest is not promoted with an unresolved Critical.
  2. High. Recorded in the results, reviewed per release; blocking once shown reachable in this context.
  3. 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:10 today), 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-98432 is 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.
  4. Suppressions are explicit. Either an OpenVEX statement (not_affected, fixed, under_investigation) in tests/supply-chain/vex.openvex.json, or an entry in tests/supply-chain/exceptions.json naming 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, and tests/supply-chain/check_exceptions.py fails 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.
  5. No blanket exclusions. Exceptions are per-CVE, or a scoped package and version, never a whole severity or package family.
  6. 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)​

SeverityCVEs
Critical0
High1866
Medium26735
Low10263
Negligible24
Unknown124

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).