Safeguard
Supply Chain Security•

Dependency Amplification by Ecosystem: Maven, npm, PyPI, Go, Cargo

Maven projects average 24.7 transitive packages per direct dependency, npm 4.3, PyPI 1.5. The data per ecosystem, why it differs, and how to measure yours.

Safeguard Security Team
35 min read

Maven has the highest dependency amplification of ten major ecosystems: a December 2025 study of 500 projects found each declared Java dependency brings in 24.7 transitive packages on average, against 4.48 for Go, 4.32 for npm, 1.50 for PyPI and 0.97 for Cargo. Measure your own tree with native tooling, then fix transitive vulnerabilities at the root.

That runs against the usual story, which casts npm as the worst offender. npm projects actually install slightly more packages in total; they just declare far more of them directly. Below: the research side by side, why the numbers differ, where vulnerabilities sit and how long they stay, and the commands to measure and fix your own projects.

Key takeaways

  • Maven amplifies most. Mean 24.70 times, with 28% of sampled Maven projects above 10 times; five ecosystems, including PyPI and Cargo, had none (Arafat, arXiv 2512.14739, December 2025).
  • npm has the most packages, not the highest ratio. 83.7 total packages against Maven's 80.3, but 30.9 of them declared directly.
  • Most vulnerable paths are transitive. In a study of about 3 million Maven package versions, only 31% of the 1.1 million vulnerable ones had the vulnerability in a direct dependency (Mir, Keshani and Proksch, 2023).
  • Most of those paths are never called. Only 1.2% of transitively affected packages actually reached the vulnerable code.
  • Depth adds months. In Maven, mean time to remove a vulnerable transitive dependency rises from 215 days at depth 0 to 2,075 days at depth 10, roughly six extra months per level (Przymus et al., April 2025).
  • Dev scope distorts counts. In 100 npm projects, under 1% of installed dependencies reached production (Latendresse et al., ASE 2022).
  • You can fix a transitive dependency today. Every major ecosystem lets the root project choose the version without waiting for the parent package.

What dependency amplification is and how it is measured

Dependency amplification is the ratio of transitive dependencies (everything your declared packages need, resolved recursively) to direct dependencies (what you declare in package.json, pom.xml, pyproject.toml, go.mod or Cargo.toml). Jahidul Arafat's "How Deep Does Your Dependency Tree Go?" (arXiv 2512.14739, 12 December 2025) floors the denominator at one so projects with no dependencies do not divide by zero. An amplification of 10 means each declared dependency brings ten packages you never named.

The same resolved tree gives several numbers, each answering a different question:

MetricFormulaQuestion it answers
Direct countPackages declared in the manifestHow many choices did your team make?
Transitive countResolved packages minus direct onesHow much did those choices bring along?
AmplificationTransitive divided by directHow much does each choice multiply?
Attack surface (total)Direct plus transitiveHow much third party code do you trust?
DepthLongest path from root to a leafHow many maintainers stand between you and a fix?

The paper resolved every tree with native tooling, so conflicts were mediated as a real build would. A manifest parser that follows every declared range overcounts Maven, which keeps one version per artifact, and miscounts npm, which can install several.

One method point: the headline figures are the mean of each project's ratio, not the ratio of mean counts. Maven's 74.9 transitive divided by 5.4 direct is about 13.9, not 24.70; a few extreme projects pull the mean up.

Measured amplification ratios across ten ecosystems

The study sampled 50 popular packages from each of ten ecosystems: Maven Central, npm, crates.io, PyPI, NuGet, RubyGems, Go Modules, Packagist, CocoaPods and Pub. The Kruskal Wallis test across all ten gave H = 150.90 with p = 5.74 × 10⁻²⁸, and 22 of the 45 pairwise comparisons showed large effect sizes, so the differences are not noise.

Table 1. Dependency counts and amplification per ecosystem (Arafat, arXiv 2512.14739, Tables 3 and 5, December 2025; 50 projects each)

