Vulnerability Management

Reachability Analysis vs. SCA: Which Reduces Your Backlog?

SCA lists every CVE in every dependency. Reachability filters to the ones your code actually invokes. Here is how the two compare on a real backlog.

Shadab Khan
Security Engineer
8 min read

Software Composition Analysis tells you every CVE associated with every dependency in your application, transitive or not, reachable or not. Reachability analysis tells you which of those CVEs correspond to code your application actually calls. The first produces a backlog. The second produces a worklist.

Most teams that have been running SCA for more than six months have the same complaint: the tool is technically correct and operationally useless. A typical mid-sized service sits on 1,200–4,000 open CVE findings, the queue grows faster than it shrinks, and engineering pushes back on every sprint's batch of "critical" upgrades. Reachability is the mechanism that makes SCA output tractable.

What does SCA actually do?

SCA inventories your dependencies and matches each one against a vulnerability database. The scanner reads your lockfile — package-lock.json, go.sum, poetry.lock, Gemfile.lock — or parses compiled artifacts, builds the set of components present, and for each component queries a vulnerability feed (NVD, OSV, GHSA, a commercial equivalent) to enumerate every known CVE that affects that version.

The output is a list of (component, version, CVE, severity) tuples. That is valuable — it is the raw material for everything downstream — but it is missing the piece that matters for prioritization: whether the vulnerable code path is ever executed by your application. SCA says log4j-core-2.14.1 is present and CVE-2021-44228 applies. SCA does not say whether your code ever calls a Logger method with user-controlled input.

This is by design. SCA was built in an era when the unit of delivery was a product, and the question was "which components are in my product?" Reachability is the answer to a newer question: "which of those components is my product actually using?"

Why does SCA alone produce such noisy backlogs?

SCA produces noisy backlogs because most dependencies in a modern project are only partially used, and a substantial fraction of CVEs are in code paths no one ever reaches. The math is blunt. A typical Node.js service has 1,500 transitive dependencies for 100 direct ones. A typical Python service has 400 transitive dependencies for 40 direct. At those numbers, the arithmetic of "every CVE in every version" produces four-digit backlogs trivially.

Several structural factors compound the signal-to-noise problem. CVEs are scored under CVSS against the worst-case invocation pattern, not your invocation pattern — a deserialization CVE is scored assuming attacker-controlled input flows in, even if you only deserialize constants from your own codebase. Many libraries are imported only for narrow utility functions, while CVEs live in modules you never touch. Transitive-transitive dependencies often exist only to satisfy build-time tooling and never ship into runtime code. And version-range matching in vulnerability databases is permissive: an advisory says "affects versions < 2.14.1" and every 2.x.y in your graph lights up, even if the vulnerable code was introduced in 2.12.0.

The empirical result, validated repeatedly across public studies and internal audits in 2024–2025, is that 70–90% of SCA findings against application code are non-exploitable in the application's specific configuration. That is not a claim that the CVEs are invalid; it is a claim that the component-level granularity of SCA is too coarse to tell you which ones matter.

What does reachability analysis add?

Reachability analysis builds a call graph of your application and cross-references it with the vulnerable functions identified in each CVE. A CVE is classified as reachable if there is an execution path from your application's entry points to the vulnerable function; unreachable otherwise.

There are two rough styles. Static reachability analyzes the compiled artifact or bytecode, constructs an inter-procedural call graph, and traces paths from entrypoints (HTTP handlers, CLI main, exported functions) to the known-vulnerable function in the dependency. Dynamic reachability runs the application under instrumentation (eBPF, a profiling runtime) and records the set of functions actually invoked during representative workloads. Static reachability is more exhaustive; dynamic is more accurate about real traffic patterns. Production systems increasingly combine both.

The depth of analysis matters. A shallow reachability check that only considers direct dependencies catches some noise but misses the transitive cases that dominate a backlog. A call graph that traces through several levels of transitive dependency — ten, fifty, a hundred levels deep — is what actually moves the numbers.

How much does reachability cut the backlog?

Reachability typically cuts an SCA backlog to 20–40% of its original size, with the larger reductions on services that have many transitive dependencies and narrow runtime call surfaces. The headline number most vendors quote is 60–80% noise reduction, and in our experience across mid-sized engineering teams in 2025, that range is defensible when the analysis runs deep enough.

