Reachability Analysis Explained: Used vs Vulnerable Function

A package your code calls all day can still never touch the one function the CVE is about.
Reachability analysis means five different things. The gap between a used dependency and a reached vulnerable function, where analysis breaks, and triage rules.
June 29, 2026

"Reachable" is one of the most overloaded words in application security. One tool uses it to mean the package is in your lockfile. Another means your code imports it. A third means your code calls the specific function the CVE is about. Those are very different claims, and treating them as the same one is how teams either drown in findings or quietly ignore real ones.

Reachability analysis is worth doing well because most vulnerable dependencies never execute the vulnerable code in your application. Getting it right means knowing exactly which question each signal answers.

This post explains the levels of reachability, how function-level analysis works, where it breaks, and how to use it in triage without turning it into a way to skip work.

What reachability analysis asks

A CVE in a dependency describes a flaw in specific code: a parsing function, a deserializer, a template renderer. The flaw only matters to you if your application can run that code.

Reachability analysis tries to answer that. The problem is that "can run" has several levels, each stronger than the one before.

The levels of reachability

  1. Declared. The package appears in a manifest. This is where most scanners stop. It proves nothing about execution.
  2. Resolved. The package is installed, directly or transitively, at a vulnerable version. Necessary, still not proof.
  3. Loaded. The package is present in a running deployment. A library that ships in an image but belongs to a service with no active deployment is not running anywhere.
  4. Used. Your code, or a library your code calls, invokes some function in the package. This is "used dependency" reachability: the package is live in your call graph.
  5. Vulnerable function reached. There is a call path from your code to the specific function or method the advisory identifies as vulnerable. This is the claim that actually matters for exploitability.

The gap between level 4 and level 5 is large. A project can use a large, popular library heavily and never touch the one deserialization helper that carries the CVE. A tool that reports "used" as "reachable" marks that finding urgent anyway.

"Used dependency" vs "reached vulnerable function"

These two are confused most often, so it helps to be concrete.

Say your service depends on a JSON library, and a CVE affects its polymorphic deserialization path, which is only triggered by a specific configuration and entry point. Your code calls parse() and stringify() all day.

  • Used dependency: yes. The library is called constantly.
  • Vulnerable function reached: only if some call path leads into the deserialization code the advisory names. Often it does not.

Package-level reachability would flag this finding. Function-level reachability can tell you it is not reached, and show you the evidence.

The reverse case matters too. A package your code never imports directly can still be reached, because a library you do call reaches into it. Function-level analysis has to follow calls through transitive dependencies, as well as your own source.

How function-level reachability works

Function-level reachability needs two inputs that line up.

1. The vulnerable symbols

For each advisory, which functions, methods or classes contain the flaw? Some ecosystems publish this directly. The Go vulnerability database lists affected symbols, which is how govulncheck reports only vulnerabilities your code calls. For most ecosystems the symbols have to be derived, usually by analyzing the fix commit or the difference between the vulnerable and fixed releases.

2. A call graph of your application

A graph of which functions call which, across your code and the libraries it depends on. The analysis then searches for a path from your entry points (request handlers, CLI entry points, message consumers) to any vulnerable symbol.

If both inputs exist and no path is found, the finding can be marked as having no reachable vulnerable functions. If a path is found, the path itself is the evidence: you can read it hop by hop and see exactly how your code gets there.

Where reachability analysis breaks

Static call graphs are an approximation. Knowing the failure modes tells you how far to trust a "not reachable" result.

  • Dynamic dispatch and reflection. Java reflection, Python's getattr, JavaScript's dynamic property access and plugin loaders can call code the static graph cannot see.
  • Frameworks calling your code. Web frameworks, dependency injection containers and serializers invoke handlers and constructors by convention or annotation. A graph that does not model the framework misses real entry points.
  • Callbacks and higher-order functions. A vulnerable function passed as a value and invoked later is hard to track.
  • Missing symbol data. If the advisory's vulnerable functions are not known, there is nothing to reach. That is "unknown", not "safe".
  • Umbrella and split packages. Some projects ship a meta-package whose code lives in sibling distributions. The advisory names one package, and the vulnerable code lives in another.
  • Unsupported languages. If no call graph is built for a language, function-level reachability cannot run at all.

