← All guides

Reachability: can your code actually call it

Cross-cuttingDraugr: available today

In plain terms#

Your project depends on a library. The library has a known vulnerability. A scanner reads your lockfile, matches the version against an advisory database, and reports it.

What the scanner has not told you is whether your code ever calls the vulnerable part. Most libraries are large and most projects use a corner of one. The advisory is about a specific function; you may never call it, and may not call anything that calls it.

Reachability analysis answers that question by building a call graph, every function that calls every other function, starting from the entry points of the code that ships, and checking whether any path arrives at the vulnerable function.

Why it matters#

It is the difference between a backlog and a work queue.

A dependency scan on a mature project returns hundreds of findings, most of them rated high, almost all of them in transitive dependencies nobody on the team chose directly. Sorted by severity, that list is unusable, because everything is high and so nothing is first.

Reachability splits it. The findings your code can reach are a much smaller set, and they are the ones where the severity rating means what it says. The rest are still real, since a future release might call into them and the dependency still needs upgrading, but they are not what you do this morning.

It also makes a gate practical. A pipeline that fails on every high-severity dependency finding gets switched off within a week. One that fails on the findings your code reaches, while still reporting the others, is one a team can live with.

Where it fits#

Reachability is a prioritization signal, not a detection one. The scanner still finds the vulnerability; reachability decides how much of your attention it deserves. It belongs with the other signals that describe the world around a finding rather than the flaw itself:

  • EPSS and KEV, which asks whether anyone is exploiting this, anywhere
  • Exposure and criticality, which ask how much this component matters and who can get to it
  • Reachability, which asks whether this code path can run here at all

These pull in different directions and that is the point. A vulnerability being exploited in the wild outranks a call graph's failure to find a path to it, because one is an observation of what is happening and the other is an inference about what could.

How it works#

The analyzer loads the packages that are in the build, resolves every call, and walks outward from the entry points. When it reaches a function an advisory names, it records the path it took to get there.

That path is the evidence, and it is what separates a reachability claim worth acting on from a number in a marketing table. A verdict that says "not reachable" and cannot show its working is asking you to take its word for a decision you are accountable for.

Common pitfalls#

  • Coverage varies enormously by language. A call graph needs the analyzer to understand how calls are resolved, and that is far easier in a statically typed, statically linked language than in one where any string can become a method name at runtime. Treat "we do reachability" as a question about which languages, not a yes or no.
  • Static analysis cannot see dynamic dispatch. Reflection, code generation, plugin loading, a method name assembled from configuration. Each of these is a call the analyzer cannot follow. Reachability is evidence about how the code is called today, not proof that a flaw cannot be triggered.
  • "No answer" is not "not reachable". This is the one that quietly does damage. An analyzer that failed to run, could not parse the project, or never covered a dependency produces the same silence as one that looked and found nothing. If a tool reports two states rather than three, you cannot tell those apart, and the failure mode is a report saying everything is fine.
  • It is true for one revision. A reachability verdict describes the code as it was when it ran. The call that makes something reachable can be written tomorrow, which is why a verdict should carry the date it was computed.
  • Reachable is not the same as exploitable. Your code calling a vulnerable function does not mean an attacker can control the input that reaches it. Reachability narrows the list; it does not finish the analysis.
How Draugr fits. Draugr runs govulncheck, the Go team's analyzer, over the Go modules in your repositories. You enable it beside the setting that ranks findings in the other direction, not in a list of scanners:

config:
  reachability:
    analyzers: [govulncheck]

That placement is deliberate. Every entry in a control's scanner block adds findings; an analyzer named here adds none and ranks findings you already have downward, which can turn a failing gate green. Enabling that should look like what it is, somewhere whoever owns the gate sees it in a diff.

Every Go dependency finding is then marked reachable, unreachable or undetermined, and a reachable one carries the call path from your own code to the vulnerable function, in the report and in the SARIF.

Three verdicts, not two, for the reason in the pitfalls above: a dependency Draugr's analyzer never covered is reported as undetermined and counted separately, so an analysis that did not run can never be read as one that found nothing. A verdict ranks a finding; it never removes one. An unreachable finding keeps the severity its scanner gave it, drops one priority band, and stays in the report, because suppressing a finding in Draugr records that a person decided, with a name attached, and an inference is not a decision. Where KEV or EPSS have already raised a finding, that wins. See reachability for Go in the docs.

The run says how the analysis went, with a row per analyzer:

Reachability:
  govulncheck  2 reachable, 2 unreachable
  Unreachable findings are ranked down in priority, not removed from the report.

And a finding whose band moved accounts for itself, the same way one raised by KEV or EPSS does:

P1  high  7.5  CVE-2022-32149  sca  trivy  checkout/go.mod
    → reachable: main → ListenAndServe → Serve → ServeHTTP → handler → preferred → ParseAcceptLanguage
P2  high  7.5  CVE-2020-14040  sca  trivy  checkout/go.mod
    ↓ ranked as medium · the vulnerable code is never called

Note that the severity is high on both. Reachability feeds the band and never rewrites what the scanner reported.

A repository with no Go module in it says so rather than looking unexamined, which matters in a polyglot project where only some repositories get verdicts:

Measured against:
  sca  govulncheck · coverage this repository has no go.mod, so its findings carry no verdict

Reachability in Draugr covers Go. For every other ecosystem the sca control reports the package-level answer, which is the honest one until an analyzer has looked.

Keep learning#