Originally published on the depproof blog.
Measured 10 September 2026 across 24 pinned public repositories, 8,943 components, five ecosystems. Every source used is free and public.
The standard remediation for a vulnerable dependency is to upgrade it. That advice quietly assumes a maintainer exists, noticed, and shipped a release.
For most packages it holds. For some it does not, and nothing in a normal findings report tells you which kind you are looking at.
The number, and the argument against it
About one component in seven in our sample carries a signal that nobody is maintaining it: 1,202 of 8,943, or 13.4%.
That figure is worth less than the split inside it, so here is the split first.
| Where the signal comes from | Components | Share of signals |
|---|---|---|
| Somebody said so. Deprecation flag, archived repository, published end-of-support date. | 242 | 20.1% |
| We inferred it. Four years since the last release, and no repository activity either. | 960 | 79.9% |
Four signals in five are an inference from silence. That is the honest shape of thismeasurement, and it is where the fair objection lives: a library can go four years without a release because it is finished, not because it was abandoned. Small, single-purpose packages do exactly that.
We think the inference still earns its place, for one reason. The question a maintenance signal answers is not is this package bad. It is if an advisory lands against this tomorrow, is a fix coming. A finished library and an abandoned one give the same answer, and it is the answer you need during an incident rather than a verdict on anybody's work.
What it looks like in a real tree
request@2.75.0
deprecated by maintainer
depth 4 · direct: no · scope: build and test
advisory CVE-2023-28155 · fix version: none published
For years this was one of the most depended-upon packages in JavaScript. Its maintainers formally deprecated it, which is a deliberate statement rather than an inference. Nobody in this project chose it: it arrives four levels down, underneath something else. The advisory against it has no fix version, because there will not be another release.
There is no upgrade to recommend. The only real options are to replace whatever pulled it in, or to write down that you accept it.
How we measured it
Twenty-four public repositories, pinned to fixed commits and rebuilt locally, across Maven, Gradle, npm, PyPI and Go. For every resolved component we asked three open sources what they knew:
- deps.dev — release history and npm deprecation
- OpenSSF Scorecard — project activity
- endoflife.date — published support windows
Those feed a ladder of five rules, applied in order, first match winning. The order matters: a maintainer's own declaration should outrank anything we merely observed.
| Rule | Kind | Components | |
|---|---|---|---|
| 01 | The package is deprecated. The maintainer marked it so. | stated | 65 |
| 02 | The repository is archived. A deliberate act, not a guess. | stated | 125 |
| 03 | This release line is past its published end of support, even where the project thrives. | stated | 24 |
| 04 | Four years since the last release, and no sign of commits. Both halves required. | inferred | 960 |
| 05 | This particular release is deprecated, though newer ones are not. | stated | 28 |
Rule 4 deliberately requires both an old release and a quiet repository, because release age alone marks every finished library as abandoned. Where the Scorecard has no data the second half cannot be applied and the rule falls back to release age by itself. That affected 82 of its 960 components, so roughly 9% of the largest rule rests on the weaker evidence.
By ecosystem
| Ecosystem | Carrying a signal | Components | Share |
|---|---|---|---|
| npm | 852 | 5,650 | 15.1% |
| Go | 226 | 1,614 | 14.0% |
| Gradle | 90 | 756 | 11.9% |
| Maven | 13 | 237 | 5.5% |
| PyPI | 21 | 686 | 3.1% |
The npm figure reflects deep transitive trees in two large JavaScript projects. The mix here is ours, not yours.
The result worth arguing about
The obvious way to dismiss all of this is to assume abandoned packages cluster in build and test dependencies, where nobody much cares. We can check that, because every resolved dependency carries the scope it was declared under.
| Reaches production | Carrying a signal | Components | Share |
|---|---|---|---|
| Ships | 659 | 4,698 | 14.0% |
| Build and test only | 372 | 2,750 | 13.5% |
| Undetermined | 171 | 1,332 | 12.8% |
It does not cluster. The rate is flat. Whatever causes a package to be abandoned has nothing to do with whether you ship it. Your production dependencies are not the well-tended ones. They are tended exactly as well as your test tooling, which is to say nobody checked either.
The cost lands at the worst possible moment. When a critical advisory drops, the escalation runs the same way every time: someone is paged, a patch window opens, teams stop what they were doing. All of it assumes a fixed version exists.
The result that cuts against us
Findings concentrate more narrowly than components. 57 of 933 findings sit on a component carrying a maintenance signal, which is 6.1%. Of those, exactly three had no fix version published at all, and none of the three ship. Two are build-and-test dependencies, one has no scope recorded. Everywhere else a newer release existed.
We went looking for the acute case, a vulnerability in production with nowhere to upgrade to, and did not find it once. On this sample the maintenance signal is a statement about a project's future, not about today's remedy. Anyone quoting these numbers should quote that too.
And a retraction
An earlier internal draft of this measurement reported that unmaintained components carry worse findings, with criticals running at more than three times the rate of lows. That was the headline. It does not survive a re-run.
| Severity | Earlier draft | Re-run |
|---|---|---|
| Critical | 17.9% | 4.9% |
| High | 14.2% | 6.7% |
| Medium | 8.5% | 6.6% |
| Low | 5.4% | 3.0% |
Criticals now sit near the bottom of the range rather than the top, under both counting units we tried. We cannot reconstruct the earlier run to explain the difference, which is itself the lesson: a measurement you cannot re-run is a number you should stop quoting.
What this does not prove
- The sample is 24 pinned open-source repositories chosen for ecosystem coverage. It is not anyone's enterprise estate.
- Four signals in five are inferred, and 82 of those rest on release age alone.
- The end-of-support rule depends on a hand-curated map from package coordinates to products and found 24 components. Treat that row as a floor.
- A maintenance signal is not a verdict about a package or the people who wrote it. Many of these are finished libraries doing exactly what they were built to do.
- Scope tells you what ships, not what runs. Whether a dependency ever loads at runtime would sharpen every number here, and we could not measure it.
The part we have not solved
Everyone agrees an abandoned dependency is a problem. Nobody agrees what you actually do on a Tuesday when it is four levels down, there is no fork, and the thing that pulled it in is a package you do need.
Things I would genuinely like argued with in the comments:
- Is four years the right threshold, or does it depend entirely on the ecosystem?
- Should a deprecated direct dependency read differently from a deprecated transitive one,given only one of them was a decision anybody made?
- Is this worth surfacing at all on a package that never reaches your shipped artifact?
- Does the flat split between shipped and test-only match what you see, or does your estate concentrate this somewhere ours did not?
Full method, the individual records and the citation block are in the original post. Figures and tables may be reproduced with attribution and a link back. If you re-run this against your own code and get something different, I would rather hear it than not.
Top comments (0)