Advisory Lag 2026 — We Measured It, and It Isn't There
Bottom line up front
Why we ran this — and what we expected
The premise seemed obvious. A CVE lands on NVD. Your scanner reads a registry advisory database. Until someone writes that advisory, your scan reports clean on a package that is publicly known to be vulnerable. Call it the advisory lag window — the period where NVD knows and your tooling doesn't.
We had written that claim ourselves, more than once, and wanted to size it per ecosystem so teams could set a rescan cadence against it.
The data did not cooperate. It is worth publishing precisely because it contradicts the thing we expected to confirm.
Method
- NVD side: every CVE with a
publisheddate in calendar year 2025, from CVE API 2.0 in four chunked quarterly queries at roughly one request per seven seconds. 49,972 records fetched; 1,810 inRejectedstate excluded; 48,162 usable. - OSV side: the complete bulk
all.zipexport for each of the eight ecosystems GeekWala supports — roughly 280,000 advisory records, from npm's 224,261 down to crates.io's 2,697. - Matching rule: for each CVE, the earliest non-withdrawn OSV advisory in a given ecosystem carrying that CVE in its
aliases.lag_days = advisory.published − cve.published. - Retained, not excluded: negative lag. It is not an error; it is the finding.
Full per-CVE rows, aggregates and limitations are in the dataset, CC BY 4.0.
Finding 1 — The lag is zero
| Ecosystem | n matched | median (days) | p90 (days) | max (days) | advisory precedes CVE |
|---|---|---|---|---|---|
| npm | 545 | 0.04 | 0.71 | 451.1 | 35.1% |
| PyPI | 738 | 0.00 | 0.16 | 427.9 | 28.6% |
| Maven | 671 | 0.05 | 0.10 | 346.2 | 12.8% |
| Packagist | 643 | 0.01 | 0.15 | 297.9 | 44.3% |
| Go | 649 | 0.01 | 0.46 | 21.0 | 38.2% |
| crates.io | 118 | −0.10 | 0.68 | 279.9 | 60.2% |
| RubyGems | 58 | 0.00 | 0.16 | 3.4 | 50.0% |
| NuGet | 91 | 0.05 | 8.99 | 41.9 | 36.3% |
Pooled across all eight: median 0.01 days, p90 0.21 days (n = 3,513 rows / 3,456 unique CVEs).
Same-day, everywhere. The 90th percentile is measured in hours — NuGet's 8.99-day p90 is the only tail worth naming, and it sits on a thin sample of 91.
The negative-lag column is the more interesting one. Coordinated disclosure means the ecosystem frequently publishes its advisory before the CVE record is minted. On crates.io the advisory usually comes first. Anyone building a workflow around "watch NVD, then wait for the registry to catch up" has the arrow pointing the wrong way a third of the time.
The distribution is not a curve with a tail — it is a spike at zero. Roughly a third of matched rows sit on the negative side of it.
Finding 2 — The real gap is coverage
Of 48,162 non-rejected CVEs published in 2025, only 3,456 (7.18%) ever matched an advisory in any of the eight tracked ecosystems. The other 44,706 (92.82%) did not.
That number needs an immediate caveat, and we would rather give it than let the figure be quoted without it: most CVEs are not open-source package vulnerabilities at all. NVD covers firmware, hypervisors, network appliances, enterprise software and operating systems. A CVE in a router has no reason to appear in npm's advisory database. This dataset cannot cleanly separate "not yet mirrored" from "not a package vulnerability in the first place," and we have not tried to guess the split.
What we can measure is the share of each ecosystem's own advisories carrying no CVE identifier at all:
| Ecosystem | no CVE alias | excluding malware reports |
|---|---|---|
| npm | 97.5% | 20.8% |
| RubyGems | 78.0% | — |
| PyPI | 49.9% | 5.6% |
| NuGet | 46.8% | — |
| crates.io | 41.2% | — |
| Packagist | 14.5% | — |
| Go | 9.6% | — |
| Maven | 3.0% | — |
npm's raw 97.5% is almost entirely an artifact: 96.9% of its no-CVE advisories are MAL- prefixed malware and typosquat reports, which can never carry a CVE by construction. Strip those and the real figure is 20.8%.
PyPI is the cleanest read: 5.6% of genuine vulnerability advisories have no CVE. Those findings are invisible to a CVE-keyed workflow permanently — not for a few hours.
Finding 3 — Severity does not move the lag
| Severity | n | median lag (days) |
|---|---|---|
| CRITICAL | 432 | 0.01 |
| HIGH | 1,046 | 0.01 |
| MEDIUM | 1,596 | 0.01 |
| LOW | 132 | 0.02 |
No meaningful differentiation. Registries are not triaging critical CVEs onto a faster advisory path, because there is no slower path to be faster than.
Finding 4 — The monthly trend is not a trend
A mechanical threshold test labels the year's direction "increasing." We are not reporting it that way, because all twelve monthly medians sit between 0.00 and 0.05 days of each other. That is noise inside a rounding error, and calling it a trend would be exactly the over-reading this report exists to avoid.
Why the myth persists
If the lag is zero, why does everyone — including us — believe in it?
Three reasons, and they are all reasonable mistakes. First, NVD enrichment is genuinely slow, and people conflate it with advisory publication. A CVE can sit in Awaiting Analysis without CVSS vectors or CPE data for weeks. That is real, visible, and frequently complained about — but it is a different clock from the one measured here. The OSV advisory does not wait for NVD's analyst queue.
Second, the tail is memorable and the median is not. Our own data has a 451-day maximum on npm. Anyone who has been burned by one of those remembers it vividly, and generalises from it. The p90 of 0.71 days is the number that should drive process design, and nobody has an anecdote about it.
Third, the coverage gap feels like a latency gap from inside a scanner. If a package has a real vulnerability with no CVE and your tooling is CVE-keyed, the scan reports clean. That looks exactly like waiting for an advisory that hasn't arrived. It isn't — it is an advisory that will never arrive in the form your pipeline is watching for.
What this means for your rescan cadence
The honest advice inverts what we would have written before measuring:
- Do not build a rescan cadence around advisory lag. There isn't enough of it to design against. If your scanner reads OSV, it knows what NVD knows, essentially immediately.
- Do build one around disclosure volume. New advisories land continuously; a weekly scan means up to seven days of not knowing about something published on day one. That exposure is real — it comes from your scan interval, not from registry latency.
- Assume CVE-keyed coverage is incomplete. A meaningful share of real vulnerability advisories never get a CVE. If your pipeline joins on CVE identifiers, it misses them silently.
Limitations
- This measures advisory-curation lag, not OSV-ingestion lag. An ecosystem-native advisory's
publishedtimestamp reflects when that ecosystem curated it, not when OSV mirrored it. Different quantities; only the first is measured here. - The 92.82% "no match" figure is dominated by CVEs that are not package vulnerabilities, not by undiscovered lag. It should not be quoted as a coverage failure rate.
- The negative-lag tail contains artifacts. Extremes reaching −1,985 days almost certainly reflect CVE reuse or backfill, not five-year-early disclosure. The median is robust to this; the minimum is not.
- Only calendar year 2025 was analysed — no cross-year comparison.
- Only the eight ecosystems GeekWala supports. Other OSV ecosystems were not pulled.
- The per-ecosystem
unresolved_cve_aliascounts (879–6,004) are not split between "CVE from a different year" (expected, probably dominant) and genuine mismatch. - Sample sizes for RubyGems (58) and crates.io (118) are thin. Their percentages are directionally useful and nothing more.
Dataset and methodology
Every number on this page comes from the linked dataset. It carries the aggregates — per-ecosystem lag distributions, the severity breakdown, the monthly series, advisory-composition counts and the CVE match summary — together with the full method and a longer limitations list than the seven above. It does not ship per-CVE rows; the join covered 48,162 CVEs and the published artifact is the aggregate layer over them. Licensed CC BY 4.0 — reproduce, cite, or extend it freely.
The analysis was a direct join run against the two public sources named in the method above, and the exact procedure — endpoints, chunking, matching rule and exclusions — is recorded in the dataset's own methodology field so it can be reproduced independently. (Note for anyone browsing the repository: scripts/research/ holds the pipeline for the quarterly exploitation series, which is a different report and a different window. Don't reach for it expecting these figures.)
Citation
GeekWala Research (2026). Advisory Lag 2026: NVD-to-OSV Advisory Publication Lag Dataset. CC BY 4.0. https://www.geekwala.com/research/advisory-lag-2026
If you reach a different conclusion from the same rows, we would rather know.