EcosystemMean directMean transitiveMean totalMean amplification95% CIMedianP95Max
Maven5.474.980.324.7011.8 to 40.94.00112.7198.5
Go Modules7.727.334.94.482.5 to 6.81.6713.922.0
npm30.952.883.74.321.5 to 8.50.9622.176.2
RubyGems4.712.116.84.322.5 to 6.52.0011.950.0
Pub (Dart)7.515.823.22.612.0 to 3.32.145.18.5
NuGet3.97.711.62.320.9 to 4.30.677.723.0
PyPI4.26.110.21.501.1 to 2.01.002.95.0
Packagist12.212.224.51.240.9 to 1.60.922.35.3
Cargo13.715.128.80.970.7 to 1.30.752.33.9
CocoaPods0.50.71.20.320.1 to 0.50.001.32.0

Read the median beside the mean. The median Maven project amplifies four times; the mean of 24.70 comes from a long tail reaching 112.7 at the 95th percentile and 198.5 at the maximum. npm's median is below one, while its 95th percentile reaches 22.1. The Open Source Software Supply Chain Security book reproduces the same Maven, npm, NuGet and PyPI averages from this paper, as a secondary source.

Ranked: mean amplification by ecosystem

Each block is roughly one unit of amplification (one transitive package per direct dependency), rounded. Source: Arafat, arXiv 2512.14739, Table 3.

RankEcosystemMean amplificationScale
1Maven24.70×█████████████████████████
2Go Modules4.48×████▌
3npm4.32×████
3RubyGems4.32×████
5Pub2.61×██▌
6NuGet2.32×██
7PyPI1.50×█▌
8Packagist1.24×█
9Cargo0.97×█
10CocoaPods0.32×▎

First to last is a 77 fold gap; first to second is more than five fold. The paper's hierarchical clustering puts Maven in a cluster of its own.

Why Maven reaches 24.7 times while npm sits near 4.3 times

Four forces explain most of the gap.

1. The denominator: npm projects declare far more

npm projects declared 30.9 direct dependencies on average, almost six times Maven's 5.4, and amplification divides by that number. npm's small package culture shows up as a long direct list, not a deep tree under each entry. On "how much third party code do I trust?", npm and Maven are level in this sample. On "how much did I get that I never chose?", Maven is far ahead.

2. Frameworks are packaged as starters

Maven grew around comprehensive frameworks. The paper's opening example is a Spring Boot project with eight direct dependencies resolving to 127 packages; one web starter alone brings 47 transitive packages, including a servlet container and a JSON library. That is convenient, and it is why one Maven line can carry a vulnerable library you never chose.

