A dependency scanner tells you that version 4.17.15 of a library has a prototype pollution bug. What it cannot tell you is whether your code ever calls the function the bug lives in. For most dependency findings that is the question that decides the priority, and for most teams it is answered, if at all, by an engineer reading code at the end of a long ticket queue.

In short

  • Reachability asks one question per dependency finding: does this repository use the function the advisory says is vulnerable?
  • Four answers, each with its evidence: reachable (the call sites, file and line), imported but not called, not imported at all, or unknown with the reason.
  • Only evidence moves a score down. “Not imported” takes 2 points off, “imported, not called” 1 point; “reachable” adds half a point; “unknown” changes nothing.
  • It is a static index matched by name, not a full call graph. Transitive dependencies stay unknown, and we say so on every finding.
  • Covered today: JavaScript and TypeScript (npm), Python (PyPI), Java and Kotlin (Maven).

The situation

An illustrative payments service has 140 open dependency findings. Twenty-two are rated High or Critical. The AppSec lead knows from experience that many of them are in code paths the service never touches, but cannot prove which, and the developers have no time to read twenty-two advisories against the code base. So the team does what most teams do: it upgrades whatever upgrades cleanly and lets the rest age past their SLA.

Industry
data

Multiple vendor and academic studies of open-source dependency use have reported that a large share of known-vulnerable dependencies are never reached from application code. The exact proportion varies widely by ecosystem, method and sample, so treat any single headline percentage with caution; the direction is consistent.

Sources: published reachability analyses from software composition analysis vendors and academic work on dependency call graphs; figures differ by methodology.

Why scanners miss it

A software composition analysis tool reads the manifest and the lockfile. It knows which package versions are installed and which advisories name them. It does not read your source code, so it cannot tell the difference between a library that is imported on every request and one that was added three years ago for a script nobody runs. Severity, the only signal it has, is the same in both cases.

TODAY Every vulnerable version is equally urgent

The finding says the package version is affected. Whether the vulnerable function is ever called is left to whoever picks up the ticket.

WITH RISK CONTEXT The code’s own usage decides

The finding says which vulnerable functions the advisory names, where the package is imported, and where (if anywhere) those functions are called.

How InsightFortify answers it

Every repository scan builds a usage index of the checkout. Nothing is executed. Python is read through its syntax tree; JavaScript and TypeScript through a tokenizer that understands strings, templates, comments and regular-expression literals, so a commented-out import does not count; Java and Kotlin through their imports and per-file calls. The index records which packages each file imports and which of their functions it calls or references.

For each npm, PyPI and Maven dependency finding, the vulnerable functions come from the advisory itself where OSV lists them, or from a curated map of about ninety well-known advisories. The finding is then classified against the index.

sequenceDiagram participant W as Worker participant R as Repository checkout participant I as Usage index participant O as OSV advisory data participant F as Finding W->>R: clone and scan W->>I: build index - imports, calls, references W->>O: vulnerable functions for this advisory O-->>W: e.g. lodash.template W->>F: classify against the index alt function called F->>F: reachable, plus 0.5, call sites as evidence else imported, function unused F->>F: imported_not_called, minus 1.0 else direct dependency never imported F->>F: not_imported, minus 2.0 else evidence incomplete F->>F: unknown, no change, reason stated end

Each classification carries its evidence or its reason, and appears as a named score factor.

