Direct vs Transitive Dependencies: The Difference That Decides Your Supply-Chain Risk

Written by the Rafter Team

A direct dependency is a package you explicitly add to your project—the line you typed into package.json, requirements.txt, go.mod, or pom.xml. A transitive dependency is a package that direct dependency needs in order to run, pulled in automatically without you ever naming it, reviewing it, or knowing it exists. You install one library. It drags in ten, or a hundred, behind it.
That distinction is not academic trivia. Most of the third-party code running in your application, and most of the CVEs that eventually hit it, live in the transitive layer—the packages nobody on your team chose, read the source of, or added to a review checklist. Understanding where that layer starts and how to see into it is the difference between a dependency audit that covers 5% of your risk and one that covers all of it.
A typical Node.js project declares 30 to 50 direct dependencies in package.json but resolves to 500 to 1,500 transitive packages once the full tree is expanded. If your review process stops at the manifest, you have looked at roughly 3% of the third-party code actually shipping in your build.
The Quick Verdict
Direct and transitive dependencies are not two different threat categories—they are two different levels of the same tree, and the practical differences between them determine how you audit, patch, and prioritize.
| Direct Dependency | Transitive Dependency | |
|---|---|---|
| Definition | A package you explicitly declared | A package your direct dependency requires |
| Where it's declared | Your manifest (package.json, go.mod, requirements.txt) | Your lockfile only (package-lock.json, go.sum, poetry.lock) |
| Who chose it | You or your team | The maintainer of a package you chose |
| Typical count | Dozens | Hundreds to thousands |
| Visibility without tooling | High—you can read the manifest | Low—buried several levels deep |
| Who reviews it before adding it | Sometimes, if you're disciplined | Almost never |
| Where most CVEs are found in practice | Minority | Majority |
The verdict: transitive dependencies are not more dangerous per package, but there are so many more of them, with so much less scrutiny, that they account for the bulk of real-world dependency risk. Treating "I checked my dependencies" as synonymous with "I checked my package.json" is the single most common gap in software composition analysis.
What Is a Direct Dependency?
A direct dependency is any package your project lists by name in its own manifest file. If you ran npm install express, pip install requests, or added github.com/gin-gonic/gin to your go.mod, that package is direct. You made a decision to bring it in, and—in theory—you can see it, version it, and remove it with a single edit to a file you control.
Direct dependencies are the visible part of your supply chain. They show up in npm ls --depth=0, in a grep of your manifest, in a five-minute code review. Most security processes stop here, because this is the part that is easy to see.
What Is a Transitive Dependency? A Concrete Example
A transitive dependency is a package that one of your direct dependencies requires in order to function, resolved automatically by your package manager without any action from you. It does not appear in your manifest. It appears in your lockfile, several levels down, because something you did add needed it.
Here is what that looks like in practice. Say your package.json declares exactly one direct dependency:
your-app
└── express@4.19.2 (direct—you added this)
Express itself depends on other packages to handle request bodies, cookies, and static files. Your package manager resolves those automatically:
your-app
└── express@4.19.2 (direct—you added this)
├── body-parser@1.20.2 (transitive—express requires it)
│ ├── bytes@3.1.2 (transitive—body-parser requires it)
│ └── qs@6.11.0 (transitive—body-parser requires it)
├── cookie@0.6.0 (transitive)
└── send@0.19.0 (transitive)
└── mime@1.6.0 (transitive, two levels below your one direct dependency)
You typed one line—express—and the resolved tree pulled in six additional packages you never named. That is a small, tame example. A real Express application with a handful of direct dependencies for logging, auth, testing, and a database driver routinely resolves to several hundred transitive packages once every branch of the tree is expanded. Multiply that by a typical monorepo with dozens of services, and the transitive layer dwarfs the manifest by an order of magnitude.
This is exactly the layer where major incidents have landed. The Log4Shell vulnerability (CVE-2021-44228) mostly hit organizations that had never heard of Log4j, because it arrived buried three or four levels deep inside frameworks like Spring Boot rather than as something anyone had typed into a manifest. Read more on how that class of risk compounds at depth in why transitive dependencies are your biggest blind spot.
Why Do Most of Your CVEs Live in the Transitive Layer?
Simple arithmetic explains it. If your project has 40 direct dependencies and 900 transitive ones, roughly 96% of the packages running in your application were never individually chosen by anyone on your team. Vulnerability counts scale with package counts, so the layer with the most packages produces the most CVEs—not because transitive packages are worse code, but because there are vastly more of them and far less review attention per package.
There is also a review-gap effect on top of the raw numbers. A direct dependency at least got a moment's consideration when someone ran the install command—a glance at download counts, a check of the GitHub repo, maybe a look at the changelog. A transitive dependency four levels down got none of that. Nobody on your team has ever looked at its source, checked who maintains it, or noticed when its last release shipped.
Attackers understand this asymmetry. Dependency confusion attacks specifically target the resolution process itself, tricking a package manager into pulling a malicious package instead of the legitimate internal or transitive one your build expected—an attack that works precisely because nobody is watching that layer closely enough to notice the substitution. The OWASP Top 10 formalizes this concern as its own category, vulnerable and outdated components, for exactly this reason: the risk isn't hypothetical, and it isn't limited to packages you can name from memory.
Why Do Lockfiles Matter for Transitive Dependencies?
Your manifest declares a version range for direct dependencies—^4.19.0, for instance—but it says nothing about which exact versions of the packages underneath got resolved. That job belongs to the lockfile: package-lock.json, yarn.lock, poetry.lock, go.sum, or the equivalent for your ecosystem.
The lockfile is the only artifact in your repository that records the complete, exact, resolved dependency graph—every transitive package, pinned to a specific version and, in most modern lockfile formats, a content hash. Without it, two developers running npm install on the same manifest at different times can end up with different transitive trees, because floating version ranges resolve differently depending on what has been published since the manifest was last touched.
That non-determinism is a security problem, not just a build-consistency one. A scanner—or a human—that reads only the manifest cannot tell you what actually shipped. Commit your lockfile, and treat changes to it with the same scrutiny you'd give a change to the manifest itself; a lockfile diff that swaps in an unexpected transitive package, or drops a hash pin, is worth a second look before it merges.
How Do SCA Tools Resolve the Full Dependency Graph?
Software composition analysis, or SCA, is the practice of scanning your dependencies—direct and transitive—against known-vulnerability databases. A tool that only reads your manifest is not doing SCA in any meaningful sense; it is doing a much smaller job that happens to look similar from a distance.
A real SCA tool parses your lockfile, not your manifest, because that is the only place the full transitive graph is written down. From there it expands every branch, resolves each package to its exact version, and cross-references that version against vulnerability sources like the National Vulnerability Database and the GitHub Advisory Database. The output is a graph, not a list—which matters, because knowing that a vulnerable package is buried four levels under a direct dependency you do control is what tells you which manifest line to actually touch to fix it.
This is where tooling choice starts to matter in practice. Some scanners resolve transitive dependencies thoroughly across every ecosystem you use; others quietly limit deep resolution to whichever language they support best and give shallower coverage everywhere else. If you're evaluating options, the current SCA tools comparison walks through how the major players—including Rafter—differ on lockfile parsing and transitive coverage, and the complete guide to dependency scanning and SCA covers the broader mechanics of how these scans work end to end.
What Should You Actually Do About Transitive Dependency Risk?
Start by committing your lockfile and never letting CI resolve dependencies without one. A build that runs npm install against a bare manifest, with no lockfile present, resolves transitive versions fresh every time, which means the graph you tested is not necessarily the graph you shipped.
Run a full-tree inspection, not a manifest read, when you audit. npm ls --all, mvn dependency:tree, pipdeptree, and go mod graph all expand the resolved graph rather than the declared one, and each takes under a minute to run against a real project.
Scan on every pull request, not on a periodic schedule. A monthly batch scan finds a vulnerable transitive package weeks after it merged; a scan gated on the PR catches it before it reaches your main branch, when the fix is a one-line version bump instead of a production incident.
This gap matters most for teams shipping with AI coding agents in the loop. An agent that scaffolds a feature runs its own installs, adds its own direct dependencies, and never pauses to ask whether the resulting transitive tree is something a human reviewed—the coding agent is the supply chain now, and that supply chain includes everything pulled in underneath the packages the agent typed in on your behalf.
Rafter runs SCA, static analysis, and secret scanning together as a single check in your CI, on the pull request, resolving the full dependency graph—direct and transitive—before code merges rather than after. For a team writing code with agents in the loop as often as humans, that PR-gated check is the point where the transitive layer actually gets looked at, instead of being assumed clean because nobody could see it.
Frequently Asked Questions
What is a transitive dependency?
A transitive dependency is a package that one of your direct dependencies requires in order to function, resolved automatically by your package manager without you naming it in your own manifest. It's recorded in your lockfile, not your package.json or equivalent, and it can sit several levels below the package you actually installed.
What is the difference between a direct and transitive dependency?
A direct dependency is one you explicitly declared in your manifest; a transitive dependency is one that a direct dependency pulled in on its own. The practical difference is scale and scrutiny—you typically have dozens of direct dependencies and hundreds to thousands of transitive ones, and the transitive layer receives far less individual review.
Are transitive dependencies more dangerous than direct ones?
Not per package, but in aggregate, yes. There are far more transitive packages than direct ones in any real project, each one got less scrutiny before it landed in your tree, and the sheer volume means most of your dependency-related CVEs statistically land in that layer rather than in the packages you chose directly.
How do I see my transitive dependencies?
Use your package manager's tree-inspection command—npm ls --all for Node.js, mvn dependency:tree for Maven, pipdeptree for Python, or go mod graph for Go—to expand the full resolved graph rather than just the manifest. A dedicated SCA tool that parses your lockfile will show you the same graph alongside which packages have known vulnerabilities.
Can I pin transitive dependency versions?
Yes, in most ecosystems. npm supports overrides in package.json, Yarn supports resolutions, and other package managers have similar mechanisms for forcing a specific version of a transitive package regardless of what the direct dependency requests. Pin transitive versions when you need to force a patched release ahead of your direct dependency's own upstream fix, and remove the override once that dependency catches up.
Does a lockfile include transitive dependencies?
Yes—the lockfile is specifically the artifact that records the complete resolved graph, direct and transitive, pinned to exact versions and usually content hashes. It is the only reliable source for what actually ships, which is why any SCA process that skips the lockfile and reads only the manifest is missing the majority of your real dependency surface.