Maven also had the weakest correlation between direct and transitive counts (Spearman's rho 0.196, not significant), against 0.906 for PyPI and 0.838 for Go. Counting a Maven project's direct dependencies tells you almost nothing about its tree. You have to resolve it.

3. Zero dependency packages pull the npm mean down

Forty per cent of sampled npm packages had no dependencies, against 28% for Maven. Each one scores zero and drags the mean down. The paper notes this explains part of npm's lower average, but Maven tops the table despite its own large zero dependency share.

4. Standard libraries and resolution rules

Python's broad standard library, Cargo's optional dependencies and feature flags, Go's standard library culture and Apple's system frameworks for CocoaPods all hold transitive growth down for structural reasons.

Resolution rules matter even more for remediation. Maven keeps one version per artifact, chosen by "nearest definition wins", with the first declaration breaking ties at equal depth (Apache Maven documentation). npm can install several versions side by side. Go's minimal version selection takes the highest version any module in the graph requires, not the newest available (Go Modules Reference). These rules decide what a fix looks like.

A scope asymmetry worth knowing

The study counted compile and runtime scope for Maven, but included development dependencies for npm and Cargo. That inflates npm and Cargo, so the asymmetry works against Maven's lead, not for it.

What share of projects exceed 10 times amplification

A mean hides whether a problem is systematic or confined to outliers. The paper answers that with the share of projects above 10 times and a concentration ratio (maximum attack surface over mean).

Table 2. Exposure profile per ecosystem (Arafat, arXiv 2512.14739, Tables 6, 7 and 8, December 2025)

EcosystemProjects at or above 10×Mean attack surface (packages)P95 attack surfaceMax attack surfaceZero dependency shareDirect to transitive correlation (rho)
Maven14 of 50 (28%)80.3397.245028%0.196 (not significant)
RubyGems7 of 50 (14%)16.849.67816%0.368
npm6 of 50 (12%)83.7250.678540%0.398
Go Modules3 of 50 (6%)34.9133.626336%0.838
NuGet2 of 50 (4%)11.630.016312%0.397
Cargo0 (0%)28.8132.418910%0.681
Packagist0 (0%)24.563.013210%0.342
Pub0 (0%)23.254.36120%0.598
PyPI0 (0%)10.230.25720%0.906
CocoaPods0 (0%)1.27.01276%0.950

Two risk shapes appear. Maven's is systematic: over a quarter of projects exceed 10 times, with a concentration ratio of 5.6. npm's is outlier driven: a ratio of 9.4, and the largest tree in the study, 785 packages. So audit every Maven tree, and hunt for the outliers in npm and RubyGems. Cargo is wide rather than deep: no project crossed 10 times, yet its 95th percentile attack surface (132.4) is close to Go's.

How deep dependency trees go, and where vulnerabilities sit

Amplification counts packages. Depth counts the links between you and the package that needs fixing, and it governs how long a fix takes to arrive.

Where vulnerable packages sit in npm

"Original Sin of npm" (Robinson et al., arXiv 2604.17668, April 2026, ACM ASIA CCS 2026) sampled 1,077,946 npm packages from a snapshot of 3.9 million, built their dependency networks with the deps.dev API and matched them against OSV and NVD. Its findings:

  • 21.60% of the sampled packages (232,836) had at least one known vulnerability somewhere in their dependency network.
  • 61.30% depended on at least one other package.
  • Affected packages clustered at four to six levels deep.
  • The top five CVEs accounted for 21.24% of vulnerability instances and touched 41.58% of affected packages; 23 CVEs accounted for half of all cases.
  • The median time from the first affected release to a fix was 59 months.

That concentration helps defenders: fixing a few widely used packages, once, in the right place, removes a large share of findings.

Where vulnerable packages sit in Maven

Mir, Keshani and Proksch ("On the Effect of Transitivity and Granularity on Vulnerability Propagation in the Maven Ecosystem", arXiv 2301.07972, January 2023) resolved full transitive trees for about 3 million recent Maven package versions and built whole program call graphs to test reachability. Their results:

  • About 1.1 million package versions, roughly 40% of the dataset, were affected by a known vulnerability somewhere in their transitive dependency set.
  • Only 31% of those had the vulnerability in a direct dependency. The rest were exposed only through transitive ones.
  • At the call graph level, only 1.2% of transitively affected packages actually had a reachable path to the vulnerable code.
  • With reachability analysis restricted to direct dependencies, 55.8% of the truly affected packages were found. Extending to depth three found 94%.

Package level matching across the whole tree finds almost everything, including many paths your code never calls. Matching only direct dependencies misses most exposure. Reachability across the transitive tree turns a huge list into a short one. The same paper cites the Log4j flaw disclosed at the end of 2021 as affecting around 35,000 Java projects, about 8% of the Maven ecosystem; see Log4Shell and the lessons of transitive Java dependencies.

How long transitive vulnerabilities persist in Maven

"Out of Sight, Still at Risk: The Lifecycle of Transitive Vulnerabilities in Maven" (Przymus, Fejzer, Narębski, Rykaczewski and Stencel, arXiv 2504.04803, April 2025) ran survival analysis over 132,235 Maven projects, 3.38 million artifacts and 2,676 CVEs spanning August 2005 to August 2024. It measured two clocks:

  • Cumulative time to fix: from CVE publication until the project no longer has the vulnerable version anywhere in its tree.
  • Single level time to fix: how long one artifact takes to ship a release free of the vulnerable version once its own dependency is fixed.

Table 3. Time to remove a vulnerable transitive dependency by depth, Maven (Przymus et al., arXiv 2504.04803, Table II, April 2025)

Depth levelMean cumulative daysMedian cumulative daysMean single level daysDependencies at this level
0215651463,046
142914093184,579
259733487417,797
379235583439,878
41,10458482455,303
51,14567678482,524
61,36488874428,033
71,49295872375,772
81,9001,61773248,152
91,9431,64268198,331
102,0751,82259149,685

Look at the fourth column. Each maintainer is reasonably quick: after level 0, single level delay is 59 to 93 days on average. The damage is in the sum, since each link waits for its own release. The authors' rule of thumb: mean CVE lifetime of about six months per level plus eight months. Dependencies peak at level 5, where mean cumulative delay already exceeds three years.

The authors note two caveats: they treat every project with a vulnerable dependency as vulnerable, which overstates exposure, and they measure from CVE publication, ignoring embargoes. Even so, at depth three or more, waiting for the chain is waiting for years.

How production versus dev scope changes the numbers

The same project can produce counts an order of magnitude apart depending on scope. Latendresse, Mujahid, Costa and Shihab ("Not All Dependencies are Equal", ASE 2022, arXiv 2207.14711) studied 100 JavaScript projects: under 1% of installed dependencies were released to production, 59% of declared runtime dependencies were not used in production, and 28.2% of declared development dependencies were. Most security alerts, they concluded, target dependencies that never ship.

EcosystemWhat counts as "shipped"What usually does notCommand flag to separate them
npmdependencies (and installed peers)devDependenciesnpm ls --omit=dev
Mavencompile and runtime scopetest and provided are not transitive-Dscope=runtime or -DincludeScope=runtime
GradleruntimeClasspathtestRuntimeClasspath, annotation processors--configuration runtimeClasspath
Cargonormal dependenciesdev dependencies; build dependencies run at build time onlycargo tree -e normal or -e no-dev
Gomodules that provide packages to your buildmodules present in the graph but contributing no imported packagego list -deps ./... versus go list -m all
PyPIwhat your runtime environment installstest and lint extras, build backendsseparate requirements or dependency groups

Apache's documentation states that test and provided dependencies are not transitive. npm shows the largest gap because bundling, linting and test tooling lands in the same tree. Two warnings: build and development dependencies still run on laptops and CI runners with credentials, and declared scope can be wrong both ways, as the 28.2% figure shows. Count production scope for prioritisation; keep the full tree for build hygiene.

Where the datasets disagree

QuestionOne answerAnother answerWhy they differ
Which ecosystem amplifies most?Maven, 24.70× (Arafat 2025)npm is commonly assumed to be worstAmplification divides by direct count; npm projects declare 30.9 direct dependencies
Which ecosystem has the most packages per project?npm, 83.7 mean totalMaven, 80.3 mean totalClose in this sample; npm counts include dev and peer dependencies
Maven amplification24.70× mean of ratiosabout 13.9× ratio of means (74.9 / 5.4)A few extreme projects raise the mean of ratios
Share of Maven vulnerable packages via transitive paths69% transitive only (31% direct)Abstract phrases it as "about 1/3" needing full transitive resolutionDifferent denominators in the same paper (vulnerable packages versus whole dataset)
Original Sin of npm, CVE concentration23 CVEs account for half of cases (results)"only 20 CVEs" (discussion)Internal inconsistency in the preprint; we use the results section
Dependency Dilemmas, independent artifacts15.4% stated93,101 of 635,003 is about 14.7%Denominator unclear in the abstract; treat as approximate

None of these change the practical conclusions, but they are a reason to name the scope, sample and averaging method whenever you quote a dependency figure.

Measure your own dependency tree, step by step

The research describes popular packages, not your applications. These steps give you direct, transitive and depth counts with native tooling. Run them from a clean checkout with a lockfile.

Step 1: npm

# Direct production dependencies
npm ls --omit=dev --depth=0 --parseable | tail -n +2 | wc -l

# Every installed production package path (logical tree, all depths)
npm ls --omit=dev --all --parseable | tail -n +2 | sort -u | wc -l

# Deepest logical chain in the production tree
npm ls --omit=dev --all --json \
  | jq '[paths(objects) | select(length > 1 and .[-2] == "dependencies") | length / 2] | max'

# Count non-dev packages straight from a v2 or v3 lockfile
jq '[.packages | to_entries[] | select(.key != "" and (.value.dev | not))] | length' package-lock.json

# Why is a package in the tree?
npm explain minimist

npm ls shows the logical graph, not the node_modules layout, and recurses one level without --all. Two versions of one package count twice, which is right for exposure.

Step 2: PyPI

# Resolve without installing and write a machine-readable report
python -m pip install --dry-run --ignore-installed --quiet \
  --report report.json -r requirements.in

# Everything pip would install
jq '.install | length' report.json

# What you asked for directly
jq '[.install[] | select(.requested)] | length' report.json

# Tree view and reverse lookups from an installed environment
pipdeptree
pipdeptree --reverse --packages urllib3
pipdeptree -o json > tree.json

pipdeptree 4.2.9, released 19 October 2026, also writes CycloneDX JSON and can read a PEP 751 pylock.toml without installing anything. Older releases use --json rather than -o json.

Step 3: Maven

# Direct dependencies only, compile and runtime scope
mvn -q dependency:list -DincludeScope=runtime -DexcludeTransitive=true -DoutputFile=direct.txt

# Full resolved set, compile and runtime scope
mvn -q dependency:list -DincludeScope=runtime -DoutputFile=all.txt
grep -cE '^ +[^ ]+:[^ ]+:' direct.txt all.txt

# Tree, including nodes dropped during conflict mediation
mvn dependency:tree -Dscope=runtime -Dverbose=true

# Where does one artifact come from?
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind

# Machine-readable tree (maven-dependency-plugin 3.7.0 or later)
mvn dependency:tree -DoutputType=json -DoutputFile=target/dependency-tree.json

-Dverbose=true is the flag most teams miss: without it you only see the versions Maven kept, not the vulnerable one another path requested.

Step 4: Gradle

./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyInsight --dependency commons-codec --configuration runtimeClasspath

dependencyInsight explains which path selected a version, which you need before adding a constraint.

Step 5: Go

# Modules in the build list (includes modules that may contribute no package)
go list -m all | tail -n +2 | wc -l

# Direct requirements of the main module
go list -m -f '{{if not .Indirect}}{{.Path}}{{end}}' all | grep -v '^$' | tail -n +2 | wc -l

# Modules that actually provide packages to your build
go list -deps -f '{{if not .Standard}}{{with .Module}}{{.Path}}{{end}}{{end}}' ./... | sort -u | wc -l

# Raw module requirement graph: one edge per line
go mod graph | wc -l

# Why is a module needed?
go mod why -m golang.org/x/net

The gap between go list -m all and go list -deps is Go's scope question: the module graph can hold modules your build never imports a package from.

Step 6: Cargo

# Direct normal dependencies
cargo tree -e normal --depth 1 --prefix none | tail -n +2 | wc -l

# Unique packages in the normal dependency tree (includes the root line)
cargo tree -e normal --prefix none | sed 's/ (\*)$//' | sort -u | wc -l

# Maximum depth
cargo tree -e normal --prefix depth | sed -E 's/^([0-9]+).*/\1/' | sort -n | tail -1

# Who depends on a crate, and which crates appear in more than one version
cargo tree -i openssl-sys
cargo tree -d

cargo tree marks repeated packages with (*), which the sed strips. It shows normal, build and dev edges by default, so pass -e normal to approximate what ships.

Step 7: record the numbers

Record direct, transitive, total, amplification, maximum depth, scope and date per repository, and compare against Tables 1 and 2. Above your ecosystem's 95th percentile is worth a conversation; a release that jumps by dozens of packages is worth a review (see is transitive growth a risk signal). With the Safeguard CLI, safeguard scan --dir . writes JSON results under ./output, and the platform shows which projects contain a given component and version.

Fix a transitive dependency without waiting for the parent package

Table 3 shows what waiting for each parent costs. Every major ecosystem lets the root project choose the version itself.

Table 4. Override mechanisms by ecosystem (official documentation, fetched 23 October 2026)

EcosystemMechanismScope of effectMain restriction
npmoverrides in package.jsonWhole tree, or only under a named parentRead only from the root package.json; conflicts with a direct dependency spec unless you use the $name reference
MavendependencyManagement, or a direct declarationTakes precedence over mediation for transitive dependenciesDoes not affect plugins' dependencies
MavenexclusionsRemoves one transitive artifact from one pathYou must supply the replacement yourself if it is still needed
Gradleconstraints with optional strictlyParticipates in conflict resolution without adding a direct dependencyPublished only in Gradle Module Metadata; Maven consumers may not see it
Cargo[patch.crates-io]Whole graph, including transitive pathsRoot workspace only; patched version must satisfy the requirement
Gorequire a higher version; replace for a forkMain module only; replace and exclude are ignored in dependenciesreplace needs a matching require
PyPIconstraints file with -cLimits versions the resolver may choose, including indirect onesDoes not install anything by itself; as of pip 26.2 does not affect isolated build environments

npm

{
  "dependencies": { "some-framework": "^4.2.0" },
  "overrides": {
    "minimist": "1.2.8",
    "some-framework": { "semver": "7.5.4" }
  }
}

The first entry applies everywhere, the second only beneath some-framework. Confirm with npm ls minimist.

Maven

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.17.2</version>
    </dependency>
  </dependencies>
</dependencyManagement>

Per Apache, dependencyManagement takes precedence over mediation for transitive dependencies. Confirm with mvn dependency:tree -Dverbose=true. With a framework BOM, set the version property the BOM exposes, or declare the artifact directly in your own dependencyManagement.

Gradle (Kotlin DSL)

dependencies {
    constraints {
        implementation("commons-codec:commons-codec:1.15") {
            because("transitive 1.11 has a known vulnerability")
        }
    }
}

This mirrors Gradle's own documentation example. Add strictly only to stop anything upgrading past your pin.

Cargo

[patch.crates-io]
some-crate = { git = "https://github.com/your-org/some-crate", branch = "security-fix" }

Check with cargo tree -i some-crate. For a bump within the allowed range, cargo update -p some-crate --precise 1.2.3 needs no patch.

Go

go get golang.org/x/net@v0.33.0
go mod tidy
go list -m golang.org/x/net

Minimal version selection takes the highest required version, so requiring a newer one in your go.mod raises it for the whole build. Use replace only for a fork; it is ignored when others consume your module.

PyPI

echo "urllib3>=2.2.2" > constraints.txt
python -m pip install -r requirements.txt -c constraints.txt
pipdeptree --reverse --packages urllib3

Overrides are a bridge, not a destination

Each override uses a version the parent's maintainers did not test. Run your tests, record the reason (Gradle's because is a good model), and remove it once the parent ships the fix. Our guide to fixing a vulnerable transitive dependency in npm, pip, Maven and Gradle covers edge cases.

