DEV Community

Cover image for Your EOL Risk List Is Probably Sorted Wrong
end-of-life.org
end-of-life.org

Posted on

Your EOL Risk List Is Probably Sorted Wrong

Every "upgrade this now" list I have seen ranks end-of-life software the same way:
count the CVEs published since the version stopped receiving fixes, sort descending,
start at the top.

That ordering is easy to compute and it is misleading.

Here is the current top of that list, pulled from
end-of-life.org, which joins vendor EOL dates against
the NVD and counts what each dead branch never received:

Version EOL since CVEs missed Rated critical
MySQL 5.7 Oct 2023 127 0
Go 1.15 Aug 2021 122 12
Go 1.16 Mar 2022 111 10
Go 1.17 Aug 2022 106 10
MySQL 8.1 Oct 2023 103 0

Key Takeaways

  • The version at the top of the list by CVE count has zero critical CVEs. The one below it has twelve.
  • CVE count conflates two different things: how long the branch has been dead, and how fast vulnerabilities accumulate in that ecosystem.
  • Normalising by time changes the ranking. MySQL accrues missed CVEs roughly 50% faster per year than Go does.
  • Severity and rate are both in the data already. Neither shows up in a plain descending sort.

The number one entry has no critical vulnerabilities

MySQL 5.7 has missed 127 CVEs since October 2023. Zero of them are rated critical.

Go 1.15 has missed 122 — five fewer — and twelve of those are critical.

If you are triaging with a fixed budget and you work top-down, you spend it on the
MySQL upgrade first. The data in the same table says the Go upgrade is the one
carrying critical-severity exposure.

I am not claiming MySQL 5.7 is safe. 127 unpatched vulnerabilities is 127
unpatched vulnerabilities, and CVSS scores are a
severity model, not a measure of exploitability in your environment. The point is
narrower: the sort key is throwing away the severity column.

Volume also measures how long the corpse has been sitting there

The second problem is that CVE count rewards age.

Go 1.15 went EOL in August 2021. It has been unpatched for 1,820 days. MySQL 5.7
went EOL in October 2023, roughly 1,060 days ago. Go 1.15 had almost twice as long
to accumulate its 122.

Divide by time and the picture inverts:

Version Days EOL CVEs missed CVEs/year
MySQL 5.7 ~1,060 127 ~44
MySQL 8.1 1,020 103 ~37
Go 1.17 1,469 106 ~26
Go 1.16 1,609 111 ~25
Go 1.15 1,820 122 ~24

The Go branches cluster tightly around 24-26 CVEs per year. The MySQL branches sit
at 37-44. Same measurement, same source, and the two ecosystems separate cleanly.
Staying one more year on an EOL MySQL branch costs you roughly 50% more missed
fixes than staying one more year on an EOL Go branch.

That is a forecasting number, which is what you actually want when the question is
"can this upgrade wait another quarter." Raw CVE count is a backward-looking number
dressed up as a risk score.

Details for any single branch — the full timeline, the per-year CVE breakdown, and
the individual CVEs with their CVSS scores — are on the version pages, for example
MySQL 5.7.

Why the two ecosystems differ this much

Honestly: I do not know, and neither does the data.

Plausible contributors are that MySQL advisories arrive in Oracle's quarterly
Critical Patch Update batches under its
Lifetime Support Policy,
that Go's release policy — each major
version supported until two newer ones ship — produces a different branch cadence
entirely, and that NVD attribution practices differ by vendor. Any of those could
produce this gap without either project being less safe than the other.

What I will defend is the narrower claim: if you are ranking branches, rank them
within an ecosystem, or normalise by time, or both.
Comparing a raw MySQL count
against a raw Go count and calling the bigger one riskier is comparing two different
measurements.

Computing this yourself

The data is a free JSON API — no key, no signup, no rate limit, CORS open, rebuilt
daily. GET /api/v1/products/{name}.json returns every branch of a product:

const res = await fetch('https://end-of-life.org/api/v1/products/mysql.json');
const { result } = await res.json();

const dead = result.releases
  .filter(r => r.isEol && r.cvesMissedSinceEol)
  .map(r => {
    // eolFrom is either YYYY-MM-DD or YYYY-MM; Date.parse handles both
    const years = (Date.now() - Date.parse(r.eolFrom)) / (365.25 * 864e5);
    return {
      branch: r.name,
      missed: r.cvesMissedSinceEol,
      perYear: +(r.cvesMissedSinceEol / years).toFixed(1),
    };
  })
  .sort((a, b) => b.perYear - a.perYear);

console.table(dead);
Enter fullscreen mode Exit fullscreen mode

The fields worth knowing:

  • isEol / eolFrom — whether the branch is dead and when it died. There are also eoasFrom (active support ended) and eoesFrom (extended support ended); not every vendor defines all three, and a phase the vendor does not publish comes back null rather than a guess.
  • cvesMissedSinceEol — the count this whole post is about. It is null for products without CVE analysis, which matters (see below).
  • hasCveAnalysis on the product object — check this before you trust a null.

Dropping that into a CI job that reads your lockfiles and flags branches above a
per-year threshold is maybe an afternoon of work, and it gives you a number you can
put in front of whoever approves the upgrade. Full endpoint docs are at
end-of-life.org/api.

The limit worth stating

CVE analysis currently covers 13 products — Django, VMware ESXi, Go, Keycloak,
MySQL, Node.js, PHP, PostgreSQL, Python, Ruby on Rails, Ruby, Spring Boot, Apache
Tomcat. The directory tracks 462 products and 8,307 versions, but for the other 449
you get lifecycle dates only, and cvesMissedSinceEol is null.

So this method works today for the layer of your stack most likely to be a runtime
or a database, and not for everything else. A null there means "not analysed,"
not "no CVEs" — do not let a dashboard render it as zero.

What I would actually sort by

Three columns, in this order:

  1. Critical count. Twelve criticals beats 127 mediums for where the first upgrade window goes.
  2. CVEs per year. Forward-looking. Answers "what does one more quarter cost."
  3. Total missed. Useful context, terrible primary key.

The data for all three is sitting in the same response. It is only the sort that
needs fixing.


Lifecycle dates are read from vendor sources — for the two products above, Oracle's
Lifetime Support Policy
and MySQL release notes, and Go's
release history. Vulnerability data comes from the
NVD API. Every date and count in this post is reproducible from
those sources plus the JSON API; the full source list per product is on
data sources. This project is not endorsed or
certified by the NVD and is not affiliated with any vendor —
about · contact.

Top comments (0)