Safeguard
Open Source Security

How Dependency Graphs Reveal Hidden Supply Chain Risk

Dependency graph analysis reveals which transitive packages can actually reach your code. From Log4Shell to the xz backdoor, see why flat scans miss what graphs catch.

Vikram Iyer
Security Researcher
8 min read

A single npm install can quietly pull in 700 or more packages you never chose, wrote, or reviewed. Your team picked ten direct dependencies; the resolver picked the other 690. This is the reality of modern software: what looks like a short manifest file is actually the visible tip of a dependency graph that can run five, ten, even fifteen levels deep. Somewhere in that graph sits a transitive maintainer you've never heard of, a package that hasn't been touched since 2019, or a library one commit away from becoming your incident. Traditional vulnerability scanning checks direct dependencies against known-bad lists, but it can't answer the harder question: which of these thousands of nodes can actually reach your production code, and which are just along for the ride? Dependency graph analysis exists to answer exactly that question — and the incidents of the last decade show why it can no longer be optional.

What Is a Dependency Graph, and Why Does It Matter for Security?

A dependency graph is the full, recursive map of every package your software pulls in — direct and transitive — modeled as nodes and edges rather than a flat requirements file. When a developer adds one direct dependency to a package.json, pom.xml, or requirements.txt, that single line can expand into dozens or hundreds of resolved packages once the package manager walks each dependency's own dependencies, and theirs, and so on. Google's Open Source Insights team documented this starkly during Log4Shell: CVE-2021-44228, disclosed on December 9, 2021, ended up affecting more than 35,000 Java packages on Maven Central, and the vast majority of those were pulled in transitively — teams had no direct line of sight into log4j-core at all. A flat vulnerability list tells you "log4j-core 2.14.1 is vulnerable." A dependency graph tells you how that package reached your build, through which plugin, through which parent artifact, and whether your code path ever calls the vulnerable method in the first place. That distinction — reachability versus mere presence — is the entire value proposition of graph-based analysis over list-based scanning.

How Deep Do Modern Dependency Trees Actually Go?

Deep enough that most engineering teams have never actually looked at the bottom. It's common for a JavaScript or Java project with a dozen direct dependencies to resolve five to ten levels down before the graph terminates, and individual "leaf" packages at that depth are often maintained by a single volunteer with no CI, no code signing, and no security review process. The 2016 left-pad incident is the canonical warning shot: an 11-line utility package, unpublished by its author in a dispute over a naming trademark, broke builds for Babel, React, and thousands of other projects worldwide within hours — because it sat several layers deep in graphs nobody had actually mapped. Log4Shell repeated the lesson at enterprise scale: security teams spent weeks in December 2021 and January 2022 trying to manually enumerate every JAR that shaded or repackaged log4j-core, because it was buried three or four layers inside build plugins, uber-JARs, and vendor SDKs that didn't advertise the dependency anywhere visible. Without a computed graph, "are we affected?" is a weeks-long forensic exercise instead of a query that returns in seconds.

Why Did the xz Utils Backdoor Almost Get Past Everyone?

Because it was engineered to live exactly where flat dependency lists and human code review don't look: inside the build system of a transitive dependency, one hop removed from the actual attack surface. The xz utils backdoor, tracked as CVE-2024-3094, was the product of a multi-year social engineering campaign — a contributor using the name "Jia Tan" built trust in the project starting around 2021 before slipping obfuscated code into release tarballs of liblzma versions 5.6.0 and 5.6.1 in February 2024. The payload was hidden in test files and M4 macros rather than readable source, and it worked by hijacking sshd indirectly: many Linux distributions link sshd against libsystemd, which in turn links against liblzma for compression — a transitive relationship almost no one had mapped or was watching. It was discovered by pure chance on March 29, 2024, when PostgreSQL developer Andres Freund noticed SSH logins were consuming an extra 500 milliseconds of CPU time and investigated. Had the graph relationship between sshd, libsystemd, and liblzma been continuously modeled and monitored for anomalous new maintainer activity, the suspicious version bump and unexplained binary blobs in the release tarball were exactly the kind of graph-level signal that could have surfaced the backdoor before it reached distributions like Debian Sid and Fedora Rawhide.

