False Positive CVE Management and Auditor Justification
Documenting why CVEs don't matter beats pretending they don't exist.

A container scan comes back with 200-plus open CVEs on a base image nobody touched this sprint. A developer spends two days working through the list and finds that the overwhelming majority don't actually affect the shipped product: dead code, build-time dependencies stripped out before the final layer, versions the scanner misread. Then the next scan runs, and the same 200 findings appear again, because nothing about that two-day investigation got written down anywhere the scanner or an auditor could find it. That's the real failure mode in vulnerability management right now: not too many CVEs, but zero durable record of why most of them don't matter.
Why CVSS alone produces a false positive problem
CVSS was built to score the severity of a vulnerability in the abstract, and it does that job fine. What it can't do is account for whether a given deployment runs the vulnerable code, whether a compensating control sits in front of it, or whether the network topology makes the flaw reachable. A CVSS 10.0 finding sitting behind three layers of network segmentation and an unauthenticated internal-only interface trips the same scanner alarm as an actual zero-day sitting wide open on a public endpoint. The score doesn't know the difference, and it was never designed to.
That mismatch isn't a theoretical complaint, it is visible in the data. Plenty of moderate-severity findings carry a high real-world probability of exploitation, while a large share of findings rated at the top severity tier are close to zero. This is the gap that EPSS, the Exploit Prediction Scoring System built by FIRST, was designed to close: it assigns each CVE a probability of exploitation within the next 30 days, using observed attacker behavior rather than a static severity rubric. Version 3 of EPSS reportedly raised predictive performance by 82% over its predecessor, and Version 4 shipped in March 2025. Research comparing the two approaches consistently finds EPSS beats CVSS on both efficiency and coverage when the job is prioritization.
CISA's Known Exploited Vulnerabilities catalog, KEV, plays a related but distinct role: it confirms after the fact what EPSS often flagged early. A finding with a near-zero EPSS score and no KEV listing is about as strong a candidate as exists for a documented, defensible non-action. Not a shrug. A documented one.
The four VEX statuses and five justifications that give a determination legal and operational weight
SBOM and VEX get treated as interchangeable in a lot of shops, and that confusion causes real problems. An SBOM says what's in the software: a static inventory, generated once per build. VEX, the Vulnerability Exploitability eXchange, captures a dynamic and time-sensitive judgment about whether a specific vulnerability actually affects that software. In TR-03183-2, Germany's BSI states that an SBOM should carry no vulnerability information at all, and CSAF/VEX is where that information belongs. That's not a stylistic preference, it's regulatory guidance drawing a hard line between a static artifact and a living determination.
VEX defines four statuses. Not_affected means no action is required, but it can't stand alone, it needs one of five standardized justifications supporting it, which is what makes the status meaningful rather than a bare assertion. Affected requires an action_statement, no exceptions. Fixed states which product versions carry the remediation. Under_investigation flags that the assessment isn't finished yet, an honest placeholder rather than a guess dressed up as a conclusion.
The five justifications for not_affected aren't interchangeable, and they carry different weights of evidentiary strength. In roughly the order they should get evaluated:
Component_not_present is the strongest and easiest to verify: the vulnerable component simply never ships. This is common with build-time or test-time dependencies that get discarded in a multi-stage container build. Vulnerable_code_not_present covers the case where the library is present but the specific vulnerable code isn't, and in practice this usually comes down to backporting. Debian, for example, frequently ships a security fix without bumping the version string a scanner reads, and this generates a false positive at scale. Vulnerable_code_not_in_execute_path means the code exists in the binary but is never called, and this one can actually be proven rigorously, through symbol analysis rather than assertion. Vulnerable_code_cannot_be_controlled_by_adversary is harder to defend, since it rests on a claim about the entire trust boundary around that code path, not just the code itself. And inline_mitigations_already_exist requires naming the actual mitigation, an input filter, a WAF rule, a seccomp profile, and where it lives. A mitigation nobody can point to on request isn't auditable, and if that control ever gets removed, the justification quietly becomes a lie nobody meant to tell.
The evidence chain that SSVC structures behind a VEX determination
Stakeholder-Specific Vulnerability Categorization, SSVC, gives structure to how evidence turns into a decision. It runs gathered facts through a decision tree calibrated to the deployer's role and produces a prioritization action: Immediate, Out-of-cycle, Scheduled, or Defer, backed by a citable chain of evidence rather than a gut call.
For a Deployer, SSVC asks four questions. Exploitation status, drawn from KEV, EPSS, and public exploit availability. Reachability, whether the vulnerable path actually executes in this deployment. Exposure, how accessible the component is over the network. And technical impact, taken directly from the CVSS vector rather than re-derived by a human guessing at severity all over again.
Anything that can be computed by a script should be computed by a script, and the analyst's judgment gets reserved for the parts that genuinely require it, a design principle from the cve-triage methodology. A fact re-derived by hand every time is a fact written twice, and nothing forces those two copies to agree with each other. That's how drift creeps into triage records even when nobody's being careless.
The guardrail logic that sits on top of this needs to be policy, not discretion. Reachability inconclusive plus Total Impact routes to under_investigation, automatically. A verified compensating control may demote a finding one step, but only when it's actually verified, and only when that verification gets written down alongside the demotion.
What auditors and regulators now require instead of a Jira comment
The EU Cyber Resilience Act does not require a CVE-free product. That standard would be unreachable for nearly any commercial software of meaningful complexity, and the CRA doesn't pretend otherwise. What it requires is evidence that exploitability has actually been assessed, which is a much more achievable and much more auditable bar.
CRA Article 14's reporting obligations, which take effect September 11, 2026, set a 24-hour window for early warning on actively exploited vulnerabilities. That clock starts at discovery, not once triage wraps up. A team scrambling to figure out affected status after an exploit goes public is already behind. Pre-built VEX records, generated as part of routine triage rather than under incident pressure, remove that scramble.
Annex I, Part II lays out two obligations that matter here directly: manufacturers have to identify and document vulnerabilities and components, including through an SBOM, and once a fix ships, they have to share and publicly disclose information about the vulnerabilities it addresses. A particular advisory data format is built for exactly this, and a single document in that format can double as a valid VEX document at the same time.
Line that up against what most teams actually have on hand today. A comment buried in a closed Jira ticket carries no timestamp integrity, isn't machine-readable, isn't signed, and often disappears the moment the ticket gets archived. A signed, timestamped VEX document with a standardized justification is the opposite on every count: presentable to an auditor, ingestible by a regulator's own tooling, and replayable months or years later by someone who wasn't in the room when the call got made.
How environment provenance makes triage determinations defensible over time
A justification like vulnerable_code_not_in_execute_path or inline_mitigations_already_exist is only true for one specific product version running in one specific environment configuration. Changing either one allows the justification to go from true to false without anyone noticing, because nothing forced a re-check.
Environment drift is the actual mechanism behind this kind of silent staleness. Unpinned transitive dependencies and system libraries that nobody's tracking shift between one build and the next, quietly, with no record of what changed or when. A study of 5,298 Docker builds found that only 6.4% of rebuilt images matched the original set of installed package versions exactly. That means, in the overwhelming majority of cases, the environment a triage decision was made against is not the environment that actually ships to production. The justification was true on Tuesday, and nobody checked its truth again on Friday.
Declarative, pinned environments with input-derived hashes are the fix for this, not a nice-to-have. A VEX determination gets tied to a specific environment hash at the moment it's made, and if that hash ever changes, the determination gets flagged for review automatically rather than silently carried forward as if nothing happened.
Building the repeatable triage workflow: steps, roles, and artifact ownership
Triage isn't a one-time cleanup sprint, it's a loop: scan, triage, commit the resulting artifacts, monitor for environment or advisory changes, and re-triage the moment either one shifts. Treating it as a project with an end date is how teams end up re-litigating the same 200 CVEs every quarter.
The first step, before anyone writes a justification, is gathering evidence. That means pulling the full advisory from OSV, GHSA, or the CVE record itself and resolving the alias set, since EPSS and KEV are CVE-only feeds and a finding with no CVE alias should get skipped rather than guessed at. It means recording the EPSS score and KEV status at the moment of triage, because both change over time and the timestamp attached to that snapshot matters as much as the number itself. And it means determining whether the dependency is direct or transitive, and whether it ships in production or only appears at build or test time; that distinction often decides which of the five justifications even applies.
None of this is exotic. It's disciplined bookkeeping applied to a problem that most teams have been treating as a one-off firefight. The workflow doesn't eliminate judgment, SSVC and the VEX justifications still require someone to make a real call. What it eliminates is amnesia: the two days spent re-deriving a conclusion the team already reached, because nobody wrote it down in a form an auditor, a regulator, or next quarter's scan could actually use.
Sources
- GitHub - fedius01/cve-triage: Agent skill for Claude Code and OpenAI Codex that triages one CVE/GHSA/OSV id against a Python repo using RBVM: CVSS, EPSS, KEV, reachability and exposure → SSVC Deployer decision → CycloneDX/OpenVEX record Dependency-Track can ingest.
- VEX Vulnerability Management | DevGuard Blog
- Common Vulnerabilities and Exposures (CVEs)
- Known Exploited Vulnerabilities Catalog | CISA
- arxiv.org
- apiiro.com
- cisa.gov
- picussecurity.com


