## 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][1]) 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) and `analysis.justification` (why), as described in CycloneDX’s exploitability use case. ([cyclonedx.org][1]) * 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 in `not_affected` cases). ([GitHub][2]) Graph relationships (if you use SPDX 3.0.1 as your internal graph layer): * Model dependencies and containment via SPDX `Relationship` and `RelationshipType`, which formalize “Element A RELATIONSHIP Element B” semantics used to compute transitive impact. ([SPDX][3]) --- ### 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: 1. prefer **purl**, then **CPE**, then (name, version, supplier). 2. persist alias mappings (vendor naming variance is normal). #### B. Represent the world as two layers 1. **Graph layer** (what is shipped/depends-on/contains what) 2. **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): 1. **Fixed/Resolved** (with evidence of fix scope) 2. **Not affected** (with valid justification and conditions satisfied) 3. **Affected/Exploitable** 4. **Under investigation / In triage** 5. **Unknown** CycloneDX’s exploitability model explicitly supports “state + justification” to make “not affected” meaningful, not a hand-wave. ([cyclonedx.org][1]) #### 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_affected` should have a justification/impact statement. ([GitHub][2]) 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. [1]: https://cyclonedx.org/use-cases/vulnerability-exploitability/?utm_source=chatgpt.com "Security Use Case: Vulnerability Exploitability" [2]: https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md?utm_source=chatgpt.com "spec/OPENVEX-SPEC.md at main" [3]: https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Classes/Relationship/?utm_source=chatgpt.com "Relationship - SPDX Specification 3.0.1"