StatusWhat it meansScoreEvidence shown
ReachableA vulnerable function is called or referenced (passed as a callback, stored)+0.5Up to ten file:line call sites
Imported, not calledThe package is imported; none of its vulnerable functions is used through any import−1.0The import sites
Not importedA direct dependency that nothing imports and no configuration or script names−2.0What was searched
UnknownAnything that cannot be said with evidence0The reason, e.g. transitive dependency
flowchart TD A["Dependency finding
npm, PyPI or Maven"] --> B{"Advisory names
vulnerable functions?"} B -- "no" --> U["Unknown
score unchanged"] B -- "yes" --> C{"Vulnerable function
called or referenced?"} C -- "yes" --> R["Reachable
+0.5, call sites shown"] C -- "no" --> D{"Package imported
anywhere?"} D -- "yes" --> I["Imported, not called
minus 1.0"] D -- "no" --> E{"Direct dependency, evidence
complete, not loaded by name?"} E -- "yes" --> N["Not imported
minus 2.0"] E -- "no" --> U style R fill:#fee2e2,stroke:#ef4444 style N fill:#ccfbf1,stroke:#0d9488 style I fill:#ecfdf5,stroke:#10b981

The decision order. Every path that lacks complete evidence ends at unknown.

Where “unknown” is the honest answer

The design rule is that a score only goes down on complete evidence. Several situations do not give it, and the finding says which:

  • Transitive dependencies. Whether the package that pulls it in calls the vulnerable code is not analysed, so the status is unknown.
  • Packages loaded by name. Database drivers, servers, command-line tools, logging backends and framework families are often loaded through configuration rather than an import statement.
  • The module object passed around whole, star imports, and repositories too large to index completely.
  • Advisories that name no function. If neither OSV nor the curated map says what is vulnerable, there is nothing to look for.

The limits, plainly

Calls are matched by name through import bindings. A local variable that shadows an import, a function reached through another library, reflection, eval and require(variable) are not followed, and Java instance calls are matched by method name in a file that imports the class. Go, Rust, Ruby, .NET and PHP are not covered yet. This is why the negative statuses demand complete evidence and why unknown never moves a score.

What it looks like in the console

The finding page gains a panel titled “Is the vulnerable code used?” listing the vulnerable functions, where they came from, and the call or import sites. The findings list gains a reachability column and filter, so a team can ask for “P2 and reachable” or “P1 but never imported”.

A dependency finding in InsightFortify classified as imported but not called, with the factor in its score breakdown and the vulnerable-code panel
A dependency finding from our NodeGoat scan. The package is imported, the vulnerable functions are not used, and the score shows the one-point reduction.

The effect, measured honestly

On our mixed-language test application (fourteen dependency findings across npm, PyPI and Maven), the actionable P1 and P2 count fell from ten to eight, and three findings dropped by two points each because their packages were never imported. On deliberately vulnerable public applications with deep, old dependency trees, the effect is much smaller: most of their vulnerable packages are transitive, so they stay unknown. Reachability earns its keep on services with flatter, directly managed dependencies, which is most modern application code.

Rolling reachability into triageWho does what in the first month
1
Rescan the top 20 repositoriesAppSec lead

The index is built on the next full repository scan; CI uploads reuse the repository’s last index.

2
Review the not-imported listService owners

These are usually dead dependencies: removing them closes the finding and shrinks the attack surface.

3
Fix reachable P1/P2 firstDevelopers

The call sites are in the finding; the fix is usually an upgrade or replacing one call.

4
Sample imported-not-calledAppSec analyst

Spot-check a few against the code to build trust in the classification before relying on it.

5
Upgrade the rest on the normal cadencePlatform team

Lower priority is not no priority: they still reach their SLA date.

A worked example

Take an illustrative Node.js service that depends on an old version of lodash with an advisory against its template function. Two files use lodash:

// src/report.js
const _ = require('lodash');
const render = _.template(req.body.layout);   // line 12: the vulnerable function, on request input

// src/util.js
import { chunk, uniq } from 'lodash';  // imported, but template is never used here

Illustrative code. The index records both import styles and the call through the alias.

The finding is classified reachable with one piece of evidence: src/report.js:12 _.template. Half a point is added to the score, and the evidence tells the developer exactly where to look. If report.js did not exist, the same finding would be imported, not called because util.js imports lodash but never uses template, and the score would drop by one point. If no file imported lodash at all and it were a direct dependency, it would be not imported, a two-point drop and usually a sign the dependency can simply be removed.

Questions to ask any vendor that claims reachability

  • Which languages, exactly? Reachability for one ecosystem is often marketed as reachability in general.
  • Where do the vulnerable functions come from? Advisory data, a curated list, or a model that guesses? Each has different failure modes.
  • What happens to transitive dependencies? If they are marked unreachable without analysing the package in between, risk is being hidden, not removed.
  • Can I see the evidence? A reachable finding should show its call sites; an unreachable one should say what was searched.
  • Does unreachable lower the score or hide the finding? Lowering keeps it on the SLA clock; hiding takes it off the books.
Metric to trackWhy it matters
Share of dependency findings with a known statusShows how much of the estate the index can speak to; unknown is not a failure, but a large unknown share limits the benefit
Not-imported direct dependencies removedDead dependencies removed is attack surface removed, not just a lower score
Reachable P1/P2 median time to fixThe findings with call-site evidence should be the fastest to close

Where the vulnerable-function data comes from

Reachability is only as good as its knowledge of what is vulnerable. Advisory quality varies: some advisories name the exact functions or modules affected, many name only the package and version range. InsightFortify uses the advisory’s own data first, as published through OSV, and keeps it on the vulnerability record so findings are re-classified automatically when an advisory is enriched later. Where advisories are silent, a curated map covers about ninety widely seen advisories.

When neither source names a function, the finding is unknown and says so. That is deliberately conservative. The alternative, guessing which functions are vulnerable from the advisory text, would produce confident classifications that are wrong in ways nobody can check.

What it means for each reader

For the CISO

A defensible reason to fix some vulnerable dependencies later: evidence that the vulnerable function is not used.

Auditors see the factor and its evidence on the finding, not a judgment call in a spreadsheet.

For the AppSec lead

Spend review time where the code actually calls the vulnerable function.

Treat unknown as unknown: the transitive majority still needs the normal upgrade cadence.

For the board

Developer hours go to the dependency risk that can actually be exercised.

The method is transparent enough to explain in one sentence.

“Is the vulnerable code even called?” deserves an answer with evidence, or an honest “unknown”.

Reachability that lowers scores without proof would trade noise for blind spots. Ours only moves a score when the evidence is complete.

Keep reading