The rule that follows: when the analysis cannot decide, it should err toward reachable. A tool that treats "could not analyze" as "not reachable" is suppressing findings, not triaging them.

Using reachability in triage without skipping work

Reachability should set the order of work, not decide whether work happens. A few rules keep it honest:

  • Unreachable means defer, not close. The vulnerable code is still in your build. A code change next week can create a path to it. Keep the finding, lower its urgency, and let it re-score when the code changes.
  • Separate "not reachable" from "unknown". Report them as different states. If your dashboard lumps them together, you cannot tell good news from missing data.
  • Combine with runtime and exposure. A reached vulnerable function in a service with no deployment is less urgent than one on an internet-facing Tier 1 service. Reachability is one input, alongside whether the code is loaded, exposed and authenticated.
  • Demand the path. A reachable verdict should come with the call path. A not-reachable verdict should come with the list of vulnerable symbols that were checked. "Trust us" is not evidence.
  • Watch for known exploitation. An entry in CISA's Known Exploited Vulnerabilities catalog on a reachable finding should jump the queue immediately.
  • Still fix it eventually. Unreachable vulnerabilities are cheap to remove when an automated upgrade is available. Defer is a priority, not a verdict.

Reachability is not exploitability

A reached vulnerable function means the flaw can run. It does not mean an attacker can control the input that triggers it, that the service is exposed, or that a mitigation is not in place. Exploitability adds exposure, authentication, input control and compensating controls on top of reachability.

That is why reachability works best as one signal in a risk model, not as a replacement for one. For how these signals combine, the case for fixing everything in the right order goes further.

How Heeler determines reachability

Heeler combines two independent checks and treats a vulnerability as reachable only when both agree.

  • Runtime reachability: the vulnerable library is loaded in a running deployment, established by correlating your deployed services with the dependency.
  • Static, function-level reachability: Heeler resolves the vulnerable functions for the advisory and traces your call graph to see whether your code reaches any of them. When the symbols exist and nothing reaches them, the finding is mitigated as having no reachable vulnerable functions.

Where function-level analysis does not apply, runtime reachability decides on its own and errs toward reachable, so nothing is silently dropped. The finding page shows the reachability graph and lists the specific reachable vulnerable symbols, and the Function Reachability filter narrows a list to findings whose vulnerable function your code reaches.

Reachability then feeds Autotriage, which places each finding in Urgent, Plan or Defer alongside exposure, business impact and threat signals. The SCA prioritization docs cover the model, and supported technologies lists language coverage.

FAQ

What is reachability analysis in SCA?

Reachability analysis determines whether your application can actually execute the vulnerable code in a dependency. The strongest form checks whether a call path exists from your code to the specific vulnerable function named in the advisory.

What is the difference between a used dependency and a reachable vulnerability?

A used dependency is one whose functions your code calls at all. A reachable vulnerability requires a call path to the specific function that contains the flaw, which is a much stronger and more precise claim.

Can I ignore unreachable vulnerabilities?

No. Defer them. The vulnerable code is still in your build, and a later code change can make it reachable, so the finding should stay open and re-score as code changes.

Why would reachability be "unknown"?

Reachability is unknown when the vulnerable functions for an advisory are not identified or no call graph exists for the language. Unknown should be treated as potentially reachable, not as safe.

Is reachability the same as exploitability?

No. Reachability says the vulnerable code can run. Exploitability also depends on exposure, authentication, whether an attacker controls the input, and whether a mitigation is in place.

See the path behind the verdict

Heeler traces whether your code reaches the vulnerable function, confirms whether the library is loaded in production, and shows the evidence on the finding. Get a demo.

What’s new on Heeler
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Related resources

See All Resources