Software Supply Chain

Dependency Graph Analysis: Finding Hidden Transitive Risks

Your project has 50 direct dependencies. It actually depends on 1,200 packages. Transitive dependency analysis is how you find the risks hiding three layers deep.

Shadab Khan
Security Engineer
8 min read

Every developer understands direct dependencies. You add express to your package.json, you know you depend on Express. What most developers don't internalize is the scale of what comes along for the ride.

A typical Node.js project with 30 direct dependencies will pull in 500 to 1,500 transitive dependencies. A Java application using Spring Boot might have 200 direct dependencies resolving to over 2,000 total. Your dependency graph is an iceberg, and your package.json is the tip above the waterline.

The security implications are significant. Over 80% of vulnerabilities discovered in open source affect transitive dependencies — packages your team never chose, probably never heard of, and definitely never reviewed.

What Makes Transitive Dependencies Dangerous

Direct dependencies receive at least some scrutiny. Someone on the team evaluated the package, considered alternatives, maybe checked the GitHub stars and download counts. Transitive dependencies receive no scrutiny. They're inherited automatically, selected by someone else's dependency resolution.

This creates several concrete risks:

Invisible Attack Surface

A vulnerability in a transitive dependency is functionally identical to a vulnerability in your own code — it ships in your artifact, it runs in your environment, it can be exploited by attackers. But you have zero visibility into it without tooling.

The Log4Shell vulnerability (CVE-2021-44228) demonstrated this at scale. Many affected organizations didn't directly depend on Log4j. They depended on frameworks that depended on libraries that depended on Log4j. Finding these nested dependencies required full graph analysis that most teams hadn't performed.

Version Conflict and Resolution Opacity

When multiple paths in your dependency graph require different versions of the same package, the package manager applies resolution algorithms. npm might hoist one version and nest another. Maven uses a "nearest wins" strategy. Gradle applies its own conflict resolution.

The resolved version might not be the one you expect. Worse, it might not be the version that was security-tested by the intermediate dependency's maintainer. Version resolution can silently introduce vulnerable versions of transitive dependencies.

Maintainer and Ownership Risk

Transitive dependencies are maintained by people you have no relationship with. A package four levels deep in your graph might be maintained by a single developer who lost interest three years ago. Or it might have been transferred to a new owner you've never vetted.

The event-stream incident in 2018 remains the textbook example. A new maintainer was granted publishing rights to a transitive dependency of a popular Bitcoin wallet. They injected credential-stealing code that targeted a specific application. The attack went undetected for weeks because nobody was monitoring a dependency that deep in the graph.

Building and Analyzing Dependency Graphs

Effective dependency graph analysis requires three capabilities: complete resolution, graph construction, and risk scoring.

Complete Resolution

The first step is resolving your complete dependency tree — not just what's declared, but what actually gets installed. Package managers provide this:

# npm - generate full dependency tree
npm ls --all --json > deps.json

# Maven - output dependency tree
mvn dependency:tree -DoutputType=dot

# Gradle - dependency report
gradle dependencies --configuration runtimeClasspath

# pip - show installed packages with dependencies
pip install pipdeptree && pipdeptree --json

The output includes every package at every level, with version numbers and the dependency paths that introduced them.

Graph Construction

A flat list isn't enough. You need the graph structure to understand how dependencies relate. Key graph properties to analyze:

Depth — How many levels deep does your dependency graph go? Deeper graphs mean more packages outside your direct control. Graphs exceeding 8-10 levels deep deserve scrutiny.

Fan-out — How many transitive dependencies does each direct dependency introduce? A single direct dependency that pulls in 200 transitive packages is a concentration risk.

Shared dependencies — Which packages appear in multiple dependency paths? A package depended on by 15 of your 30 direct dependencies is critical infrastructure for your application. A vulnerability in that shared dependency affects your entire application.

Single points of failure — Are there packages maintained by a single developer that sit at critical junctions in your graph? These are your highest-risk nodes.

Risk Scoring

With the graph constructed, apply risk scoring to each node:

  • Known vulnerabilities — CVEs reported against the specific installed version
  • Maintainer activity — Last commit date, release frequency, number of active contributors
  • Popularity and adoption — Download counts provide a rough proxy for community review
  • License compliance — Transitive dependencies can introduce license obligations you didn't anticipate
  • Age and staleness — Packages that haven't been updated in years may contain unpatched vulnerabilities

