A typical Java service pulls in well over a hundred transitive dependencies, and a routine scan might flag a dozen of them for known CVEs. In most cases, the vulnerable function inside those flagged packages is never actually invoked — the JAR was imported for one unrelated utility method, and the dangerous code path just sits there unused. Snyk's reachability analysis tries to answer a narrower, more useful question than "is this CVE present in my tree": does your code actually call the vulnerable function?
Rather than stopping at manifest-level matching between a lockfile and a vulnerability database, Snyk builds a static call graph that traces execution paths from your application's own entry points, through your code, into the dependency tree, and down to the specific method a CVE was filed against. This post walks through how that call-graph construction works, what statuses Snyk actually reports, which languages it covers, and where static reachability analysis runs into real limits.
What Problem Is Reachability Analysis Actually Solving?
Reachability analysis exists because dependency-based vulnerability scanning conflates "installed" with "exploitable," and those are rarely the same thing. A software composition analysis (SCA) tool that only reads a package-lock.json or pom.xml can tell you that log4j-core 2.14.1 is somewhere in your dependency tree, which is exactly what triggered the mass panic during Log4Shell (CVE-2021-44228) in December 2021. What that manifest-level check cannot tell you is whether your application's logging calls ever pass attacker-controlled input into the JNDI lookup path in JndiLookup.class that made Log4Shell exploitable. Plenty of organizations had log4j-core in their tree purely as a transitive dependency of a dependency, with no code path anywhere near the vulnerable lookup class.
That gap is what produces the long, undifferentiated vulnerability backlogs security teams complain about. Reachability analysis is Snyk's attempt to sort a flat list of "known-vulnerable packages present" into a shorter list of "vulnerable functions your code can actually reach," so triage effort goes toward the subset that matters first.
How Does Snyk Build the Call Graph From Your Code to the Vulnerable Function?
Snyk builds the graph statically, without executing the application, using the same source-parsing and abstract-syntax-tree analysis underpinning Snyk Code, its SAST engine. According to Snyk's own documentation, the analysis needs your actual source files, not just a manifest or lockfile — it has to parse your functions and their call sites to know which library methods your code invokes, then walk outward from there.
Mechanically, the process starts at your application's functions and traces outbound calls: your code calls a function in a direct dependency, that function calls into a transitive dependency, and so on, until the graph either terminates at the specific method a CVE advisory identifies as vulnerable, or it doesn't. If a chain of static call edges connects your source code to that method, Snyk marks the vulnerability as reachable. If the parser can complete the analysis but never finds such a chain, it marks the vulnerability as having no path found. This is fundamentally a graph-traversal problem layered on top of ordinary dependency resolution — the novelty isn't in detecting the CVE, it's in proving (or failing to prove) a static edge exists between your code and the vulnerable function.
What Reachability Statuses Does Snyk Actually Report?
Snyk assigns each vulnerability one of a small set of discrete labels rather than a continuous probability score. Per Snyk's documentation, results generally fall into a few buckets: a function-call path was found (reachable), the analysis ran but no static path was found, or reachability data isn't available at all — because the ecosystem isn't supported, the analysis hit a scale or complexity limit, or the package is a dependency Snyk can't introspect deeply enough.
These labels aren't cosmetic — they feed directly into Snyk's priority scoring, which combines reachability status with CVSS severity, exploit maturity (is there a public PoC or known exploitation in the wild), and social signals like how much attention a CVE is getting. A critical CVSS score attached to a reachable function pushes a finding to the top of the queue; the same CVSS score on a package with no path found gets deprioritized, though notably not deleted from the backlog — Snyk is explicit that "no path found" is a statement about the limits of static analysis, not a guarantee of safety.
Which Languages and Package Managers Does Reachability Analysis Actually Cover?
Coverage is limited to a specific set of ecosystems, not universal across everything Snyk scans for vulnerabilities. At the time of writing, Snyk's documented reachability support centers on Java and Kotlin projects built with Maven or Gradle, JavaScript and TypeScript projects using npm or Yarn, and Python projects using pip — the same ecosystems where Snyk Code's static analysis engine has its deepest language support. Package managers and languages outside that list may be scanned for known vulnerabilities as usual, but without a reachability verdict attached, meaning every finding in those ecosystems defaults to the older "vulnerable dependency present" triage model.
It's also worth being precise about scope: the call graph Snyk builds is bounded by the repository it's analyzing. A microservice architecture where the vulnerable function is only reached via an HTTP call from a separate downstream service sits outside what a single-repo static call graph can see, because that edge is a network call at runtime, not a function call in source. Reachability analysis answers "does this codebase call this function," not "does this vulnerable function ever get invoked anywhere in our system."
Where Does Static Reachability Analysis Break Down?
Static analysis can't fully model behavior that only resolves at runtime, so a "no path found" result means the parser didn't locate a static edge — it doesn't mean the function is provably unreachable. Several patterns are known to be hard for any static call-graph analyzer, Snyk's included: reflection and dynamic class loading (Class.forName in Java, __import__ in Python), dependency-injection frameworks like Spring that wire up implementations at runtime rather than through direct calls in source, interface and polymorphic dispatch where the concrete implementation isn't determinable from the call site alone, and dynamically interpreted strings — the exact mechanism that made Log4Shell exploitable in the first place, since the dangerous input arrived through string interpolation in a log message rather than a conventional method call.
The reverse failure mode also exists: a function can show as statically reachable while being practically unreachable, because it sits behind a feature flag that's disabled in production, a configuration branch that's never selected, or an admin-only code path gated by authentication your attacker never clears. Static analysis has no visibility into runtime configuration, so it will mark such a path reachable even though it never actually executes in a given deployment. This is a structural property of any static, source-only approach — it establishes what's theoretically possible given the code as written, not what's operationally true given how the software is configured and run.
How Safeguard Helps
Understanding reachability mechanics matters because it clarifies exactly what a "reachable" or "no path found" label is and isn't telling you — a claim about static call graphs within a single codebase, not a verdict on real-world exploitability across your whole environment. Safeguard approaches software supply chain security from that same starting point: a vulnerability finding is only as useful as the context around it.
Safeguard correlates SBOM and dependency data across an organization's full repository and pipeline footprint, so a vulnerable package's presence — and its call paths — can be tracked consistently across services rather than repo by repo. That matters directly for the cross-service blind spot static reachability tools have: when a vulnerable function is only invoked via a call from another internal service, fleet-wide dependency and provenance visibility helps close the gap that single-repo call-graph analysis structurally can't cover. Safeguard also tracks build and release provenance so teams can see not just whether a vulnerable function exists in source, but whether the artifact containing it actually shipped to production, and layers in policy enforcement and exception workflows so triage decisions — reachable, not reachable, or accepted risk — are recorded and auditable rather than living only in a scanner's dashboard. The goal isn't to replace static reachability analysis but to give the teams using it the organization-wide context needed to act on its results with confidence.