Safeguard
Vulnerability Analysis

Reachability analysis for prioritizing vulnerabilities

Reachability analysis cuts vulnerability noise by 70-90% by tracing which CVEs are actually callable from your code, not just present in your dependency tree.

James
Principal Security Architect
8 min read

A vulnerability scanner that flags 1,400 CVEs in a single microservice is not prioritization — it's noise. Most of those findings sit in code paths that never execute: a vulnerable XML parser bundled in a dependency your application never imports, a deserialization bug in a logging library configured to a mode you don't use. Reachability analysis solves this by tracing whether the vulnerable function in a flagged package can actually be called from your application's entry points. Instead of "you have a critical CVE in libxml2," it tells you "your parseUserUpload() function calls into the vulnerable xmlParseMemory routine in three services." That distinction is the difference between a 1,400-item backlog and a 40-item one. This post explains how reachability analysis works, why CVSS scores alone routinely misdirect remediation effort, and what a real reduction in false-positive vulnerability alerts looks like when call-graph analysis is applied to a production codebase.

What is vulnerability reachability analysis?

Vulnerability reachability analysis determines whether the specific vulnerable code inside a dependency is actually invoked by your application, as opposed to merely present in your dependency tree. A scanner running Software Composition Analysis (SCA) matches package names and versions against CVE databases like the NVD — it sees that your package-lock.json pulls in lodash@4.17.15 and flags CVE-2020-8203, regardless of whether your code calls _.zipObjectDeep(), the function where the prototype-pollution bug actually lives. Reachability analysis goes one layer deeper: it builds a call graph from your application's entry points (HTTP handlers, CLI commands, message consumers) down through every function call, including into third-party libraries, and checks whether that graph reaches the vulnerable function or class. If the path doesn't exist, the CVE is present in your tree but not reachable in your running application. Static reachability analysis builds this graph from source and bytecode; dynamic reachability analysis confirms it by observing actual execution during tests or in production via instrumentation.

Why do CVSS scores alone fail to prioritize the right vulnerabilities?

CVSS scores measure the theoretical severity of a vulnerability in isolation, not whether your specific deployment can be exploited through it. A CVSS 9.8 "Critical" score for CVE-2023-44487 (the HTTP/2 Rapid Reset flaw disclosed October 10, 2023) is accurate for any HTTP/2-terminating server — but if your service runs behind a load balancer that already caps concurrent stream resets, your practical exposure is lower than the score implies. The inverse problem is more common: a "Medium" 5.3-severity deserialization bug can be trivially reachable and remotely exploitable in your specific call path, while a "Critical" 9.8 sits in a code branch gated behind an admin-only feature flag nobody has enabled. Sonatype's 2023 State of the Software Supply Chain report found that only 11% of vulnerable open-source dependency downloads pulled in a version with a known, exploitable vulnerability actually reachable at runtime — meaning nearly 9 in 10 CVSS-flagged alerts in that sample described risk that CVSS's severity number alone couldn't distinguish from noise. Teams that triage strictly by CVSS score end up patching low-risk dependencies on a Friday afternoon while an exploitable one ships untouched.

How does reachability analysis actually work under the hood?

Reachability analysis works by constructing a call graph of your application — starting at entry points and tracing every function call, including across package boundaries — then checking whether any path in that graph terminates at a function known to contain the vulnerable code. For compiled and interpreted languages alike (Java, Go, Python, JavaScript/TypeScript), static analysis tools parse the abstract syntax tree of your first-party code and the bytecode or source of your dependencies, resolve imports and method calls, and produce a directed graph. A reachability engine then intersects that graph against a database mapping CVEs to the exact functions, classes, or line ranges they affect — for example, CVE-2021-44228 (Log4Shell, disclosed December 9, 2021) maps specifically to JndiLookup.lookup() in log4j-core. If your call graph shows any path from an HTTP handler through to that method — which it does in most Log4j 2.x deployments using default appenders — the vulnerability is confirmed reachable. Dynamic analysis complements this by instrumenting a running application or test suite to record which functions actually execute, catching reflection, dynamic dispatch, and plugin-loading patterns that static analysis can miss or over-approximate.

