Dependencies
What is a transitive dependency?
A transitive dependency is a package your software uses because another package needs it. You never declared it, but it is in what you ship, with its own vulnerabilities and license. Here is how to see them, fix them and prove which ones went into each release.
Updated
Direct and transitive dependencies
A direct dependency is one you declare in your manifest. A transitive dependency is one your dependencies declare, and theirs in turn, as deep as the tree goes. The package manager resolves the whole tree when it installs, and usually records the result in a lockfile.
Take an app that declares one package, express. Running npm ls --all shows what that one
line brings with it:
my-app
โโโฌ express โ direct: you declared it
โโโฌ body-parser โ transitive, depth 1
โ โโโ raw-body โ transitive, depth 2
โโโ cookie โ transitive, depth 1
โโโฌ send โ transitive, depth 1
โโโ mime-types โ transitive, depth 2 The real tree is longer. Most of the packages in a typical application arrive this way, which means most of the code you ship was chosen by the package manager, not by you.
| Direct dependency | Transitive dependency | |
|---|---|---|
| Who chose it | Your team, in the manifest | The package manager, from your dependencies' manifests |
| Where it is listed | Manifest and lockfile | Lockfile, if there is one |
| Who controls the version | You | The parent package's version range, unless you override it |
| How you fix a vulnerability | Upgrade it | Upgrade the parent, or override the version |
Why transitive dependencies matter
They carry the same risks as direct dependencies, with less visibility and less control.
- Vulnerabilities. When Log4Shell, a zero-day in log4j, was disclosed, Google's Open Source Insights team found 35,863 Maven Central artifacts that depended on the affected log4j code. About 7,000 depended on it directly. The majority pulled it in as a transitive dependency, and many could not be fixed until a package in between shipped a fix first.
- Supply chain attacks. The xz utils backdoor (CVE-2024-3094) reached OpenSSH without OpenSSH depending on xz. Several Linux distributions patch OpenSSH to support systemd notification, and libsystemd depends on liblzma, the library xz provides.
- Licenses. Every transitive package has its own license. A copyleft license three levels down is as binding as one you added yourself.
- Breaking changes. A parent package that accepts a version range lets a new transitive release in on the next fresh install, even when nothing in your manifest changed.
How to list your transitive dependencies
Every major package manager can print the resolved tree:
| Ecosystem | Command |
|---|---|
| npm | npm ls --all |
| pnpm | pnpm list --depth Infinity |
| Python (uv) | uv tree |
| Python (pip) | pipdeptree |
| Go | go mod graph |
| Rust | cargo tree |
| Maven | mvn dependency:tree |
| Gradle | gradle dependencies |
To find out why a single package is there, ask for the path to it: npm explain <package>,
pnpm why, yarn why or cargo tree -i <crate>. To work with the paths
rather than print them, see dependency graphs.
How to fix a vulnerable transitive dependency
- Upgrade the parent. Find the direct dependency that pulls the package in and move to a release that requires a fixed version. This is the cleanest fix, because the parent's maintainers have tested it.
- Refresh the lockfile. If the parent's version range already allows the fixed version, updating
the lockfile entry is enough, for example with
cargo update -p <crate>or a freshnpm update. - Override the version. If the parent has not shipped a fix, force the version yourself:
overridesin npm and pnpm,resolutionsin Yarn, a constraints file with pip, or a higher requirement ingo.mod. Test it, because the parent was never released against that version. - Replace or remove the parent. If neither works, the dependency that brings the package in may no longer be worth keeping.
Some software composition analysis tools add reachability analysis, which checks whether your code can call the vulnerable function at all, to decide which of these fixes comes first. See SCA tools compared for the options.
Why the lockfile is not the whole story
Most tools build their view of transitive dependencies from the lockfile. That works when the lockfile is there, current and the only thing the build installs from. In CI that is often not the case:
- No lockfile. A
requirements.txtwith version ranges or a project that never committed its lockfile is resolved fresh on every build, so the transitive versions can change between builds. - Caches. CI caches restore packages from earlier runs instead of fetching them, and what they contain is not always what the current lockfile says. See what the GitHub Actions cache hides.
- Build tools. Linters, compilers, code generators and setup steps bring their own dependency trees into the build. They are not in your application's manifest, but they run with access to your source and secrets.
- Install scripts and downloads. A package's install script can fetch more code, and a build step can download a binary directly. Neither leaves an entry in a lockfile.
The only complete record of which transitive dependencies went into a release is one taken while that release was built. Build-time SBOMs explains the difference between recording a build and scanning its files afterwards.
Transitive dependencies and the CRA
The EU Cyber Resilience Act asks manufacturers to draw up an SBOM covering "at the very least the top-level dependencies of the products" (Annex I, Part II, point 1). Transitive dependencies are not named, so the legal floor is lower than many guides suggest. But the CRA also asks you to identify and handle vulnerabilities in the components your product contains, and a vulnerable transitive package is one of them. An SBOM that stops at the first level leaves most of your dependency tree out of that process. See CRA SBOM requirements for the details.
How CRACI tracks transitive dependencies
CRACI is a GitHub Actions runner that records the build instead of scanning it. While each job runs, a package-aware proxy records every package the job fetches, at every depth of the tree, including packages restored from CI caches. That gives a more complete view of the dependencies in use than scanner-based tools.
- Every depth, stated honestly. Each job's SBOM carries a completeness state, so you know when a recording could not see everything.
- The tree, visible. A dependency graph shows the SBOM's dependency layers, in several views.
- Exports that include them. CycloneDX and SPDX exports include transitive dependencies, each with its declared license.
- Which builds were affected. When a new vulnerability lands, build history shows which past builds contained the vulnerable package, direct or transitive, so you know the period you shipped it (see are we affected by a CVE?). History follows your retention, 180 days by default.
- Policy gates. A build that contains a specific CVE can be blocked before it ships.
CRACI does not do reachability analysis. If you need to know whether your code calls a vulnerable function, pair it with an SCA tool that does. See SBOM generation in CRACI or book a demo to record one of your own builds.
Transitive dependencies: frequently asked questions
What is a transitive dependency?
A transitive dependency is a package your software uses because another package needs it. If your app depends on library A and A depends on library B, then B is a transitive, or indirect, dependency of your app. You never declared it, but it ships with your software.
What is the difference between a direct and a transitive dependency?
A direct dependency is one you declare in your manifest, such as package.json, pyproject.toml or go.mod. A transitive dependency is one your direct dependencies declare, at any depth. You choose direct dependencies; the package manager chooses transitive ones when it resolves the tree.
Is a transient dependency the same as a transitive dependency?
Yes, in practice. "Transient dependency" is a common slip for "transitive dependency". The correct term is transitive, from the transitive relation: if A depends on B and B depends on C, then A depends on C.
How do I fix a vulnerability in a transitive dependency?
First upgrade the direct dependency that pulls it in, if a release with a fixed version exists. If not, force the fixed version with your package manager's override mechanism, such as npm overrides, Yarn resolutions or a pip constraints file, and test that the parent package still works with it.
How do I list all transitive dependencies?
Every package manager can print the resolved tree: npm ls --all, pnpm list --depth Infinity, uv tree or pipdeptree, go mod graph, cargo tree, mvn dependency:tree or gradle dependencies. For what a production build actually installed, record the build itself.
Does the EU Cyber Resilience Act require transitive dependencies in the SBOM?
The regulation sets the floor at "at the very least the top-level dependencies of the products" (Annex I, Part II, point 1). Transitive dependencies are not named, but a vulnerable transitive package is still in your product, and the CRA asks you to handle vulnerabilities in the components you ship.
See every dependency your build pulled in
Run one GitHub Actions workflow on CRACI and get a recorded SBOM of every package it fetched, at every depth of the tree.
Book a demo