<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Cyclonedx | FirewallSync</title><link>https://firewallsync.com/tags/cyclonedx/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Tue, 03 Nov 2026 04:40:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/cyclonedx/index.xml" rel="self" type="application/rss+xml"/><item><title>CycloneDX VEX: analysis.state and justification to OpenVEX</title><link>https://firewallsync.com/posts/cyclonedx-vex-analysis-state-justification-openvex-csaf-mapping/</link><guid>https://firewallsync.com/posts/cyclonedx-vex-analysis-state-justification-openvex-csaf-mapping/</guid><pubDate>Tue, 03 Nov 2026 04:40:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>CycloneDX VEX uses six analysis states and nine justifications; OpenVEX and CSAF use four statuses and five justifications. Here is a field-by-field mapping, which pairs line up and which do not, how Trivy reads CycloneDX VEX, and a tested Python converter that refuses to invent justifications.</description><enclosure url="https://firewallsync.com/posts/cyclonedx-vex-analysis-state-justification-openvex-csaf-mapping/cover_hu15575583779999317849.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/cyclonedx-vex-analysis-state-justification-openvex-csaf-mapping/cover_hu15575583779999317849.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/cyclonedx-vex-analysis-state-justification-openvex-csaf-mapping/cover_hu15575583779999317849.jpg" alt="Close-up of a silver modular synthesizer panel with black knobs, patched with pink, green, orange, red and yellow cables" width="1200" height="630"></p><p><strong>Scope.</strong> This is a field-level mapping guide. We compared the CycloneDX JSON schemas for 1.6 and 1.7 (identical enums for the fields below), OpenVEX specification v0.2.0 and its JSON schema, and the CSAF 2.0 JSON schema and VEX profile text from the OASIS TC repository, then read how Trivy (main branch, commit <code>1d1ab074</code>, November 2, 2026) consumes CycloneDX VEX. The converter below was run with Python 3.13; its output validated against the OpenVEX 0.2.0 JSON schema and its inputs against the CycloneDX 1.6 schema. Our <a href="/posts/vex-explained-what-a-not-affected-statement-means-and-how-to-read-one/">VEX explainer</a> covers what the statuses mean and how far to trust them; this article is only about moving statements between formats.</p>
<h2 id="the-short-answer">The short answer</h2>
<p>If you receive CycloneDX VEX and your pipeline speaks OpenVEX or CSAF (or the reverse), the states map almost one-to-one, but the justifications do not.</p>
<table>
  <thead>
      <tr>
          <th>CycloneDX <code>analysis.state</code></th>
          <th>OpenVEX <code>status</code></th>
          <th>CSAF <code>product_status</code></th>
          <th>Trivy (code)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>not_affected</code></td>
          <td><code>not_affected</code></td>
          <td><code>known_not_affected</code></td>
          <td>not_affected</td>
      </tr>
      <tr>
          <td><code>false_positive</code></td>
          <td><code>not_affected</code> (no exact match)</td>
          <td><code>known_not_affected</code> (no exact match)</td>
          <td>not_affected</td>
      </tr>
      <tr>
          <td><code>exploitable</code></td>
          <td><code>affected</code></td>
          <td><code>known_affected</code></td>
          <td>affected</td>
      </tr>
      <tr>
          <td><code>in_triage</code></td>
          <td><code>under_investigation</code></td>
          <td><code>under_investigation</code></td>
          <td>under_investigation</td>
      </tr>
      <tr>
          <td><code>resolved</code></td>
          <td><code>fixed</code></td>
          <td><code>fixed</code></td>
          <td>fixed</td>
      </tr>
      <tr>
          <td><code>resolved_with_pedigree</code></td>
          <td><code>fixed</code> (pedigree evidence is lost)</td>
          <td><code>fixed</code></td>
          <td>fixed</td>
      </tr>
  </tbody>