What's the difference between reachability and exploitability?

Reachability tells you a vulnerable function can be called; exploitability tells you an attacker can trigger that call path with attacker-controlled input to cause harm. These are sequential filters, not synonyms. Reachability analysis answers "is xmlParseMemory in the call graph from any entry point?" Exploitability analysis goes further and asks "can an unauthenticated network request reach that call, and does the data along that path stay attacker-controlled the whole way, with no sanitization in between?" CVE-2023-4863, the libwebp heap-buffer-overflow disclosed September 11, 2023, is a useful example: it was reachable in any application that decoded untrusted WebP images, but exploitability depended on whether the specific decode path preserved attacker-controlled dimension fields all the way to the vulnerable buffer allocation, which varied by how each application called the library. In practice, reachability analysis is the higher-leverage first filter because it's cheaper to compute reliably at scale and it already cuts 70-90% of a typical alert volume (per the Sonatype figures above) before exploitability modeling — which requires taint tracking and is more expensive to run continuously — even enters the picture.

How much can reachability analysis reduce a vulnerability backlog?

Reachability analysis typically cuts open-source vulnerability alert volume by 70-90% by removing CVEs in dependency code that's never called, based on multiple published industry studies of real codebases. An oft-cited analysis of Java applications by Google's OSS security team found that roughly 73% of vulnerabilities flagged by traditional SCA scanning existed only in class files never loaded onto the JVM's classpath at runtime — meaning the vulnerable code was shipped but never executed regardless of what the SCA tool reported. Applied to a concrete case: a team with 1,400 open CVE findings across 60 microservices, running reachability analysis against their call graphs, might see the actionable list shrink to roughly 150-250 findings — the subset where a path exists from a live entry point to the vulnerable function. That's not a rounding error; it's the difference between an engineering team burning two sprints on dependency bumps versus a focused week of patching the dependencies that actually matter. The remaining findings also come pre-ranked by path length and entry-point exposure (internet-facing vs. internal-only), which lets teams sequence the truly urgent Log4Shell-style cases ahead of internal batch-job dependencies.

What are the limitations of reachability analysis?

Reachability analysis can produce false negatives when dynamic language features — reflection, eval, dynamic class loading, plugin architectures — create call paths that static analysis can't resolve at build time. A Java application using Spring's classpath scanning to instantiate beans by name, or a Python app that imports a module string built from a config file, creates a call edge that doesn't exist in source until runtime, so a purely static call graph can under-report reachability and clear a vulnerability that's actually exploitable. This is why mature reachability engines combine static call-graph construction with dynamic confirmation from test-suite execution or runtime instrumentation, and why any vendor's "reachable/not reachable" verdict should be treated as a strong prioritization signal rather than an absolute exploitability guarantee — an "unreachable" finding should get deprioritized, not deleted from tracking, because a future code change can make a previously dead path live. Container-based and interpreted-language coverage also varies by tooling maturity: reachability support for Go and Rust binaries is generally more mature than for highly dynamic frameworks like Ruby on Rails, where metaprogramming obscures call graphs.

How Safeguard Helps

Safeguard's reachability analysis builds a real call graph from your first-party code down through every open-source dependency, so a CVE only surfaces as actionable when there's a live path from an actual entry point to the vulnerable function — cutting the kind of 1,400-finding backlog described above down to the subset your team genuinely needs to fix. Griffin AI, Safeguard's security analyst engine, layers exploitability context on top of that reachability graph, tracing whether attacker-controlled input can actually flow along the reachable path before ranking a finding above internal, low-exposure code. Safeguard generates and ingests SBOMs automatically on every build, so the dependency inventory feeding reachability analysis stays current without a separate manual scan step. When a reachable, exploitable vulnerability is confirmed, Safeguard can open an auto-fix pull request with the minimum version bump or patch needed to close the path — turning a triage decision directly into a mergeable code change instead of a ticket that sits in a backlog for another sprint.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.