8.0 KiB
Product Advisory: Deterministic VEX-first vulnerability verdicts with CycloneDX 1.7
1) The problem you are solving
Modern scanners produce a long list of “components with known CVEs,” but that list is routinely misleading because it ignores context: whether the vulnerable code is shipped, configured, reachable, mitigated, or already fixed via backport. Teams then waste time on false positives, duplicate findings, and non-actionable noise.
A VEX-first approach solves this by attaching exploitability/impact assertions to SBOM components. In CycloneDX, this is expressed via the Vulnerability / Analysis model (often used as VEX), which can declare that a component is not affected, under investigation/in triage, exploitable/affected, or resolved/fixed, along with rationale/justification and other details. CycloneDX explicitly frames this as “vulnerability exploitability” context, including a state and a justification for why a vulnerability is (or isn’t) a practical risk. (cyclonedx.org)
The core product challenge is therefore:
- You will ingest multiple statements (vendors, distros, internal security, runtime evidence) that may conflict.
- Those statements may be conditional (only affected on certain OS, feature flags, build options).
- You must produce a single stable, explainable verdict per (product, vuln), and do so deterministically so audits and diffs are reproducible.
2) Product intent and outcomes
Primary outcome: Reduce noise while increasing trust: every suppression or escalation is backed by evidence and explainable logic.
What “good” looks like:
- Fewer alerts, but higher signal.
- Each vuln has a clear final verdict plus reason chain (“why this was marked not_affected/fixed/affected”).
- Deterministic replay: the same inputs produce the same outputs.
3) Recommended data contract (CycloneDX 1.7 aligned)
Use CycloneDX 1.7 as the canonical interchange for impact/exploitability assertions:
-
SBOM: components + dependencies (CycloneDX and/or SPDX)
-
Vulnerability entries with analysis fields:
analysis.state(status in context) andanalysis.justification(why), as described in CycloneDX’s exploitability use case. (cyclonedx.org)
-
Optional ingress from OpenVEX or CSAF; normalize into CycloneDX analysis semantics (OpenVEX defines the commonly used status set
not_affected / affected / fixed / under_investigation, and requires justification innot_affectedcases). (GitHub)
Graph relationships (if you use SPDX 3.0.1 as your internal graph layer):
- Model dependencies and containment via SPDX
RelationshipandRelationshipType, which formalize “Element A RELATIONSHIP Element B” semantics used to compute transitive impact. (SPDX)
4) Product behavior guidelines
A. Single “Risk Verdict” per vuln, backed by evidence
Expose one final verdict per vulnerability at the product level, with an expandable “proof” pane:
- Inputs considered (SBOM nodes, relationship paths, VEX statements, conditions).
- Merge logic explanation (how conflicts were resolved).
- Timestamped lineage: which feed/source asserted what.
B. Quiet-by-design UX
- Default views show only items needing action: Affected/Exploitable, and Under Investigation with age/timeouts.
- “Not affected” and “Fixed/Resolved” are accessible but not front-and-center; they primarily serve audit and trust.
C. Diff-aware notifications
Notify only on meaningful transitions (e.g., Unknown→Affected, Affected→Fixed), not on every feed refresh.
5) Development guidelines (deterministic resolver)
A. Normalize identifiers first
Create a strict canonical key for matching “the same component” across SBOMs and VEX:
- prefer purl, then CPE, then (name, version, supplier).
- persist alias mappings (vendor naming variance is normal).
B. Represent the world as two layers
- Graph layer (what is shipped/depends-on/contains what)
- Assertion layer (CycloneDX 1.7 vulnerability analysis statements, plus optional runtime/reachability evidence)
Do not mix them—keep assertions as immutable facts that the resolver evaluates.
C. Condition evaluation must be total and deterministic
For each assertion, evaluate conditions against a frozen Context:
- platform (OS/distro/arch), build flags, enabled features, packaging mode
- runtime signals (if used) must be versioned and hashed like any other input
If a condition cannot be evaluated, treat it explicitly as Unknown, not false.
D. Merge conflicts via a documented lattice
Define a monotonic merge function that is:
- commutative (order independent),
- idempotent (reapplying doesn’t change),
- associative (supports streaming/parallel merges).
A pragmatic priority (adjust to your policy):
- Fixed/Resolved (with evidence of fix scope)
- Not affected (with valid justification and conditions satisfied)
- Affected/Exploitable
- Under investigation / In triage
- Unknown
CycloneDX’s exploitability model explicitly supports “state + justification” to make “not affected” meaningful, not a hand-wave. (cyclonedx.org)
E. Propagation rules must be explicit
Decide and document how assertions propagate across the dependency graph:
- When a dependency is Affected, does the product become Affected automatically? (Typically yes if the dependency is shipped and used, unless a product-level assertion says otherwise.)
- When a dependency is Not affected due to “code removed before shipping,” does the product inherit Not affected? (Often yes, but only if you can prove the affected code path is absent for the shipped artifact.)
- Keep propagation rules versioned to avoid “policy drift” breaking deterministic replay.
F. Always emit a proof object
For every final verdict emit:
- contributing assertions (source IDs), condition evaluations, merge steps
- the graph path(s) that made it relevant (SPDX Relationship chain or CycloneDX dependency references) This proof is what lets you be quiet-by-design without losing auditability.
6) Interop guidance (OpenVEX / CSAF → CycloneDX 1.7)
If you ingest OpenVEX:
- Map OpenVEX status to CycloneDX analysis state (policy-defined mapping).
- Enforce OpenVEX minimums:
not_affectedshould have a justification/impact statement. (GitHub)
If you ingest CSAF advisories:
- Treat them as another assertion source; do not let them overwrite higher-confidence internal evidence without explicit precedence rules.
7) Testing and rollout checklist
- Golden test vectors: fixed input bundles (SBOM + assertions + context) with expected verdicts.
- Determinism tests: shuffle assertion ordering; results must be identical.
- Regression diffs: store prior proofs; verify only intended transitions occur after feed updates.
- Adversarial cases: conflicting assertions, partial conditions, alias mismatches, missing dependency edges.
8) Common failure modes to avoid
- Treating “not affected” as a suppression without requiring justification.
- Allowing “latest feed wins” behavior (non-deterministic and unauditable).
- Mixing runtime telemetry directly into SBOM identity (breaks replay).
- Implicit propagation rules (different engineers will interpret differently; results drift).
If you want, I can also provide a short, implementation-ready “resolver contract” (types, verdict lattice, proof schema) that is CycloneDX 1.7-centric while remaining neutral to whether you store the graph as CycloneDX dependencies or SPDX 3.0.1 relationships.