A security team triaging a Snyk Open Source scan often sees the same pattern: a dependency tree with 40 transitive packages, a dozen flagged CVEs, and no way to tell which ones actually matter without reading source code line by line. Snyk's reachability analysis was built to answer that question automatically — but it does not answer it equally well everywhere. A Java monorepo built with Maven gets a materially different (and generally more complete) analysis than Snyk Python reachability produces for a service using dynamic imports, and a JavaScript codebase sits somewhere in between. That gap is not a bug; it is a direct consequence of how each language exposes (or hides) its call structure to static analysis tooling. This post walks through the mechanics Snyk has publicly documented for how reachability is computed, why Java was the first language supported, and where JavaScript and Python coverage still runs into structural limits.
What does "reachability analysis" actually mean in Snyk?
Reachability analysis in Snyk is a static determination of whether the vulnerable function inside a flagged open source dependency is ever called, directly or transitively, from your application's own code. Rather than stopping at "your lodash version has CVE-2021-23337," Snyk builds a call graph starting at your application's entry points and walks it outward through your direct and transitive dependencies, checking whether execution can ever reach the specific vulnerable function identified in the advisory — not just the vulnerable package as a whole. Snyk publishes the result as one of a small set of verdicts on the vulnerability card: reachable functions are called somewhere in the traced paths, "no path found" means the call graph analysis completed but found no route to the vulnerable code, and issues outside the supported scope are simply not evaluated for reachability at all. This is a static, source/build-artifact-level technique — it inspects code and dependency metadata rather than instrumenting a running process, which is the key difference from runtime/IAST-style reachability used by some other tools.
Why did Java get reachability analysis first?
Snyk introduced reachability analysis for Java ahead of other ecosystems because the JVM's build and packaging model gives static analysis the cleanest possible input. Maven and Gradle projects resolve a fully pinned dependency tree at build time, and compiled artifacts (JARs) expose explicit method signatures and class hierarchies that a call-graph builder can walk with high confidence. Because Java is statically typed and uses explicit imports and interfaces rather than duck typing, Snyk's analysis can match a call site to a specific method in a specific class with far less ambiguity than in a dynamically typed language. This is also why, in Snyk's documentation, Java (via Maven and Gradle) has historically had the deepest and most mature reachability coverage of any supported ecosystem: the language's own design does most of the disambiguation work that the analyzer would otherwise have to infer.
How does reachability analysis work for JavaScript and TypeScript?
Reachability for JavaScript and TypeScript works the same way conceptually — static call-graph construction from application entry points through npm or Yarn-resolved dependencies — but the graph is harder to build precisely because the language itself is more permissive. CommonJS's require() calls and dynamic property access, prototype-based inheritance, and the fact that a single npm package can ship radically different code paths depending on runtime conditions all make it harder for a static analyzer to prove, rather than guess, that a given call site resolves to a given function. TypeScript's type annotations help narrow this down when they're present and accurate, but plain JavaScript projects give the analyzer less to work with. In practice this means JavaScript/TypeScript reachability verdicts lean more heavily on the "no path found" bucket than Java's do, not because the vulnerabilities are less real, but because the tool is more often unable to prove a definitive path either way through highly dynamic call patterns like callback registries, event emitters, or plugin-style require(variableName) loading.
What makes Snyk Python reachability structurally harder?
Python reachability is constrained by the same dynamic-language problem as JavaScript, but pushed further by Python-specific patterns like duck typing, monkey-patching, and importlib-based dynamic imports that are common in frameworks such as Django and Flask. A static call graph in Python has to contend with the fact that a function reference passed as an argument, a class method overridden at runtime, or a module imported by string name from a configuration file are all first-class, everyday patterns rather than edge cases — which means the analyzer frequently cannot prove non-reachability with the same confidence it can in Java. Snyk's pip-based Python support for reachability was rolled out after Java and JavaScript/TypeScript, reflecting both the engineering difficulty of building a usable call graph for a language this dynamic and the ecosystem's dependence on native extensions (C-extension-backed packages like numpy or cryptography) that sit outside what pure Python source analysis can trace at all. The practical effect is that Python coverage today tends to be narrower in scope — concentrated on pure-Python call paths — than the coverage available for equivalent Java or JavaScript projects.
Does a "no path found" result mean a vulnerability is safe to ignore?
No — "no path found" is an absence of proof, not proof of absence, and Snyk's own labeling reflects that distinction rather than asserting safety. It specifically means the analyzer could not trace a call path to the vulnerable function given the static information available, which can happen because the path genuinely doesn't exist, or because the path exists but is constructed dynamically in a way the analyzer can't follow — a distinction that matters enormously for risk acceptance decisions. This is why reachability status should be read as a prioritization signal, not a binary safe/unsafe gate: a "reachable" result is a strong, actionable signal to fix now, while "no path found" should shift a vulnerability lower in the queue without removing it from the queue entirely, especially in Python and JavaScript codebases where dynamic-loading patterns are common enough to routinely defeat static tracing.
How should teams account for these coverage gaps across a polyglot stack?
Teams running Java, JavaScript, and Python services side by side should treat reachability scores as directly comparable only within a language, not across languages, because the underlying analysis has different blind spots in each ecosystem. A Java service showing mostly "reachable" or definitively "not reachable" verdicts and a Python service showing a high proportion of "no path found" results are not necessarily telling you the Python service is riskier — they may simply reflect that Python's dynamic import patterns are harder to trace statically. The practical takeaway is to pair reachability output with complementary signals — actual runtime dependency usage, exposure of the service to untrusted input, and the severity/exploitability of the underlying CVE — rather than triaging purely on the reachability label, particularly for Python and JavaScript codebases where "no path found" volume is structurally higher regardless of true risk.
How Safeguard Helps
Safeguard's software supply chain security platform is built around the same principle this post has been describing: static reachability signals are valuable but language-dependent, and treating them as uniformly reliable across a polyglot codebase creates blind spots. Safeguard correlates dependency and vulnerability data across your Java, JavaScript, and Python services with your actual build artifacts, SBOMs, and deployment context, so a "no path found" verdict in a dynamic language doesn't silently drop a real risk from view. Rather than asking teams to manually normalize prioritization across ecosystems with different analysis maturity, Safeguard layers exposure, provenance, and runtime context on top of vulnerability data to help security and engineering teams decide what actually needs attention first — whether the underlying scanner's call-graph analysis was able to reach a definitive verdict or not. For organizations running mixed-language stacks, that consistency in how risk gets prioritized, independent of any single scanner's per-language coverage limits, is often the difference between a backlog that gets triaged and one that gets ignored.