reachabilitySCAAppSec by Derek Voss

Reachability Analysis vs. Advisory Matching: Why Most SCA Tools Are Telling You Half the Story

Advisory-only SCA tools match CVE databases against your lockfile. That's necessary but not sufficient. Here's what you're missing when you stop there.

Reachability Analysis vs. Advisory Matching: Why Most SCA Tools Are Telling You Half the Story

Every SCA tool in the market will tell you when a package in your lockfile has a known CVE. That part is solved — the NVD, GitHub Advisory Database, and OSV are comprehensive, well-maintained, and freely accessible. The hard part isn't knowing which packages are vulnerable. The hard part is knowing whether your specific application ever calls the code that's actually vulnerable.

That gap — between "this package has a CVE" and "your code reaches the vulnerable function" — is where advisory-only SCA tools run out of signal. And it's the gap responsible for most of the alert fatigue that drives developers to mute their security tools entirely.

What advisory matching actually does

Advisory-only SCA works like this: parse your package-lock.json (or yarn.lock, Pipfile.lock, go.sum — pick your ecosystem), extract the full list of packages and versions, then cross-reference each against one or more advisory databases. Any package version that falls within a CVE's affected range triggers an alert.

This is not wrong. It's a necessary first step. The problem is that it treats every vulnerable package as equally urgent, regardless of whether your application ever exercises the code path that contains the vulnerability. A CVE in a function you never call is not an active risk — it's potential risk. The distinction matters enormously when you're triaging a queue of 80 alerts every morning.

Consider a concrete scenario: a backend service built on Express, with lodash as a transitive dependency several levels deep in the tree. A moderate-severity CVE is published against a specific lodash prototype pollution vector. Advisory-only tools flag it. But if your service never calls the lodash functions that expose the vulnerable codepath — perhaps your dependencies use lodash for array utilities that aren't affected — then the flagged CVE has zero exploitability surface in your deployed application. Advisory matching cannot tell you this. It can only tell you the version is in range.

The call graph intersection problem

Reachability analysis takes a different starting point. Instead of asking "which packages in your lockfile have CVEs?", it asks: "which CVEs in your lockfile are reachable from your application's actual entry points?"

The mechanics require two data structures. First, a dependency graph — this is what advisory-only SCA builds. It maps your packages, their versions, and their transitive relationships. Second, a call graph — this is what most SCA tools skip because it's harder to compute. A call graph traces function-level calls from your application's entry points (HTTP handlers, CLI entrypoints, background job runners) through your own code and into the package functions they invoke.

The reachability verdict comes from the intersection: does any call path from your entry points reach a function that's within the vulnerable code of a flagged package? If yes, the CVE is reachable. If no, it isn't — regardless of whether the package version is in the advisory's affected range.

This is a fundamentally different question than advisory matching answers. And it requires static analysis of your codebase — not just lockfile inspection.

Why most tools stop at the lockfile

Building a call graph is significantly more expensive than parsing a lockfile. For dynamically-typed languages like JavaScript/TypeScript or Python, call graph construction involves type inference, scope analysis, and handling of dynamic imports and runtime dispatch patterns that make precise analysis hard. For compiled languages like Go or Rust, the analysis is more tractable but still requires understanding the build graph.

Advisory-only tools optimized for speed and ecosystem breadth at the cost of depth. They can scan a package-lock.json in milliseconds. They can cover 50 ecosystems. They don't need to touch your source code at all — which also appeals to buyers with code-retention concerns.

We're not saying advisory-only SCA is bad — it's a reasonable first layer and every engineering org should have it. What we're saying is that stopping there leaves you with an unfiltered firehose of alerts, most of which aren't actionable given your specific codebase. That unfiltered signal is what drives the alert fatigue cycle: high volume → ignored alerts → real vulnerabilities missed.

What reachability changes in practice

Take a mid-size Node.js API service — say, 40,000 lines of TypeScript, with a lockfile containing around 900 packages. Run advisory matching against the lockfile and you might surface 30-40 CVE alerts across severity levels on any given week, depending on advisory database freshness. That's a triaging burden that lands on either your AppSec team or, more commonly, developers who have other work to do.

Run a call graph intersection on the same codebase, and that alert set collapses substantially. In our experience analyzing real application codebases, somewhere between 60-80% of CVE advisories in a typical lockfile have no reachable call path in the application that actually ships to production. That's not a number we're claiming precision on — it varies by codebase, ecosystem, and how broad the vulnerable function's API surface is. But the directional reality is consistent: most advisory hits don't represent active exploitability.

The remaining 20-40% — the alerts where a real call path exists — those deserve your immediate attention. Reachability analysis does not reduce the severity of CVEs. It filters the signal so that the CVEs you action are the ones that matter.

Partial reachability: the nuance worth understanding

Reachability analysis is not binary in all cases. Some scenarios are genuinely harder to classify:

  • Optional code paths: A vulnerable function might be reachable only when a specific feature flag is enabled or a specific configuration option is set. Is that "reachable"? It depends on your deployment configuration, not just your code structure.
  • Dynamic dispatch: Languages with heavy use of reflection, dynamic method dispatch, or plugin systems make static call graph construction imprecise. A call to a dynamically-loaded module may or may not reach the vulnerable function.
  • Test code: Many test suites import packages with CVEs as dev dependencies. A call graph that includes test code will surface reachability in tests — is that a production risk? Usually not, but the analysis needs to scope appropriately.

A well-designed reachability engine surfaces these ambiguities with a confidence indicator rather than forcing a binary yes/no. "REACHABLE via express handler at server.js:142" is higher confidence than "POTENTIALLY REACHABLE via dynamic require — verify manually." Both are more useful than "package version in CVE range."

The advisory matching + reachability combination

The right answer is not "replace advisory matching with reachability analysis" — it's "run advisory matching first to identify candidate CVEs, then apply call graph reachability to triage them." Advisory matching is fast and comprehensive. Reachability adds the depth that converts a raw advisory hit into an actionable severity assessment for your specific codebase.

What this means architecturally: your SCA pipeline needs both a lockfile/advisory phase and a static analysis phase that can reason about your application's actual code structure. The output should be a reachability verdict attached to each advisory hit — not a separate alert stream, but a signal that elevates or deprioritizes the advisory result based on call graph evidence.

The tools that get this right don't just ship faster remediation — they build the kind of developer trust that keeps security checks in the pipeline rather than muted or bypassed. High-signal alerts get fixed. Noise gets ignored. The goal is to make every alert your developers see worth their attention.

That's the half of the story most SCA tools are still missing.

False Positives Kill Developer Trust: The Hidden Cost of Noi...