EPSS scores the probability a CVE gets exploited somewhere in the world. Reachability determines whether the vulnerable code path even exists in your running application. They cut different kinds of noise — run both and your list gets shorter than either technique manages alone. Run only reachability, though, and you risk deprioritizing an active CISA KEV entry that should never move.
Point two scanners at the same repository. One comes back with 40 findings. The other comes back with 6.
Neither tool is lying. They're answering different questions. One asks, "how likely is this CVE to be exploited anywhere on the internet?" The other asks, "does your code even call the vulnerable function?" A CVE can score badly on the first question and pass cleanly on the second, or the reverse, and both answers can be true at once.
I've watched teams pick a side in this argument like it's a religious dispute. It isn't. They're two instruments measuring two different things, and the fight only exists because vendors market them as competitors.
What We'll Cover
- What reachability analysis actually does
- Where reachability analysis breaks down
- What EPSS still gets wrong
- Reachability vs EPSS: the comparison table
- How to use both together
- What GeekWala checks and doesn't
- FAQ
What Reachability Analysis Actually Does
"Reachability" gets used as a single word for three different techniques, and vendors aren't shy about blurring them. Separating the three levels honestly matters: this taxonomy is our own synthesis of how the major vendors tier their methodology, not an industry standard, so don't expect every vendor's docs to use these exact labels.
Manifest-level. Is the vulnerable package present in your dependency tree at all? This is what most "reachability" marketing screenshots actually show, and it isn't reachability in any meaningful sense. It's the same information a plain npm audit or pip list already gives you. If a scanner's reachability claim reduces to "package X is installed," you've paid for a lookup, not an analysis.
Import/module-level. Does your code actually import or require the vulnerable package, rather than it sitting unused as a transitive dependency five levels deep? This filters out real noise, but it says nothing about which functions inside that package you call.
Function/call-graph level. Does a traceable path exist from your application's entry points down to the specific vulnerable function? This is the expensive one. It requires building a call graph of your entire codebase, mapping it against the vulnerability's exact location, and proving (or failing to prove) a connecting path.
That third tier is where the real engineering happens, and it's also where things get hard. Call-graph construction is language-dependent: an analyzer that handles Java's type system confidently will choke on Python's duck typing. Even a well-built graph breaks on four patterns: dynamic dispatch (the called method is decided at runtime), reflection (Class.forName(), getattr(), anything resolving a symbol from a string), eval and its cousins (generated code with no static call-graph edge), and dependency-injection containers (a framework wires the concrete implementation, not a line the analyzer can follow).
None of this makes function-level reachability worthless. It's a specific, bounded technique with specific, bounded failure modes, a very different claim than "reachability tells you what's actually exploitable."
Where Reachability Analysis Breaks Down
This is the part most vendor content skips straight past.
Dynamic languages produce false negatives, not just noise. A call graph that "proves" a function is unreachable can simply be wrong. Python, Ruby, and JavaScript are the three languages where this happens most: monkey-patching, __getattr__ overrides, string-based require() paths, and metaprogramming all create real call paths a static graph won't see. "Unreachable" from a call-graph tool is a claim about what the analyzer could prove, not what your application does at runtime.
Reachability answers a control-flow question, not a data question. A vulnerable parser that only triggers on malformed input is "reachable" the instant a user uploads a file, but a call-graph tool that just checks whether your code calls the parsing function marks it reachable whether or not any input ever exercises the unsafe branch. Knowing a function is called isn't the same as knowing the vulnerable path inside it ever executes.
It needs your source, so it stops at container boundaries. A base image you pulled from a registry, a vendored binary, or a third-party service you call but didn't build gives the analyzer nothing to trace. Reachability analysis describes the code you wrote, not your deployed surface area.
It's per-application, so it doesn't compose across a fleet. The same dependency version can be unreachable in Service A and sit directly on the request path in Service B. Reachability gets computed and re-computed per application, per deploy. It has no "is this generally dangerous" answer, because that answer changes with every codebase that calls it.
I'd go further: reachability is the most oversold term in this category. Three different things ship under that word, and the cheapest one to compute, "is the package in your manifest," is the one most often shown in the demo.
What EPSS Still Gets Wrong
To be fair to reachability, EPSS has its own blind spots. What is EPSS? covers the model in depth, so we won't re-derive it here. The short version relevant to this comparison, straight from FIRST's own EPSS model documentation: EPSS is a global average, not a statement about your environment. A 3% exploitation probability describes the whole internet's exposure; it says nothing about whether your specific deployment (internet-facing, unauthenticated, holding customer data) makes you a more attractive target than average. A CVE at EPSS 0.03 can still be the one that hits you.
And EPSS has no concept of your code. It scores the CVE, not your usage of the dependency that contains it. A package with an EPSS 0.9 finding is irrelevant noise if you never call the vulnerable function. That's precisely the gap reachability exists to close.
Reachability vs EPSS: The Comparison Table
| Axis | EPSS | Reachability |
|---|---|---|
| Question it answers | Will anyone exploit this CVE, anywhere, in the next 30 days? | Does a path exist from my entry points to the vulnerable code? |
| Data required | CVE metadata plus global threat intelligence (FIRST.org's model) | Your source code, built into a call graph |
| Scope | Global: one score per CVE, same for every user | Per-application: recomputed for every codebase |
| Cost to compute | Free to consume (FIRST publishes it), zero setup | Expensive: static analysis infrastructure, language-specific tooling |
| Where it fails | Says nothing about your environment or your controls | Dynamic dispatch, reflection, eval, DI containers, container images without source |
| What it can't tell you | Whether your code even uses the vulnerable function | Whether attackers anywhere are actually going after this CVE |
How to Use Both Together
The two techniques compose cleanly if you sequence them right, and the sequencing matters more than either signal alone.
Start with CISA KEV. Confirmed active exploitation sits at the top regardless of what the other two signals say. Use EPSS to order everything else, acting as your primary sort key across the findings that aren't yet confirmed exploited. Reachability comes last and plays a narrower role: defer within a tier (two EPSS-0.4 findings, one reachable and one not, patch the reachable one first), and, when it proves something stronger than "uncalled," support a documented VEX statement.
The quadrant below is the mental model worth keeping: KEV membership sets the floor, EPSS sorts everything else, and reachability only ever acts as a tiebreaker inside a tier.
▲ EPSS Score — Exploit Probability (high → low)
KEV listing and EPSS score answer different questions — confirmed-exploited vs. likely-to-be-exploited. Triage needs both, not one or the other.
That last clause needs precision, because the sloppy version of this rule is wrong and a security-literate reader will catch it. The sloppy version: never deprioritize a KEV entry on reachability grounds, full stop. Too absolute. CISA's own VEX Status Justification documentation lists vulnerable_code_not_in_execute_path as a valid not_affected justification, with no carve-out for KEV membership. SSVC's deployer decision tree guarantees active exploitation never resolves to "defer," but active-exploitation-plus-controlled-exposure-plus-low-impact still lands on "Scheduled" rather than "Immediate." The urgency tier can legitimately shift even while the requirement to act does not. And the old blanket rule, BOD 22-01, which set remediation timelines purely from KEV catalog dates, was superseded by BOD 26-04, which uses a four-factor risk matrix instead of KEV membership alone.
The defensible version: reachability can move a KEV entry down an urgency tier, and, if it proves the vulnerable code is genuinely absent from your build (not merely uncalled today), it can support a documented VEX not_affected claim. That's a formal exception with a paper trail, not a routine deprioritization you make on a Tuesday and forget. What is VEX? covers this in depth. Reachability is the evidence a not_affected claim rests on, not the claim itself.
Staring at a scanner that just returned 200 CVEs? What to Do When Your Scanner Finds 200 CVEs walks through the practical sequencing. For the full three-signal framework this section assumes (KEV, EPSS, CVSS as tiebreaker), see the prioritization guide.
What GeekWala Checks and Doesn't
Full honesty here, because it's the only way this article earns credibility: GeekWala does not perform call-graph reachability analysis. There's no static analyzer building call graphs from your source in our stack, and we're not going to describe our EPSS and KEV enrichment as something it isn't.
What GeekWala does is prioritize using EPSS scores and CISA KEV status, enriched onto every finding across all eight ecosystems we support: npm, PyPI, Maven, Packagist, Go, crates.io, RubyGems, and NuGet. That's a deliberate trade-off. Function-level reachability needs a different static-analysis engine per language and source access we don't always have. Exploitation probability and confirmed-exploitation status generalize across every ecosystem without any of that. The same EPSS score means the same thing whether the CVE sits in a Go module or a Rails gem.
That's a real limitation, not a footnote. If your team already runs function-level reachability tooling, EPSS and KEV enrichment is a complement to it, not a substitute. Reachability tells you whether your code calls the vulnerable function; GeekWala tells you whether anyone is bothering to exploit it.
Still shopping for your first SCA platform rather than deciding how to prioritize what one already found? See our software composition analysis tools compared for how reachability specialists, developer-first scanners, and free options stack up.
Frequently Asked Questions
If a CVE is unreachable, can I close the ticket?
Not on its own. "Unreachable" from a call-graph tool means the analyzer couldn't find a path, not that no path exists. Dynamic dispatch, reflection, and string-based code loading all produce real call paths static analysis misses, particularly in Python, Ruby, and JavaScript. Deprioritize it, document it, but don't close it outright unless you've separately confirmed the package version genuinely doesn't ship the vulnerable code path at all.
Which languages have usable reachability tooling today?
It depends which vendor and which tier of their methodology you mean, and the table moves, so check current docs before committing budget to a specific claim. As of our August 2026 check: Snyk's function-level reachability is General Availability for Java (Maven, Gradle), JavaScript/TypeScript (npm, Yarn, pnpm), and Python (pip, poetry, pipenv), with C# (NuGet, paket) in Early Access. Endor Labs runs full call-graph analysis on roughly seven languages, falling back to a lighter "potentially reachable" bucket outside that set; its marketed "40+ languages" figure is a general SCA-coverage count, not reachability depth. Semgrep ships GA dataflow reachability, tracing whether tainted input reaches the vulnerable function, across roughly 10-12 languages. That it's more precise than plain call-graph matching is Semgrep's claim, not an independently verified one. Socket has true function-level reachability after acquiring Coana in April 2025, though .NET and Rust run at class level rather than function level.
Does reachability replace the need to patch?
No, and treating it that way is the most common misuse of the technique. Reachability is a prioritization signal, not a remediation decision. An unreachable-today finding can become reachable the moment someone adds a code path that calls it: a refactor, a new feature, a dependency upgrade that changes what's imported. Patching removes the vulnerability; reachability just tells you which unpatched ones to worry about first.
Can I claim VEX not_affected based on a reachability scan alone?
Only with caution, and only for the strongest form of the finding. A scan that says "no call path found" supports not_affected only when paired with evidence the vulnerable code is genuinely absent from what you ship, not merely uncalled in the paths the analyzer traced. CISA's VEX Status Justification documentation recognizes vulnerable_code_not_in_execute_path as a valid justification, but it's meant to be a documented, defensible claim with a paper trail, not a checkbox you tick because a scanner returned zero for one CVE.
Neither tool wins this argument. They were never playing the same game. EPSS tells you what the world is doing; reachability tells you what your code is doing. The teams that get the most value skip the debate and use KEV as the floor, EPSS as the sort, reachability as the tiebreaker, in that order, every time.
Want to see where your own dependencies land on EPSS and KEV before you invest in reachability tooling? Scan your dependencies. Every finding comes back enriched with exploitation probability and confirmed-exploitation status, across all eight ecosystems, no account required.


