Reference · Dependencies

Transitive dependencies: the code you ship but never chose

You pick a dozen libraries. The build resolves hundreds. Almost all of the third-party code in your release is transitive — pulled in by something you chose, or by something that pulled that in. It is code no one evaluated, whose advisories land on nobody’s desk, and whose licenses apply to your product all the same.

Updated 24 Aug 2026 · by the depproof team

In one sentence: a transitive dependency is a dependency of a dependency — you never declared it, you usually cannot upgrade it directly, and it counts as yours the moment you ship it.

Direct, transitive, and the gap between them

A direct dependency is one you declared: it is named in your pom.xml, build.gradle, package.json, go.mod, or pyproject.toml. Somebody on your team chose it, and probably had a reason.

A transitive dependency is everything that arrives with it. Maven’s own documentation describes the mechanism plainly: it “avoids the need to discover and specify the libraries that your own dependencies require by including transitive dependencies automatically”. That automation is the entire value of a package manager. It is also why the list of things you are responsible for is an order of magnitude longer than the list of things you picked.

The gap between the two lists is not a rounding error. In a 2019 USENIX Security study of the npm ecosystem, Zimmermann et al. found that installing an average package “introduces an implicit trust on 79 third-party packages and 39 maintainers”. The absolute number moves with the ecosystem and the year; the shape does not. You audit the one. You ship the eighty.

Why the risk concentrates down there

  • Nobody is watching them. Teams subscribe to advisories for the libraries they chose. A vulnerability announced in a package four levels down arrives with no notification, no changelog anyone reads, and no name that appears anywhere in your source.
  • You cannot fix them where you found them. A direct dependency has an obvious remedy: upgrade it. A transitive one is controlled by whoever declared it, so the fix usually has to travel through a maintainer who has their own release schedule.
  • The licenses apply regardless. Obligations attach to what you distribute, not to what you typed. A copyleft library that arrives four levels down is in your artifact and its terms are in force.
  • They are how supply-chain attacks reach you. A compromised release of a small, popular, deeply-nested package is inside thousands of builds before anyone reads the diff — which is what makes an inventory of the full graph a security control rather than paperwork.

Who decides which version you get

When two of your dependencies want different versions of the same library, something has to break the tie — and every ecosystem breaks it differently. This is worth knowing before you try to fix anything, because it explains why the version in your build is often one that nothing in your project asked for.

Ecosystem Conflict rule What that means
Maven Nearest definition wins The version closest to your project in the tree is used; if two sit at the same depth, the first declaration wins.
Gradle Highest version wins When two dependencies require different versions of the same module, Gradle selects the highest by default.
npm · pnpm · yarn Several versions can coexist The tree can nest a second copy under the parent that needs it, so “which version do we run?” may have more than one answer at once.
Go Highest required version wins Minimal version selection walks the module graph tracking the highest version any module requires, and builds with that.
Python One version, or an error The environment is flat — a package has exactly one installed version, and irreconcilable requirements fail the install rather than resolving.

Two consequences worth holding on to. First, a version bump in one corner of the tree can silently change the version another dependency gets — the resolved set is a property of the whole graph, not of any one declaration. Second, in JavaScript the question “what version of this library do we run?” can have several correct answers simultaneously, so a finding may need fixing in more than one place.

Seeing your own graph

Before fixing anything, find the path. Every toolchain will print it, and the path is what tells you which direct dependency to act on:

mvn dependency:tree                 # Maven
gradle dependencies                 # Gradle
npm ls lodash                       # npm — every path to one package
pnpm why lodash                     # pnpm
go mod graph                        # Go
poetry show --tree                  # Python (Poetry)
uv tree                             # Python (uv)

Documented by each toolchain: Maven · Gradle · npm · pnpm · Go · Poetry · uv.

A scan is the same information with the advisories and licenses already joined on, which is the part a tree command cannot do. What matters is that it reports the introducing dependency alongside the finding — the vulnerable package tells you what is wrong, and the path tells you where to change it.

Fixing one you don’t control

Triage first, and triage on exposure rather than on count. A first scan of a real graph routinely returns more findings than any team can remediate at once, and the ones worth stopping a release for are the ones with evidence of exploitation behind them — severity alone is a weak prioritiser. Once you know which finding you are acting on, the options run in rough order of preference; the first one that works is the one to take.

  • 1 · Upgrade the direct dependency. Most of the time the maintainer has already released a version that requires the fixed library, and the whole problem is a version bump on something you do declare. It is the only fix that leaves you on a combination the maintainer actually tested.
  • 2 · Force the version yourself. Every ecosystem lets you dictate the version of something you never declared — see the table below. Fast, and occasionally the only lever you have, but you are now running a pairing nobody tested. Treat it as a bridge, and leave a note saying what it is waiting for.
  • 3 · Replace or drop the direct dependency. When the introducing library is unmaintained, the vulnerability is a symptom and the maintenance status is the problem. This is the expensive fix and sometimes the correct one.
  • 4 · Accept it, on the record. If nothing upstream is fixed and the path is not reachable in your application, say so deliberately: risk acceptance, written down as a waiver with an owner, a reason and an expiry — and VEX when customers scan your artifacts themselves. An undocumented decision is indistinguishable from an oversight at audit time, and an expiry is what stops it becoming permanent by accident.

The override mechanism, by ecosystem