Can Dependency Graph Analysis Catch Malicious Packages Before a CVE Exists?

Yes — because most supply chain attacks are visible as graph and behavioral anomalies long before any CVE is filed. CVEs are reactive by design; someone has to find and disclose the issue first. Malicious package campaigns, by contrast, leave signals the moment they're published: a new maintainer suddenly gaining publish rights, a package adding a postinstall script that phones home, or a sharp version jump with no corresponding source changes. In November 2021, the popular npm packages coa and rc — combined, downloaded tens of millions of times weekly — were compromised after their maintainer accounts were hijacked, and malicious versions began harvesting credentials within hours of publication. In March 2022, the maintainer of node-ipc shipped versions 10.1.1 through 10.1.3 with code that detected users' geolocation and overwrote files on machines with Russian or Belarusian IP addresses — a protest action that turned a widely-used dependency into a wiper, discovered only because downstream projects noticed unexpected file corruption. Sonatype's 2023 State of the Software Supply Chain report found that researchers identified 245,032 malicious packages across open source registries that year, a majority of which were never assigned a CVE at all, because CVEs are reserved for vulnerabilities in legitimate code, not intentionally malicious packages. Graph analysis that tracks maintainer changes, publish velocity, and install-script behavior at every node is the only way to catch this category before it ships to production.

How Should Teams Operationalize Dependency Graph Analysis?

By treating it as a continuous, reachability-aware process integrated into the SBOM and CI/CD pipeline — not a point-in-time scan run before a release. Generating a Software Bill of Materials in SPDX or CycloneDX format is the necessary first step, but an SBOM is a snapshot; a dependency graph is a living model that needs to be recomputed on every dependency bump, every lockfile change, and every new transitive resolution. Independent research from application security vendors analyzing production codebases has repeatedly found that only a small fraction of known-vulnerable dependencies — commonly cited in the 10-15% range — are actually reachable, meaning the vulnerable function is ever called by the application's own code. Without reachability analysis layered on top of the graph, security teams drown in noise: hundreds of "critical" CVE alerts for code paths that can never execute. Effective operationalization means three things running together: the graph is recomputed automatically on every build, new or changed maintainers and anomalous publish patterns are flagged before packages are approved, and vulnerability severity is re-ranked based on whether the affected function sits on a reachable call path from your application's entry points.

How Safeguard Helps

Safeguard builds dependency graph analysis directly into the software supply chain security pipeline instead of treating it as a bolt-on report. Every scan constructs the full transitive graph for a repository — direct and indirect dependencies alike — so teams can see exactly how a vulnerable or suspicious package like liblzma, log4j-core, or a compromised npm package actually reaches their build, not just that it's present somewhere in a lockfile. Safeguard layers reachability analysis on top of that graph, so a CVE buried four levels deep in an unreachable code path is deprioritized automatically, while a vulnerability sitting on a path your application actually calls is surfaced as a real, actionable finding rather than one more line in a 200-item report. On the malicious-package side, Safeguard continuously monitors maintainer changes, publish anomalies, and install-script behavior across the graph, generating alerts on the same class of signal that would have caught incidents like the coa/rc account takeovers or node-ipc protestware before they spread further downstream. SBOMs generated in SPDX and CycloneDX formats stay synchronized with the live graph on every build, giving compliance and security teams an audit-ready record without manual reconciliation. And because the graph is tenant-aware and integrated into CI/CD as a policy gate, teams can block a merge or release the moment a new transitive dependency introduces unacceptable risk — turning the multi-week forensic scramble that followed Log4Shell into a query that returns before the pull request even merges.

Never miss an update

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