Skip to main content
Platform
Platform Overview How It Works
Solutions
AppSec Teams Platform Engineering Pricing Blog
Sign In Request Early Access
Dependency graphs and static scans measure different security properties
Back to blog

Why dependency graphs and static scans measure different things

By Mikael Sundqvist

There is a common conflation in application security tooling between two distinct types of analysis: dependency graphs and static code scanning. They are often used together and their outputs are sometimes presented in the same dashboard. The distinction between them matters because they answer different questions, they have different blind spots, and the third question that neither answers is the one that determines whether a finding is actually exploitable.

What a dependency graph tells you

A dependency graph, as produced by package manager lockfiles and SCA tools that analyze them, tells you: given this service's declared and resolved dependencies, which library packages are part of its artifact? It is a membership question. The dependency graph answers "does this service include library X at version Y?"

This is valuable for vulnerability matching. When CVE-2024-12345 affects library X versions 1.0 to 2.3, a complete dependency graph lets you determine in seconds which services in your deployment are carrying a version in that range. For a fleet of services, this is the first triage question: scope the CVE. Which services are potentially affected at the package level?

The dependency graph does not tell you how library X is used. It does not tell you which functions your application calls. It does not distinguish between a library that is used in a critical hot path and a library that was added as a transitive dependency of a test utility and is never loaded in production. It does not tell you which of library X's capabilities your code exercises.

Dependency graphs can be further categorized as direct versus transitive. Your package.json or pom.xml declares direct dependencies; those direct dependencies have their own dependencies, and so on. A typical Node.js application might have 30 direct dependencies and 500+ transitive ones. CVEs in transitive dependencies are particularly difficult to reason about from the dependency graph alone, because the invocation path from your application code to the vulnerable function may traverse multiple library boundaries, each adding uncertainty about whether the path is actually exercised.

What static code scanning tells you

Static scanning of your application code is a different kind of analysis. It looks at the source code and identifies patterns that match known vulnerability signatures: SQL query construction using string concatenation, deserialization of untrusted data, use of deprecated cryptographic functions, missing authentication checks on certain code paths. It analyzes what your code does, not what packages it imports.

SAST tools in their basic form report findings at specific code locations: "this line calls exec() with user input" or "this function deserializes without type checking." In more sophisticated implementations, they trace data flow: input enters the application at this source, flows through these transformations, and reaches this sink without sanitization. That data flow tracking is the beginning of path analysis, though typically limited to within a single service.

The limitation of static scanning is that it sees the code as text. It does not know which entry points are actually reachable from an attacker. It flags every deserialization call involving a type that could be tainted, regardless of whether that deserialization function is called from a public API handler or only from an internal scheduler that processes trusted internal messages. The scanner's job is to find patterns; classifying their risk requires knowing how the code is deployed.

The information in each, and the gap between them

To make this concrete: imagine a Python service that handles webhook requests from a payment processor. The service uses a YAML parsing library. The YAML library has a critical CVE in its full YAML load function, which allows arbitrary code execution via deserialization.

The dependency graph tells you: this service uses the YAML library. The CVE matches this library version. Flag this service for remediation.

Static scanning tells you: there is a call to yaml.load() at line 47 of parser.py. This is the vulnerable function. Flag this location.

Neither tells you: the call at line 47 is inside a function parse_internal_config(), which reads from a local configuration file, which is only written by a trusted internal provisioning process, which is never exposed to external input. The entry points that handle webhook requests from the payment processor call a different parse function that uses yaml.safe_load() exclusively.

The third question, the reachability question, requires tracing the call graph from the entry points that accept external input and determining whether those entry points can reach line 47 of parser.py. The answer in this case is no: the vulnerable function is present in the codebase and present in the dependency tree, but it is not on any path from an externally-reachable entry point. The finding is real; the risk given this application's specific topology is low.

Why this gap is wider in microservice architectures

In a monolith, the call graph is theoretically analyzable within a single codebase. The entry points are HTTP handlers or queue consumers in that codebase; the sensitive sinks are database calls or secrets accesses in the same codebase. A sophisticated SAST tool can trace data flow within the monolith and provide meaningful reachability information for many finding types.

In a microservice application, the call graph extends across service boundaries. Service A receives an external HTTP request and calls Service B's internal API. Service B processes that request and calls Service C. Service C reads from a shared database. A vulnerability in Service C's database access code is on the attack path from Service A's entry point, even though Service C is not directly exposed to external traffic.

No single-service SAST analysis can identify this path. The dependency graph for Service C tells you which libraries it uses; the static scan of Service C tells you about vulnerable patterns in Service C's code. Neither knows that Service A can trigger Service C's vulnerable code through Service B. This inter-service path analysis requires a graph model of the application topology, not just library inventory and single-service code analysis.

What the third question requires technically

Answering "can an attacker get from an exposed entry point to the data they want" requires three inputs that neither dependency graph nor static scanner provides alone:

First, a complete inventory of entry points: which API endpoints accept unauthenticated or externally-supplied input. This requires understanding your service topology and authentication model, not just the code.

Second, a call graph that spans service boundaries: which inter-service calls exist and in which direction, so that multi-hop paths can be traced. This requires either static analysis of inter-service call patterns (reading HTTP client calls, event emission, queue writes) or runtime topology data from service mesh telemetry.

Third, a classification of sensitive target nodes: which code locations or data stores are the triage-relevant destinations. A SQL query that reads product names is different from a SQL query that reads payment credentials; a call graph edge to the former is not an attack path in the same sense as an edge to the latter.

With those three inputs, forward traversal from entry points to target nodes produces the set of paths that the attacker can plausibly follow. Vulnerability findings from the dependency graph and static scanner are then filtered to those that lie on those paths. The finding count drops to the set that is actually reachable.

Using all three in a practical workflow

The dependency graph and static scanner remain the correct starting points. The dependency graph is fast and cheap to compute from lockfiles; it gives you a broad scope of CVE exposure. Static scanning catches patterns in your own code that may be independent of any CVE. Both should run in CI.

The reachability analysis layer filters those findings post-generation. It does not replace either of the earlier layers; it sits on top of them and answers the question neither can answer alone. Teams that invest in all three, rather than treating the dependency graph and static scanner as sufficient, get a finding stream that is both complete (the first two layers ensure nothing is missed at the library or code-pattern level) and filtered (the third layer reduces that complete set to the findings that represent exploitable paths in the deployed topology).

The practical consequence of not having the third layer is a triage backlog that conflates findings that matter with findings that are technically present but not exploitable in context. Every experienced AppSec engineer has seen that backlog. The dependency graph and static scanner are not the problem; they are doing exactly what they were designed to do. The problem is stopping there and treating their output as the complete picture of application risk.

See reachability analysis on your own code

Connect your repositories and apply a reachability filter to your existing scanner findings. No configuration changes to your current toolchain required.

Request early access