</table>
<p>Of CycloneDX&rsquo;s nine justifications, only <code>code_not_present</code> and <code>code_not_reachable</code> have a clear counterpart among the five CISA-derived labels that OpenVEX and CSAF share. Convert those two; carry the other seven as text.</p>
<h2 id="where-vex-sits-in-a-cyclonedx-document">Where VEX sits in a CycloneDX document</h2>
<p>CycloneDX has no separate VEX document type. VEX is the <code>vulnerabilities</code> array of a BOM, and the fields that matter are:</p>
<ul>
<li><code>id</code>: the vulnerability identifier, such as a CVE ID.</li>
<li><code>affects[].ref</code>: what the statement is about. The schema allows a <code>bom-ref</code> in the same document or a BOM-Link element. A BOM-Link has the form <code>urn:cdx:serialNumber/version#bom-ref</code>, and the schema&rsquo;s pattern requires the serial number UUID, a version of 1 or more, and a fragment.</li>
<li><code>analysis.state</code>: one of <code>resolved</code>, <code>resolved_with_pedigree</code>, <code>exploitable</code>, <code>in_triage</code>, <code>false_positive</code>, <code>not_affected</code>.</li>
<li><code>analysis.justification</code>: one of nine reasons, described below.</li>
<li><code>analysis.response</code>: zero or more of <code>can_not_fix</code>, <code>will_not_fix</code>, <code>update</code>, <code>rollback</code>, <code>workaround_available</code>.</li>
<li><code>analysis.detail</code>: free text. The schema says that if a vulnerability is not exploitable, &ldquo;this field should include specific details on why the component or service is not impacted.&rdquo;</li>
</ul>
<p>A VEX statement can sit inside the SBOM itself or in a separate BOM that only has <code>vulnerabilities</code> and refers back to the SBOM by BOM-Link. That second pattern matters for tooling, below.</p>
<h2 id="states-four-clean-pairs-and-two-lossy-ones">States: four clean pairs and two lossy ones</h2>
<p>The CycloneDX schema defines the six states in its <code>meta:enum</code>. Four map directly:</p>
<ul>
<li><code>not_affected</code>: &ldquo;The component or service is not affected by the vulnerability. Justification should be specified for all not_affected cases.&rdquo; This is OpenVEX <code>not_affected</code> and CSAF <code>known_not_affected</code>.</li>
<li><code>exploitable</code>: &ldquo;The vulnerability may be directly or indirectly exploitable.&rdquo; This is OpenVEX <code>affected</code> and CSAF <code>known_affected</code>.</li>
<li><code>in_triage</code>: &ldquo;The vulnerability is being investigated.&rdquo; This is <code>under_investigation</code> in both.</li>
<li><code>resolved</code>: &ldquo;The vulnerability has been remediated.&rdquo; This is <code>fixed</code> in both.</li>
</ul>
<p>Two lose meaning:</p>
<ul>
<li><strong><code>false_positive</code></strong>: &ldquo;The vulnerability is not specific to the component or service and was falsely identified or associated.&rdquo; OpenVEX and CSAF have no status for &ldquo;the scanner was wrong&rdquo;. The safe target is <code>not_affected</code>, which is also what Trivy&rsquo;s <code>cdxStatus</code> function does (<code>case cdx.IASFalsePositive, cdx.IASNotAffected: return types.FindingStatusNotAffected</code>). Keep the word <code>false_positive</code> in the impact statement so a reviewer can see it.</li>
<li><strong><code>resolved_with_pedigree</code></strong>: remediated &ldquo;and evidence of the changes are provided in the affected components pedigree&rdquo;. It becomes <code>fixed</code>, and the pointer to pedigree evidence is lost unless you keep it as text.</li>
</ul>
<h2 id="justifications-two-of-nine-line-up">Justifications: two of nine line up</h2>
<p>OpenVEX&rsquo;s five justification labels come from CISA&rsquo;s June 2022 status-justification document, and the CSAF 2.0 schema&rsquo;s <code>flags[].label</code> uses the same five strings. CycloneDX defines a different set of nine. Here is each one with its CycloneDX definition and our reading of the closest OpenVEX/CSAF label.</p>
<table>
  <thead>
      <tr>
          <th>CycloneDX <code>justification</code></th>
          <th>CycloneDX definition</th>
          <th>OpenVEX / CSAF label</th>
          <th>Our call</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>code_not_present</code></td>
          <td>&ldquo;The code has been removed or tree-shaked.&rdquo;</td>
          <td><code>vulnerable_code_not_present</code></td>
          <td>Map</td>
      </tr>
      <tr>
          <td><code>code_not_reachable</code></td>
          <td>&ldquo;The vulnerable code is not invoked at runtime.&rdquo;</td>
          <td><code>vulnerable_code_not_in_execute_path</code></td>
          <td>Map</td>
      </tr>
      <tr>
          <td><code>requires_dependency</code></td>
          <td>&ldquo;Exploitability requires a dependency that is not present.&rdquo;</td>
          <td>none exact (<code>component_not_present</code> means the vulnerable component itself is absent)</td>
          <td>Text</td>
      </tr>
      <tr>
          <td><code>requires_configuration</code></td>
          <td>&ldquo;Exploitability requires a configurable option to be set/unset.&rdquo;</td>
          <td>none</td>
          <td>Text</td>
      </tr>
      <tr>
          <td><code>requires_environment</code></td>
          <td>&ldquo;Exploitability requires a certain environment which is not present.&rdquo;</td>
          <td>none</td>
          <td>Text</td>
      </tr>
      <tr>
          <td><code>protected_by_compiler</code></td>
          <td>&ldquo;Exploitability requires a compiler flag to be set/unset.&rdquo;</td>
          <td>none exact</td>
          <td>Text</td>
      </tr>
      <tr>
          <td><code>protected_at_runtime</code></td>
          <td>&ldquo;Exploits are prevented at runtime.&rdquo;</td>
          <td><code>inline_mitigations_already_exist</code> only if the protection cannot be disabled</td>
          <td>Text</td>
      </tr>
      <tr>
          <td><code>protected_at_perimeter</code></td>
          <td>&ldquo;Attacks are blocked at physical, logical, or network perimeter.&rdquo;</td>
          <td>none</td>
          <td>Text</td>
      </tr>
      <tr>
          <td><code>protected_by_mitigating_control</code></td>
          <td>&ldquo;Preventative measures have been implemented that reduce the likelihood and/or impact of the vulnerability.&rdquo;</td>
          <td>none</td>
          <td>Text</td>
      </tr>
  </tbody>
