Of the four VEX statuses, not_affected is the one that does the useful work — it's how a scary CVE in a dependency comes off your queue. It's also the one a regulator scrutinises hardest, because it's the decision not to act. Getting it right isn't about picking the status; it's about being able to say why.
Why not_affected is the risky one
An affected commits you to remediation and possibly reporting — nobody argues with your caution. A not_affected is the opposite: it's a claim that a known vulnerability doesn't matter here, and it removes an obligation. If an authority later disagrees — the vulnerability was actively exploited and did reach your product — the not_affected is the record they'll examine. "Our engineer judged it" is not a position; the justification code and its evidence are.
The five justification codes
VEX doesn't let you just say not_affected — it asks why, from a fixed set of standard justifications:
| Justification | Means |
|---|---|
| component_not_present | The component isn't in this build at all. |
| vulnerable_code_not_present | The component is present, but the vulnerable code isn't in it. |
| vulnerable_code_not_in_execute_path | The vulnerable code is present but never reached. |
| vulnerable_code_cannot_be_controlled_by_adversary | Reachable, but an attacker can't drive it into the vulnerable state. |
| inline_mitigations_already_exist | A mitigation in your product neutralises it. |
The evidence each one needs
Choosing the code is half the job; backing it is the other half. Each justification points at a different kind of evidence:
component_not_present→ the SBOM for the exact build shows the component absent.vulnerable_code_not_present→ build/config evidence that the vulnerable module or function isn't compiled in.vulnerable_code_not_in_execute_path→ a reachability trace showing no path from an entry point to the vulnerable code.vulnerable_code_cannot_be_controlled_by_adversary→ an exploitation attempt that reached the code and could not trigger the condition.inline_mitigations_already_exist→ identification of the mitigation and why it neutralises this vulnerability.
Notice the escalation: the first two are answered by inventory and build facts, the middle by reachability analysis, and the hardest — reachable but not exploitable — only by an actual exploitation attempt. Which code you can honestly claim depends on how deep your evidence goes; the distinction is the subject of proof-of-exploitability vs. reachability.
Anatomy of a defensible not_affected
A defensible not_affected names the justification, and points at the artifact that proves it.
Put concretely, a not_affected that holds up has four parts: the vulnerability and the product version it's about; the justification code; a plain-language rationale; and a reference to the evidence artifact — the SBOM entry, the reachability trace, the exploitation log. Export it in CycloneDX VEX or OpenVEX; the format is incidental. What makes it survive a reasoned request is that the justification isn't asserted, it's supported — which is the whole argument of evidence-backed VEX.
Frequently asked
What are the VEX not_affected justification codes?
VEX defines five: 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 states a different reason the product is unaffected, and each calls for different supporting evidence.
Is a not_affected VEX enough without evidence?
It exports fine, but it won’t hold up. Under the CRA a market-surveillance authority can ask why a vulnerability wasn’t reported; a not_affected justified only by a human’s judgement is an assertion, not a defence. Backing the justification code with evidence is what makes it defensible.
Which justification is strongest?
None is inherently strongest — the right code is the one that’s true for your product. What varies is the evidence: component_not_present is backed by the SBOM, vulnerable_code_not_in_execute_path by a reachability trace, and vulnerable_code_cannot_be_controlled_by_adversary by an exploitation attempt.