Ecosystem Mechanism Notes
Maven dependencyManagement Pins the version used whenever a module is encountered transitively, without adding it as a direct dependency.
Gradle dependency constraints Raises the version of a transitive module without declaring a dependency on it — a no-op if nothing pulls the module in.
npm overrides Replaces a package anywhere in the tree. Honoured only in the root package.json — an override inside a dependency is ignored.
pnpm · yarn overrides · resolutions Same idea, same root-only rule: pnpm calls it overrides, yarn calls it resolutions, and both are set at the root of the project.
Go go get, or replace Requiring the fixed version raises what selection picks. A replace directive also works, but only in the main module — yours are ignored by anyone who imports you.
Python constraints file A constraints file governs which version is installed if a package is installed at all, without pulling it in itself.

One shared limitation: in JavaScript, Go and Python these levers apply to your build. npm states it directly — overrides “are only considered in the root package.json” — and Go says the same of replace, which “only applies in the main module’s go.mod file”. If you publish a library, an override protects you and not the people who depend on you. Fixing it properly upstream is the only version of the fix that travels.

Why a scan can miss them entirely

All of this assumes the tool told you the package was there. A scan that reads only what you declared reports on your dozen and stays silent about the hundreds — and silence reads exactly like good news. That is the failure mode worth designing against: a clean report can mean the graph was never fully resolved, and the built-in audit commands miss findings for reasons that have nothing to do with your code.

So the first question to ask of any SCA tool is not how many findings it produced. It is whether it resolved the full graph, and whether it says so when it could not. depproof fails the build when the graph could not be fully resolved — the one case where a short findings list is the warning rather than the all-clear — and writes the resolved graph into the CycloneDX SBOM, so the inventory you hand a customer is the one the scan actually saw. It runs on your own infrastructure; your source and findings never leave it.

Licenses travel the same path

The security half gets the attention, and the license half is where the surprise usually is. A team can review every direct dependency’s license with real care and still ship strong copyleft, because the review stopped at the top of the graph and the obligations did not.

Permissive licenses are not the safe half of that statement either. MIT and BSD-3-Clause are trivial to comply with and still carry an attribution clause: the notice file you redistribute has to account for every dependency whose text requires one, including the ones nobody selected. Teams discover this when a customer’s counsel asks for the notice file and it lists forty components against a manifest of twelve. The prerequisite is the same as for the security half — a license identified and normalised to an SPDX identifier for every component in the resolved graph, not just the declared ones, with source-available terms told apart from open-source ones.

The question that matters is not which licenses you approved, but which ones are in the artifact — which licenses can you actually ship? covers the distributability spectrum, and the compatibility matrix covers what happens when two of them meet. AGPL-3.0 is the one worth checking for first: it is the license most likely to change what you owe, and the least likely to have been chosen by anyone on your team.

Frequently asked questions

What is a transitive dependency?

A transitive dependency is a package your project pulls in indirectly — a dependency of one of your dependencies, or of one of theirs, to any depth. You declare a handful of direct dependencies; each of those declares its own, and the build resolves the whole graph. The result is that most of the third-party code you ship is code you never chose, never named, and in most cases have never heard of.

Why do transitive dependencies matter for security?

Because that is where the volume is. A 2019 USENIX Security study of npm found that installing an average package places implicit trust in 79 third-party packages and 39 maintainers. A vulnerability in any one of them is in your application exactly as much as one in a package you chose deliberately — and unlike a direct dependency, nothing about your source code mentions it, so nobody on your team is watching its advisories.

How do I find out what pulled in a vulnerable package?

Every toolchain can print the path. Use mvn dependency:tree for Maven, gradle dependencies for Gradle, npm ls <package> or pnpm why <package> for JavaScript, go mod graph for Go, and poetry show --tree or uv tree for Python. A scan report that names the introducing dependency saves you that step, because the fix is almost always applied to the direct dependency rather than to the vulnerable package itself.

Can I upgrade a transitive dependency directly?

Yes, though it is the second choice. Every major ecosystem provides an override: dependencyManagement in Maven, dependency constraints in Gradle, overrides in npm and pnpm, resolutions in yarn, a require or replace in Go, and a constraints file in Python. In JavaScript these are honoured only from the root of the project, so they fix your build and not your consumers. In all cases you are now running a combination the direct dependency never tested, so an override is a bridge to a real upgrade, not a destination.

What do I do when there is no fixed version at all?

Decide, record the decision, and set an expiry. If the vulnerable code path is not reachable from your application, or a control you already run blocks it, that is a legitimate reason not to ship an emergency patch — but it is a judgement, and judgements that live in someone’s memory get re-litigated at the next audit. A waiver with an owner, a justification and an expiry date keeps the finding visible without failing every build, and VEX communicates the same conclusion to customers who scan your artifacts themselves.

Do transitive dependencies carry license obligations too?

Yes, and this surprises people more than the security half. License obligations attach to what you distribute, not to what you typed into a manifest. A strong-copyleft library four levels down is in your shipped artifact and its terms apply to it. Teams that carefully vet the licenses of their direct dependencies routinely ship copyleft code they never inspected, because they only ever looked at the top of the graph.

Does having a lockfile mean I already know my transitive dependencies?

A lockfile records the resolved set, which is most of the way there — it is why a scan that reads the lockfile sees far more than one that reads only the manifest. What it does not tell you on its own is which of those packages carry known vulnerabilities or licenses you cannot ship, or which direct dependency introduced each one. That mapping is what an SCA tool adds on top of the resolved graph.

Where to go next

depproof is a self-hosted SCA tool for Maven, Gradle, npm, Python and Go — free for open source and teams under $1M revenue. See pricing. Product and standard names are used nominatively.