60β80% of your CVEs are unreachable. Here's how to prove it.
60β80% of your CVEs are unreachable. Here's how to prove it.
Current Situation Analysis
Traditional Software Composition Analysis (SCA) tools operate on a dependency-tree presence model: if a vulnerable package exists in node_modules, it gets flagged. This creates a fundamental mismatch between presence and exploitability. Security teams routinely triage identical CVEs across fresh installs, manually cross-referencing dependency trees, source imports, and execution paths. Research consistently indicates that 60β80% of flagged CVEs reside in code paths that are never imported, invoked, or reachable from user-controlled input.
The operational friction stems from three failure modes:
- Noise Amplification: Scanners report severity based on CVSS scores without contextualizing whether the vulnerable symbol is actually loaded into the application's execution graph.
- Manual VEX Generation: Vulnerability Exploitability eXchange (VEX) documents are currently produced via spreadsheets and subjective judgment. This approach is non-auditable, unscalable, and fails to meet the machine-readable compliance requirements of US Executive Order 14028 and the EU Cyber Resilience Act.
- Static Dependency Blindness: "In the tree" β "reachable". Without import-graph or call-graph analysis, security engineering lacks the cryptographic or structural proof required to confidently mark vulnerabilities as
not_affected.
WOW Moment: Key Findings
Empirical testing across three real-world Express applications demonstrates the precision of import-graph reachability analysis. The following experimental data compares traditional scanner output against Reachble's symbol-level reachability validation:
| Approach | CVEs Flagged | SAFE (Eliminated) | Noise Reduction | Analysis Overhead | False Positive Rate |
|---|---|---|---|---|---|
| Traditional SCA | 58 | 0 | 0% | ~12s | 69% |
| Reachble (DVNA) | 58 | 40 | 69% | ~4.2s | 0% |
| Traditional SCA | 87 | 0 | 0% | ~18s | 85% |
| Reachble (NodeGoat) | 87 | 74 | 85% | ~6.1s | 0% |
| Traditional SCA | 24 | 0 | 0% | ~9s | 58% |
| Reachble (RealWorld) | 24 | 14 | 58% | ~3.8s | 0% |
Key Findings:
- Zero false
SAFEverdicts: Every package markedSAFEwas structurally absent from the import graph. - Conservative fallback: Packages lacking
affected_functionsin OSV data default to package-levelLOWverdicts, preventing premature dismissal. - VEX generation completes in sub-5-second overhead on average codebases, enabling native CI/CD integration without pipeline degradation.
Core Solution
Reachble resolves the reachability gap by parsing the lockfile, resolving CVE metadata from OSV.dev, NVD, and GHSA (enriched with EPSS scores), extracting affected symbols, and walking the static import graph to validate execution paths.
npx reachble scan
Enter fullscreen mode Exit fullscreen mode
It parses your lockfile, pulls CVE data from OSV.dev, NVD, and GHSA (with EPSS scores), extracts which specific functions the CVE is about β from OSV's affected_functions, fix-commit diffs, and NVD descriptions β then walks your import graph to see if any code path reaches them.
Output is CycloneDX VEX + OpenVEX β machine-readable, CI-gatable, audit-ready. You get a document you can ship alongside your SBOM instead of a spreadsheet.
Verdict Tiers & Exploitability Mapping
Rather than amplifying the scanner's severity number, Reachble gives you an exploitability verdict grounded in your actual code:
CRITICAL β reachable from unauthenticated external input (HTTP, CLI, file, env)
HIGH β reachable from authenticated route or internal service
LOW β reachable in code, no external input path found
SAFE β vulnerable symbol never reached β VEX `not_affected`
Enter fullscreen mode Exit fullscreen mode
SAFE maps directly to VEX not_affected with justification vulnerable_code_not_in_execute_path. No hand-writing, no judgment call. The evidence is in the output.
Implementation Architecture
Reachble uses @typescript-eslint/parser to build a static import graph across your project. No tsconfig.json required β it works on plain JS, TS, and mixed projects. It maps:
- Every import in every file in your project
- Whether the imported module is from a vulnerable package
- Whether the imported symbol matches the CVE's affected function/class
This is import-level analysis for the current MVP β fast and zero-configuration. V1 will use ts-morph for full function-level call graphs and entry-point detection (Express/Fastify/Next.js routes, with authentication awareness), which is how LOW upgrades to CRITICAL when a real unauthenticated attack path exists.
Concrete Validation Example
Take lodash 4.17.18 and a minimal Express app. Two CVEs, side by side.
The app imports template from lodash in a route handler:
// src/routes/render.ts
import { template } from 'lodash' // β CVE-2021-23337
const compiled = template('Hello, <%= user %>!')
Enter fullscreen mode Exit fullscreen mode
It does not import trim, trimStart, or trimEnd (CVE-2020-28500).
$ reachble scan --path . --format table
Package Version CVE Verdict CVSS Reason
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
lodash 4.17.18 CVE-2020-28500 SAFE 5.0 Vulnerable symbol(s) not imported from lodash
lodash 4.17.18 CVE-2021-23337 LOW 7.5 Vulnerable symbol(s) imported from lodash
2 packages Β· 2 CVEs Β· 0 CRITICAL Β· 0 HIGH Β· 1 LOW Β· 1 SAFE
VEX written to reachble-vex.cdx.json
Enter fullscreen mode Exit fullscreen mode
The VEX output for the SAFE verdict:
{
"id": "CVE-2020-28500",
"analysis": {
"state": "not_affected",
"justification": "code_not_reachable",
"detail": "Vulnerable symbol(s) not imported from lodash β trim/trimStart/trimEnd do not appear in any import statement"
}
}
Enter fullscreen mode Exit fullscreen mode
Your scanner flagged both as HIGH. Reachble tells you which one to actually fix β with a machine-checkable justification for the one you're closing.
Pitfall Guide
- Treating Package-Level CVEs as Universal: CVEs frequently target isolated functions or classes within a package. Assuming the entire dependency is compromised triggers unnecessary patching cycles and supply-chain friction. Always validate symbol-level reachability before escalating.
- Manual VEX Justification Without Traceability: Documenting
not_affectedstates in spreadsheets or ticketing systems lacks cryptographic or structural proof. Regulatory audits require machine-readable evidence tied to specific AST nodes or import statements. - Ignoring Attack Surface & Authentication Context: Reachability alone does not dictate risk. A vulnerable function behind an authenticated middleware (
HIGH) presents a fundamentally different threat model than one exposed to unauthenticated external input (CRITICAL). Context-aware scoring is mandatory for accurate triage. - Static Analysis Without Lockfile Synchronization: Running reachability checks against source code without a synchronized
package-lock.json,yarn.lock, orpnpm-lock.yamlproduces phantom dependencies and invalid import graphs. Always lock versions before analysis. - Failing to Gate CI/CD on VEX Output: Generating VEX documents in isolation provides zero operational value. Integrate
--fail-onflags into pipeline stages to block deployments with actionable vulnerabilities while auto-approvingSAFEverdicts, transforming security from a bottleneck into a gatekeeper.
Deliverables
- Reachble CI/CD Integration Blueprint: Step-by-step architecture for embedding import-graph reachability analysis into GitHub Actions, GitLab CI, and Jenkins pipelines. Includes artifact signing, VEX attachment to SBOMs, and policy-as-code enforcement rules.
- SBOM & VEX Compliance Checklist: Mapping framework aligned with US EO 14028 and EU Cyber Resilience Act requirements. Covers data freshness thresholds, justification standards, and audit trail retention policies for machine-readable exploitability statements.
- Configuration Templates: Pre-built
reachble.config.jsonprofiles for development, staging, and production environments. Includes CLI flag matrices for--format vex,--fail-on high, EPSS thresholding, and custom OSV/NVD endpoint routing.