Concretely, on a representative Java Spring Boot service with 2,100 transitive dependencies and 180 CVE findings from SCA:

  • 180 raw findings out of the scanner
  • 72 findings in dependencies that are loaded at runtime (as opposed to build-time only)
  • 34 findings where the vulnerable function exists in a loaded module
  • 11 findings where the vulnerable function is actually in a reachable call path from application entry points
  • 4 of those 11 are reachable and flow attacker-controllable input

The ratio shifts by language and framework. Java and Python ecosystems tend to show higher reduction (80%+) because they pull in heavy dependencies of which only slices are used. Rust and Go tend to show moderate reduction because dependencies are typically smaller and more targeted. JavaScript shows high reduction because of the long tail of transitive devDependencies that ship into bundles but are not invoked.

The point is not the exact ratio. The point is the absolute number of findings that your engineers look at drops from 180 to 11, and the CVEs at the top of that 11 are the ones actually worth a Tuesday-morning patch cycle.

When does reachability not help?

Reachability is weaker against code paths that a static call graph cannot see — dynamic loading, reflection, eval, plugin systems, configuration-driven dispatch, and most interpreted runtime magic. If your application uses Java reflection to instantiate classes by string name at runtime, a static call graph cannot prove reachability of those classes. If you ship a plugin architecture that loads arbitrary user code, the plugins are out of scope.

Three concrete cases where reachability is weaker:

Dynamic class loading and reflection. Spring, Hibernate, Jackson, and similar frameworks heavily use reflection. A good reachability engine annotates these framework entry points with coarse-grained reachability (anything a @Controller can reach) and narrows from there, but the result is more conservative than for pure statically-dispatched code.

Runtime-configured pipelines. Tools like Apache Camel, Airflow, or any DAG engine resolve the actual call graph from YAML or code at startup. Static analysis sees a generic executor calling a generic task; it cannot tell which tasks exist in a given deployment without analyzing the configuration.

Data path vulnerabilities. Some CVEs — SQL injection in an ORM, deserialization of attacker-controlled payloads, prototype pollution — are reachable via data, not by direct function call. Reachability confirms the function is called; taint analysis is required to confirm attacker control of inputs.

In these cases, reachability should be layered with taint tracking and runtime observation rather than relied on alone. The right posture is to trust reachable-clean findings less aggressively in dynamic-loading environments, and to keep those findings on a slower triage queue rather than closing them outright.

How do you operationalize reachability without breaking SCA?

Run SCA and reachability together, use reachability as a prioritization layer rather than a suppression layer, and keep the raw SCA output queryable for compliance. The two outputs complement each other — SCA produces the complete inventory that regulators and auditors expect, reachability produces the operational queue that engineering actually works from.

The workflow most teams converge on by quarter two of deployment:

  1. SCA runs on every build, producing the complete CVE inventory (stored for audit, SBOM, and VEX authoring).
  2. Reachability runs on every build, producing the filtered "actually invoked" subset.
  3. SLA policies are stratified: reachable critical in 7 days, reachable high in 30 days, unreachable critical tracked quarterly, unreachable high tracked at release cadence.
  4. Unreachable-but-critical findings auto-populate VEX statements with vulnerable_code_not_in_execute_path justification, ready for PSIRT sign-off.
  5. Newly-reachable findings (from a dependency update or code change that brought previously-cold code into a hot path) are flagged for immediate review.

The biggest operational win is not the CVE count — it is the change in the conversation with engineering. "Patch these four things this week" is a conversation. "Patch these 180 things, some of them are critical, some maybe aren't" is an argument.

How Safeguard.sh Helps

Safeguard.sh combines SCA and reachability analysis in one pipeline, so the raw CVE inventory and the prioritized worklist come out of the same scan without double-tooling. Reachability analysis traces call graphs through 100 levels of dependency depth, cutting 60–80% of CVE noise before it lands in an engineer's queue and surfacing only the vulnerabilities your code actually invokes. Griffin AI takes those reachable findings and autonomously opens, tests, and proposes patch PRs, compressing reachable-critical SLAs from weeks to hours. The SBOM module records the complete component inventory on every build for audit and compliance, while VEX statements are drafted automatically for unreachable findings with the appropriate justification. Container self-healing keeps base image drift from silently reintroducing previously-patched CVEs between releases, and the TPRM module extends the same reachability-driven filtering to vendor SBOMs you ingest.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.