The combination of graph position and risk score reveals your actual exposure. A high-risk package at a critical junction in the graph is an immediate concern. A high-risk package with no dependents in your tree is a lower priority.

Common Patterns and Anti-Patterns

The Diamond Dependency Problem

Package A depends on B and C. Both B and C depend on D, but at different versions. The package manager must resolve this conflict. In some ecosystems (npm), both versions might be installed. In others (Maven, pip), only one version is selected.

This matters for security because:

  • The selected version might have known vulnerabilities
  • The non-selected version might be the one tested by the intermediate dependency
  • Upgrading one path can change resolution in unexpected ways

Phantom Dependencies

In some package managers, your code can import packages that are transitive dependencies without declaring them as direct dependencies. This works because the package exists in node_modules (npm) or on the classpath (Maven), but it creates fragile and invisible dependencies.

If the transitive dependency is removed in a future update of the intermediate package, your code breaks. If the transitive dependency is vulnerable, there's no clear upgrade path because you didn't declare the dependency.

Dependency Bloat

Some packages bring enormous transitive trees for minimal functionality. The classic example: importing a utility library for one helper function and inheriting 150 transitive packages.

Evaluate whether a direct dependency's transitive footprint is proportional to the value it provides. Sometimes writing 20 lines of code eliminates 200 transitive dependencies.

Strategies for Managing Transitive Risk

Lock Files Are Non-Negotiable

Lock files (package-lock.json, Gemfile.lock, poetry.lock, go.sum) pin the exact versions of every dependency in your graph. Without them, builds are non-deterministic and vulnerability tracking is impossible.

Commit lock files. Review lock file changes in pull requests. Treat unexpected lock file modifications as potential security events.

Regular Full-Graph Scanning

Scan your complete dependency graph — not just direct dependencies — on every build and on a regular schedule for new disclosures. The scanner must resolve the actual installed versions, not just what's declared.

Dependency Update Policies

Establish policies for how quickly transitive dependency vulnerabilities must be addressed:

  • Critical vulnerabilities in direct dependencies: Patch within 24-48 hours
  • Critical vulnerabilities in transitive dependencies: Patch within 1 week
  • High vulnerabilities in transitive dependencies: Patch within 2 weeks
  • Transitive dependencies with no active maintainer: Evaluate and replace the dependency chain

Minimize Dependency Depth

When evaluating new direct dependencies, consider the transitive cost. Two packages that provide similar functionality might have wildly different dependency footprints. The one with fewer and shallower transitive dependencies is the better choice from a security perspective.

Graph Monitoring Over Time

Your dependency graph changes with every update. Track how the graph evolves — new transitive dependencies being introduced, existing ones being removed, maintainer changes in critical path dependencies. Sudden graph expansion after a routine update warrants investigation.

The Ecosystem-Level Problem

Transitive dependency risk isn't just an individual project problem. It's an ecosystem problem. The same foundational packages appear in millions of dependency graphs across the ecosystem. A vulnerability in minimist or lodash ripples through hundreds of thousands of projects.

This concentration risk means that the security of the entire software ecosystem depends on the security practices of a relatively small number of critical packages and their maintainers. Initiatives like the OpenSSF Alpha-Omega Project are working to improve security in these critical dependencies, but progress is slow relative to the risk.

Until the ecosystem solves this structural problem, individual organizations need to manage their transitive risk actively. Assuming that someone else has vetted the code four levels deep in your dependency graph is how supply chain attacks succeed.

How Safeguard.sh Helps

Safeguard provides deep dependency graph analysis across your entire software portfolio. The platform resolves complete transitive dependency trees, constructs full dependency graphs, and applies continuous vulnerability monitoring at every level — not just the packages you directly declared. Safeguard's SBOM generation captures the full picture, including transitive dependencies that traditional tools overlook. When a new vulnerability is disclosed in a package buried six levels deep in your graph, Safeguard identifies every affected project in your organization within minutes, giving your team the context needed to prioritize and remediate before attackers can exploit the exposure.

Never miss an update

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