What dependency budget is reasonable per project type

No study prescribes a budget. This is our suggestion, using each ecosystem's own spread from Tables 1 and 2 as the yardstick.

Project typeSuggested targetReview triggerRationale
Library you publishAt or below your ecosystem's median amplification (Maven 4, Go 1.7, npm about 1, PyPI 1)Above the ecosystem's P95 amplificationEvery consumer inherits your tree; amplification compounds downstream
Command line toolSmall, flat tree; production scope onlyAny new dependency that adds more than about ten transitive packagesTools run with the user's credentials; see a practical budget for CLI dependencies
Internal serviceTotal packages below your ecosystem's P95 attack surface (Maven 397, npm 251, Go 134, Cargo 132, PyPI 30)Crossing P95, or a single release adding dozens of packagesAbove P95 you are an outlier even among popular projects
Framework heavy enterprise Java serviceTrack amplification per starter or BOM rather than per projectAny starter contributing more than 40 transitive packages you do not useMaven's weak direct to transitive correlation means only the resolved tree tells you the truth
Front end applicationProduction bundle only for vulnerability triage; full tree for build hygieneProduction dependency count rising faster than featuresUnder 1% of installed npm packages reached production in the ASE 2022 study

Treat triggers as prompts, not build gates. A Spring service with 300 packages is normal; a Python microservice with 300 is worth a look.