</table>
<p>Why so strict? The OpenVEX spec defines <code>inline_mitigations_already_exist</code> as built-in protections that &ldquo;cannot be subverted by the attacker and cannot be configured or disabled by the user.&rdquo; A CycloneDX <code>protected_at_runtime</code> or <code>protected_by_compiler</code> claim does not tell you that. <code>protected_at_perimeter</code> and <code>protected_by_mitigating_control</code> describe the deployment, not the product, and a mitigating control that &ldquo;reduce[s] the likelihood and/or impact&rdquo; is weaker than &ldquo;not affected&rdquo; in the CISA sense.</p>
<p>This mapping is our editorial reading of the published definitions. We did not find a mapping table published by the CycloneDX project or CISA.</p>
<p>OpenVEX gives you a way out: for <code>not_affected</code>, &ldquo;a VEX statement MUST include either a status justification or an impact_statement.&rdquo; So you can emit a <code>not_affected</code> statement with no label and an impact statement that preserves the original CycloneDX reason. CSAF&rsquo;s VEX profile has the same escape hatch: each <code>known_not_affected</code> product needs either a machine-readable flag or a human-readable justification in <code>threats</code> with category <code>impact</code>.</p>
<h2 id="responses-and-action-statements">Responses and action statements</h2>
<p>For affected products, both targets require an action:</p>
<ul>
<li>OpenVEX: for <code>affected</code>, &ldquo;a VEX statement MUST include a statement that SHOULD describe actions to remediate or mitigate the vulnerability&rdquo; (<code>action_statement</code>).</li>
<li>CSAF VEX profile: for <code>known_affected</code>, &ldquo;additional product specific information SHALL be provided in <code>/vulnerabilities[]/remediations</code> as an action statement.&rdquo;</li>
</ul>
<p>CycloneDX&rsquo;s <code>response</code> values give you a starting point. <code>update</code> and <code>rollback</code> fit CSAF&rsquo;s <code>vendor_fix</code> remediation category, <code>workaround_available</code> fits <code>workaround</code>, and <code>will_not_fix</code> / <code>can_not_fix</code> fit <code>no_fix_planned</code> (or <code>none_available</code>), which the CSAF profile explicitly permits for an action statement. OpenVEX takes a free-text <code>action_statement</code>, so join the responses with <code>analysis.detail</code>. If an <code>exploitable</code> statement has neither a response nor detail, the converted statement will be valid but useless; send it back to the supplier.</p>
<h2 id="bom-link-the-part-that-silently-drops-statements">BOM-Link: the part that silently drops statements</h2>
<p>A CycloneDX VEX BOM that references components by BOM-Link is bound to one SBOM version. Trivy&rsquo;s docs (v0.67) say the tool &ldquo;only supports the Independent BOM and VEX BOM format, so you need to provide a separate VEX file alongside the SBOM,&rdquo; that &ldquo;BOM-Links must be placed in <code>affects.ref</code>,&rdquo; and that the input SBOM must be CycloneDX. The usage example is <code>trivy sbom trivy.sbom.cdx --vex trivy.vex.cdx</code>. The feature is marked experimental.</p>
<p>Trivy&rsquo;s code adds a detail the docs do not spell out: if the BOM-Link&rsquo;s serial number or version does not equal the scanned SBOM&rsquo;s, it logs <code>URN doesn't match with SBOM</code> and skips the statement. Regenerate the SBOM (new serial number or version) without regenerating the VEX, and every suppression stops applying. That fails safe, since findings come back rather than disappear, but it surprises teams.</p>
<p>OpenVEX has no BOM-Link. Products are identified by an <code>@id</code> such as a Package URL, so a converter has to resolve each BOM-Link to the referenced component&rsquo;s <code>purl</code> in the SBOM. That is why the converter below takes both files.</p>
<h2 id="a-tested-converter-cyclonedx-vex-to-openvex">A tested converter: CycloneDX VEX to OpenVEX</h2>
<p>This Python 3 script reads a CycloneDX SBOM and a separate CycloneDX VEX BOM and writes an OpenVEX 0.2.0 document. It maps states as in the first table, maps only the two clean justifications, and keeps everything else as text. It is illustrative, not production-ready: it does not handle version ranges in <code>affects[].versions</code>, services, or nested BOMs beyond <code>components</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Convert a CycloneDX VEX BOM (independent VEX, BOM-Link refs) to OpenVEX v0.2.0.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Usage: python3 cdx2openvex.py sbom.cdx.json vex.cdx.json author &gt; out.openvex.json&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> json<span style="color:#f92672">,</span> sys<span style="color:#f92672">,</span> hashlib
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime, timezone
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>STATUS <span style="color:#f92672">=</span> {  <span style="color:#75715e"># CycloneDX analysis.state -&gt; OpenVEX status</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;not_affected&#34;</span>: <span style="color:#e6db74">&#34;not_affected&#34;</span>, <span style="color:#e6db74">&#34;false_positive&#34;</span>: <span style="color:#e6db74">&#34;not_affected&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;exploitable&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>, <span style="color:#e6db74">&#34;in_triage&#34;</span>: <span style="color:#e6db74">&#34;under_investigation&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;resolved&#34;</span>: <span style="color:#e6db74">&#34;fixed&#34;</span>, <span style="color:#e6db74">&#34;resolved_with_pedigree&#34;</span>: <span style="color:#e6db74">&#34;fixed&#34;</span>,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Only the pairs whose definitions line up; everything else is carried as text.</span>
</span></span><span style="display:flex;"><span>JUSTIFICATION <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;code_not_present&#34;</span>: <span style="color:#e6db74">&#34;vulnerable_code_not_present&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;code_not_reachable&#34;</span>: <span style="color:#e6db74">&#34;vulnerable_code_not_in_execute_path&#34;</span>,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">purls_by_ref</span>(sbom):
</span></span><span style="display:flex;"><span>    serial, version <span style="color:#f92672">=</span> sbom[<span style="color:#e6db74">&#34;serialNumber&#34;</span>], sbom<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;version&#34;</span>, <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    out, stack <span style="color:#f92672">=</span> {}, list(sbom<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;components&#34;</span>, []))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> stack:
</span></span><span style="display:flex;"><span>        c <span style="color:#f92672">=</span> stack<span style="color:#f92672">.</span>pop()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;bom-ref&#34;</span> <span style="color:#f92672">in</span> c <span style="color:#f92672">and</span> <span style="color:#e6db74">&#34;purl&#34;</span> <span style="color:#f92672">in</span> c:
</span></span><span style="display:flex;"><span>            out[<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;urn:cdx:</span><span style="color:#e6db74">{</span>serial<span style="color:#f92672">.</span>removeprefix(<span style="color:#e6db74">&#39;urn:uuid:&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>version<span style="color:#e6db74">}</span><span style="color:#e6db74">#</span><span style="color:#e6db74">{</span>c[<span style="color:#e6db74">&#39;bom-ref&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>] <span style="color:#f92672">=</span> c[<span style="color:#e6db74">&#34;purl&#34;</span>]
</span></span><span style="display:flex;"><span>        stack<span style="color:#f92672">.</span>extend(c<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;components&#34;</span>, []))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> out
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">convert</span>(sbom, vex, author):
</span></span><span style="display:flex;"><span>    refs, statements, problems <span style="color:#f92672">=</span> purls_by_ref(sbom), [], []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> v <span style="color:#f92672">in</span> vex<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;vulnerabilities&#34;</span>, []):
</span></span><span style="display:flex;"><span>        a <span style="color:#f92672">=</span> v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;analysis&#34;</span>, {})
</span></span><span style="display:flex;"><span>        state <span style="color:#f92672">=</span> a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;state&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> state <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> STATUS:
</span></span><span style="display:flex;"><span>            problems<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>v[<span style="color:#e6db74">&#39;id&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">: no analysis.state, skipped&#34;</span>); <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        products <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> af <span style="color:#f92672">in</span> v<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;affects&#34;</span>, []):
</span></span><span style="display:flex;"><span>            purl <span style="color:#f92672">=</span> refs<span style="color:#f92672">.</span>get(af[<span style="color:#e6db74">&#34;ref&#34;</span>])
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> purl: products<span style="color:#f92672">.</span>append({<span style="color:#e6db74">&#34;@id&#34;</span>: purl})
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>: problems<span style="color:#f92672">.</span>append(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>v[<span style="color:#e6db74">&#39;id&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>af[<span style="color:#e6db74">&#39;ref&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74"> not found in SBOM&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> products: <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        s <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;vulnerability&#34;</span>: {<span style="color:#e6db74">&#34;name&#34;</span>: v[<span style="color:#e6db74">&#34;id&#34;</span>]}, <span style="color:#e6db74">&#34;products&#34;</span>: products, <span style="color:#e6db74">&#34;status&#34;</span>: STATUS[state]}
</span></span><span style="display:flex;"><span>        detail <span style="color:#f92672">=</span> a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;detail&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>)<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> s[<span style="color:#e6db74">&#34;status&#34;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;not_affected&#34;</span>:
</span></span><span style="display:flex;"><span>            j <span style="color:#f92672">=</span> JUSTIFICATION<span style="color:#f92672">.</span>get(a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;justification&#34;</span>))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> j: s[<span style="color:#e6db74">&#34;justification&#34;</span>] <span style="color:#f92672">=</span> j
</span></span><span style="display:flex;"><span>            note <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;CycloneDX state=</span><span style="color:#e6db74">{</span>state<span style="color:#e6db74">}</span><span style="color:#e6db74">, justification=</span><span style="color:#e6db74">{</span>a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;justification&#39;</span>, <span style="color:#e6db74">&#39;none&#39;</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>            s[<span style="color:#e6db74">&#34;impact_statement&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>note<span style="color:#e6db74">}</span><span style="color:#e6db74">. </span><span style="color:#e6db74">{</span>detail<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">elif</span> s[<span style="color:#e6db74">&#34;status&#34;</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;affected&#34;</span>:
</span></span><span style="display:flex;"><span>            resp <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;, &#34;</span><span style="color:#f92672">.</span>join(a<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;response&#34;</span>, [])) <span style="color:#f92672">or</span> <span style="color:#e6db74">&#34;none given&#34;</span>
</span></span><span style="display:flex;"><span>            s[<span style="color:#e6db74">&#34;action_statement&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;CycloneDX response: </span><span style="color:#e6db74">{</span>resp<span style="color:#e6db74">}</span><span style="color:#e6db74">. </span><span style="color:#e6db74">{</span>detail<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span>        statements<span style="color:#f92672">.</span>append(s)
</span></span><span style="display:flex;"><span>    body <span style="color:#f92672">=</span> json<span style="color:#f92672">.</span>dumps(statements, sort_keys<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>encode()
</span></span><span style="display:flex;"><span>    doc <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;@context&#34;</span>: <span style="color:#e6db74">&#34;https://openvex.dev/ns/v0.2.0&#34;</span>,
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;@id&#34;</span>: <span style="color:#e6db74">&#34;https://example.com/vex/&#34;</span> <span style="color:#f92672">+</span> hashlib<span style="color:#f92672">.</span>sha256(body)<span style="color:#f92672">.</span>hexdigest()[:<span style="color:#ae81ff">16</span>],
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;author&#34;</span>: author,
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;timestamp&#34;</span>: datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)<span style="color:#f92672">.</span>isoformat(timespec<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;seconds&#34;</span>),
</span></span><span style="display:flex;"><span>           <span style="color:#e6db74">&#34;version&#34;</span>: <span style="color:#ae81ff">1</span>, <span style="color:#e6db74">&#34;statements&#34;</span>: statements}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> doc, problems
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    sbom, vex <span style="color:#f92672">=</span> (json<span style="color:#f92672">.</span>load(open(p)) <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>:<span style="color:#ae81ff">3</span>])
</span></span><span style="display:flex;"><span>    doc, problems <span style="color:#f92672">=</span> convert(sbom, vex, sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">3</span>])
</span></span><span style="display:flex;"><span>    json<span style="color:#f92672">.</span>dump(doc, sys<span style="color:#f92672">.</span>stdout, indent<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> problems: print(<span style="color:#e6db74">&#34;WARN&#34;</span>, p, file<span style="color:#f92672">=</span>sys<span style="color:#f92672">.</span>stderr)
</span></span></code></pre></div><h3 id="test-run">Test run</h3>
<p>We fed it a three-component CycloneDX 1.6 SBOM (log4j-core 2.14.1, jackson-databind 2.13.0, snakeyaml 1.30) and a VEX BOM with four statements. The assessments in the VEX are invented test data, not real vendor findings:</p>
<ol>
<li>CVE-2021-44228 on log4j-core: <code>not_affected</code>, <code>code_not_reachable</code>.</li>
<li>CVE-2022-42003 on jackson-databind: <code>not_affected</code>, <code>requires_configuration</code>.</li>
<li>CVE-2022-1471 on snakeyaml: <code>exploitable</code>, response <code>update</code>.</li>
<li>CVE-2021-44228 again, but with a BOM-Link naming SBOM version <code>2</code> instead of <code>1</code>.</li>
</ol>
<p>Both input files passed <code>check-jsonschema</code> against the CycloneDX 1.6 schema. The run printed one warning to stderr and three statements; the output validated against <code>openvex_json_schema.json</code> (v0.2.0) with the Python <code>jsonschema</code> package:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>WARN CVE-2021-44228: urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/2#pkg-log4j not found in SBOM
</span></span></code></pre></div><p>The <code>statements</code> array of the output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>[
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;vulnerability&#34;</span>: { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;CVE-2021-44228&#34;</span> },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;products&#34;</span>: [ { <span style="color:#f92672">&#34;@id&#34;</span>: <span style="color:#e6db74">&#34;pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1&#34;</span> } ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;not_affected&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;justification&#34;</span>: <span style="color:#e6db74">&#34;vulnerable_code_not_in_execute_path&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;impact_statement&#34;</span>: <span style="color:#e6db74">&#34;CycloneDX state=not_affected, justification=code_not_reachable. JNDI lookups are never invoked; the app logs through SLF4J with Logback.&#34;</span>
</span></span><span style="display:flex;"><span>},
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;vulnerability&#34;</span>: { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;CVE-2022-42003&#34;</span> },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;products&#34;</span>: [ { <span style="color:#f92672">&#34;@id&#34;</span>: <span style="color:#e6db74">&#34;pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.0&#34;</span> } ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;not_affected&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;impact_statement&#34;</span>: <span style="color:#e6db74">&#34;CycloneDX state=not_affected, justification=requires_configuration. The deserialization feature this issue depends on is disabled in our ObjectMapper configuration.&#34;</span>
</span></span><span style="display:flex;"><span>},
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;vulnerability&#34;</span>: { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;CVE-2022-1471&#34;</span> },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;products&#34;</span>: [ { <span style="color:#f92672">&#34;@id&#34;</span>: <span style="color:#e6db74">&#34;pkg:maven/org.yaml/snakeyaml@1.30&#34;</span> } ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;affected&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;action_statement&#34;</span>: <span style="color:#e6db74">&#34;CycloneDX response: update. Dependency upgrade ships in release 4.2.&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p>The second statement is the important one: it stays <code>not_affected</code> with no invented label, and the reason survives as text. The fourth input statement was dropped with a warning instead of being applied to the wrong SBOM.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul>
<li><strong>Mapping every <code>protected_*</code> justification to <code>inline_mitigations_already_exist</code>.</strong> That label claims the protection cannot be disabled. A WAF rule or a compiler flag can.</li>
<li><strong>Dropping <code>false_positive</code> statements</strong> because the target format has no matching status. Map them to <code>not_affected</code> and say why.</li>
<li><strong>Dropping the <code>detail</code> text.</strong> It is often the only evidence a reviewer has, and both OpenVEX and CSAF have a place for it.</li>
<li><strong>Reusing a VEX BOM after regenerating the SBOM.</strong> BOM-Links name an exact serial number and version.</li>
<li><strong>Treating converted VEX as first-party.</strong> Keep the original author. OpenVEX <code>author</code> should be the organization that made the assessment, not the converter.</li>
</ul>
<h2 id="where-this-fits">Where this fits</h2>
<p>VEX is the &ldquo;is my product actually affected&rdquo; step in our <a href="/posts/vulnerability-prioritization-workflow-cve-record-to-patch-ticket/">vulnerability prioritization workflow</a>, after the CVE record and before KEV, SSVC and EPSS. The SBOM that the VEX points at is covered in our guide to <a href="/posts/sbom-minimum-elements-2026-what-cisa-and-partners-changed-from-the-2021-ntia-baseline/">SBOM minimum elements in 2026</a>. If you want a signal for which <code>exploitable</code> statements to fix first, our <a href="/posts/cvss-vs-epss-vs-kev-which-signal-should-decide-what-you-patch-first/">CVSS vs EPSS vs KEV comparison</a> covers the trade-offs.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>The justification mapping is our reading of the published definitions; no standards body has published it. We read CSAF 2.0 from the OASIS TC&rsquo;s GitHub repository (schema and editor&rsquo;s draft prose) rather than the OASIS-hosted standard. We read Trivy&rsquo;s main branch, not a tagged release, and its docs for v0.67; behaviour may differ in the version you run. We did not run Trivy itself or test the reverse conversion (OpenVEX or CSAF to CycloneDX), which is harder because CycloneDX <code>affects</code> needs a component reference the OpenVEX product may not resolve to.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CycloneDX states, justifications, responses and their definitions; detail field; BOM-Link pattern</td>
          <td><a href="https://github.com/CycloneDX/specification/tree/master/schema">CycloneDX bom-1.6 / bom-1.7 JSON schemas</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>BOM-Link syntax <code>urn:cdx:serialNumber/version#bom-ref</code></td>
          <td><a href="https://www.cyclonedx.org/capabilities/bomlink/">CycloneDX BOM-Link</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>OpenVEX statuses, five justifications, justification or impact_statement rule, action_statement rule</td>
          <td><a href="https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md">OpenVEX spec v0.2.0</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CSAF product_status categories, flag labels, remediation categories</td>
          <td><a href="https://github.com/oasis-tcs/csaf/blob/master/csaf_2.0/json_schema/csaf_json_schema.json">CSAF 2.0 JSON schema</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>CSAF VEX profile requirements for known_not_affected and known_affected</td>
          <td><a href="https://github.com/oasis-tcs/csaf/blob/master/csaf_2.0/prose/csaf-v2-editor-draft.md">CSAF 2.0 prose, section 4.5</a></td>
          <td>Qualified (TC repository draft text)</td>
      </tr>
      <tr>
          <td>Trivy maps false_positive to not_affected and skips BOM-Links that do not match the SBOM</td>
          <td><a href="https://github.com/aquasecurity/trivy/blob/main/pkg/vex/cyclonedx.go">Trivy pkg/vex/cyclonedx.go</a></td>
          <td>Verified (main branch, 2026-11-02)</td>
      </tr>
      <tr>
          <td>Trivy CycloneDX VEX: separate VEX BOM, BOM-Link in affects.ref, CycloneDX SBOM only, experimental</td>
          <td><a href="https://trivy.dev/docs/latest/supply-chain/vex/file">Trivy docs: VEX file</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Converter output and schema validation</td>
          <td>Our run, Python 3.13, jsonschema, check-jsonschema</td>
          <td>Verified (code)</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>