What this means for a security programme

  • Weight effort by ecosystem shape. Maven's amplification is systematic, so audit every Java tree; for npm and RubyGems, sweep for outliers.
  • Report scope and reachability with every count. With 31% direct, 1.2% reachable and under 1% shipped in the studies above, raw transitive counts overstate exposure by one to two orders of magnitude.
  • Fix at the root. Override in the root manifest, or bump the direct parent once it has shipped.
  • Watch growth. A jump in transitive count is a cheap early signal that the tree you trust has changed.
  • Use the graph in incidents. The first question is which products contain the component, at which version, through which path. See SBOMs for finding affected products fast and dependency graph analysis for transitive risk.

How we measured

This post synthesises published research and official documentation; it is not a new crawl.

  • Amplification, attack surface, zero dependency share and correlation: Arafat (arXiv 2512.14739v1, 12 December 2025), Tables 2 to 9, read from the PDF on 23 October 2026. Fifty popular packages per ecosystem, native resolution. Scopes: Maven compile and runtime; npm production, development and peer; Cargo normal, development and build. Amplification is the mean of per project ratios.
  • Maven propagation and reachability: Mir, Keshani and Proksch (arXiv 2301.07972, January 2023), about 3 million package versions with call graphs.
  • Maven persistence by depth: Przymus et al. (arXiv 2504.04803, April 2025), Table II, 132,235 projects and 2,676 CVEs.
  • npm depth and concentration: Robinson et al. (arXiv 2604.17668, April 2026), 1,077,946 packages via deps.dev, OSV and NVD.
  • npm production scope: Latendresse et al. (arXiv 2207.14711, ASE 2022), 100 projects.
  • Maven Central independence: Shanto et al. (arXiv 2504.12261, MSR 2025), 658,078 artifacts, 93,101 with no incoming dependencies.
  • Commands and overrides: official npm, Maven, Gradle, Cargo, Go and pip documentation and the pipdeptree project page, fetched 23 October 2026.

We recomputed nothing; derived numbers (13.9 ratio of means, 69% transitive only) show their arithmetic, and the budget table is labelled as our suggestion. Limits: fifty projects per ecosystem is small, popular packages are libraries rather than applications, scope rules differ between studies, and npm's multi version installs change what depth means. Treat every figure as a dated snapshot and measure your own.

Common mistakes

  • Counting only direct dependencies. In Maven especially, the direct count predicts almost nothing about the tree (Spearman's rho 0.196 in the 2025 study).
  • Comparing ecosystems on raw package counts without scope. An npm count that includes devDependencies and a Maven count of runtime scope are not comparable.
  • Reading mvn dependency:tree without -Dverbose=true. You will miss the vulnerable version a parent requested before mediation dropped it, and you will be surprised when a different build order brings it back.
  • Counting Go's module graph as the build. go list -m all includes modules that contribute no imported package; use go list -deps ./... for what compiles.
  • Leaving overrides in forever. Each one is untested by the parent's maintainers. Record why it exists and remove it when upstream catches up.
  • Treating every transitive finding as equally urgent. With roughly 1% of transitive paths reachable in the Maven study, triage by reachability before you open tickets.
  • Ignoring build and development dependencies entirely. They do not ship, but they run in CI with secrets. Track them under a different policy rather than dropping them.

Fix it without waiting for upstream

Deep trees are where waiting hurts most: the Maven persistence data shows each extra level adds about six months before a fix reaches you through the normal release chain. The options below let the root project act on the day a fix exists, and give you a path when one does not.

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

FAQ

What is dependency amplification?

The ratio of transitive to direct dependencies. Declare 5 packages, resolve 75 more, and your amplification is 15.

Which package ecosystem has the most transitive dependencies?

By amplification, Maven: 24.70 times on average, 28% of projects above 10 times. By total packages, npm edges ahead, 83.7 against 80.3, because npm projects declare far more directly.

How many dependencies does the average Java project have?

In the 2025 sample, 5.4 direct and 74.9 transitive at compile and runtime scope. Framework applications are larger; the paper's Spring Boot example resolves 127 packages from eight declarations.

How do I count transitive dependencies in my project?

Use native tooling: npm ls --all, pip install --dry-run --report, mvn dependency:list, go list -deps ./... and cargo tree -e normal. The step by step section has full commands.

What share of vulnerabilities come from transitive dependencies?

In about 3 million Maven package versions, 69% of vulnerable ones were exposed only transitively. In npm, affected packages cluster four to six levels deep.

How long do transitive vulnerabilities stay in Maven projects?

Mean time to removal rises from 215 days at depth 0 to 2,075 days at depth 10, about six months per level.

Can I fix a transitive vulnerability without waiting for the parent package?

Yes: npm overrides, Maven dependencyManagement, Gradle constraints, Cargo [patch], a higher Go require, or a pip constraints file. Test, then remove the override once the parent ships.

Do dev dependencies count toward my attack surface?

For production triage, usually not; under 1% of installed npm dependencies reached production in one study. For build security, yes, since they run in CI.

Does reachability analysis remove the need to patch transitive dependencies?

No. It orders the work. Only 1.2% of transitively affected Maven packages reached vulnerable code, but reachability changes as code changes, so keep the rest on a backlog.

See how Safeguard would help

This demo shows a real Safeguard CLI session fixing the lodash CVE in a repository: it reads the finding and advisory, asks before each command, edits package.json, runs npm install and npm test with your approval, and pushes nothing. The same flow applies a root level override or a parent bump for a transitive finding.

Click through it at your own pace, or open it full screen.

This demo sets up the Safeguard runner on your own machine, approves it with a device code and scans from the terminal. That is how you run the measure step against private repositories where the resolved tree lives.

Click through it at your own pace, or open it full screen.

This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. You can then ask which projects contain a component and version, the first question in any transitive dependency incident.

Click through it at your own pace, or open it full screen.

To try it on your own repositories, download Safeguard and follow the CLI documentation.

Quick check

Why does npm show lower amplification than Maven even though npm projects install slightly more packages?

Amplification divides transitive packages by direct ones. npm projects in the 2025 study declared 30.9 direct dependencies on average against Maven's 5.4, so a similar total produces a much smaller ratio.

A vulnerable library sits four levels down in a Maven project. What is the fastest safe fix?

Set the fixed version in your own dependencyManagement section, confirm with mvn dependency:tree -Dverbose=true, and run your tests. Waiting for each parent to release would take years on average at that depth.

Sources

Last reviewed: 23 October 2026. Research figures are snapshots from the dates shown; measure your own projects with the commands above.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.