Software Composition Analysis (SCA) tools — automated scanners that flag vulnerable open-source packages in your codebase — have a structural gap most engineering teams don’t account for: they cannot alert on end-of-life (EOL) packages whose vulnerabilities are no longer tracked in the CVE feeds those tools consume. Sonatype’s 2025 open source risk research identified 167,286 false negatives — exploitable components that produced zero scanner alerts — with EOL versions omitted from advisories as a primary cause. HeroDevs has catalogued 5.4 million EOL package versions across npm, PyPI, Maven, NuGet, RubyGems, and crates.io. This guide explains why the gap exists, quantifies its real-world scale, and walks through a layered CLI audit workflow — pip-audit, osv-scanner, xeol, and HeroDevs EOLDS — to surface what your existing pipeline is missing.
Why SCA Tools Miss EOL Dependencies
SCA tools work by parsing your dependency manifests (requirements.txt, package.json, go.mod, pom.xml) and cross-referencing each pinned version against one or more advisory databases:
- Snyk aggregates NVD (National Vulnerability Database — NIST’s central repository of CVE data), the GitHub Advisory Database, and its own proprietary research. Snyk detects vulnerabilities approximately 47 days faster than competing databases on average.
- Dependabot relies primarily on the GitHub Advisory Database, which contains over 20,000 manually reviewed advisories. GitHub vets each entry before triggering an alert — this reduces false positives but also slows coverage for edge-case version ranges.
- osv-scanner (Google) queries osv.dev, which normalises advisories from 30+ sources including PyPA, RustSec, Go Vulnerability Database, and GitHub Advisory Database.
- OWASP Dependency-Check cross-references NVD using CPE (Common Platform Enumeration — a standardised naming scheme for software products and versions) matching.
The structural assumption shared by all of them: a CVE (Common Vulnerabilities and Exposures entry — the industry-standard identifier for a discrete, documented vulnerability) exists, has been scored by NIST, and has been assigned to a specific affected version range. The tool matches your installed version against that range and fires an alert on a match.
That assumption fails the moment a package reaches end-of-life.
When a library hits its EOL date — the point at which the maintainer stops issuing security patches — the following sequence plays out:
- The maintainer stops backporting security fixes to that version branch.
- Security researchers stop filing CVEs against it, because there is no patch to reference and no vendor advisory to anchor the CVE entry.
- MITRE — the organisation that assigns CVE IDs — and NVD stop creating entries for those version ranges, because vendor security advisories are the source document for CVE scope, and the vendor no longer issues them.
- Advisory databases (GitHub Advisory Database, osv.dev) reflect those scoped version ranges. If a flaw exists in a 5.x branch but the vendor advisory only covers the still-maintained 6.x branch, users of 5.x see a clean scan.
The result: “no alert” does not mean “not vulnerable” — it means the vulnerability was never formally documented for that version. HeroDevs draws the distinction sharply: SCA tools answer “what is vulnerable right now?” but cannot answer “what will never be fixed?” A CVE in maintained software is a problem with a timeline. A CVE in EOL software is a permanent exposure.
The Scale of the SCA EOL CVE Blind Spot
The data makes this more than a theoretical concern:
| Ecosystem | Approximate % of package versions that are EOL | |—|—| | npm (JavaScript) | ~25% | | NuGet (.NET) | ~18% | | Cargo (Rust) | ~13% | | PyPI (Python) | ~11% | | Maven Central (Java) | ~10% |
Industry surveys on what organisations actually run in production:
- 26% of organisations still run CentOS 7, which reached EOL in June 2024.
- 20%+ of large enterprises have AngularJS (EOL December 2021) deployed in active production codebases.
- 73% of healthcare providers operate equipment dependent on legacy operating systems.
- Five major frameworks were scheduled to reach EOL simultaneously in Q2 2026, compounding exposure for teams already carrying technical debt.
Global CVE volume doubled in five years while the number of unscored CVEs — those without a CVSS (Common Vulnerability Scoring System) rating, meaning most SCA tools skip them by default — increased 37x.
A concrete example: Spring Security CVE-2026-22732 lists affected versions as 5.7.x through 7.0.x. Spring Security 6.2.x, which reached EOL in December 2025, is not listed in the advisory. Any application running Spring Boot 3.2 — which ships Spring Security 6.2.x as its default security dependency — passes Snyk, Dependabot, and osv-scanner scans without a single alert. The scanner sees green. The attack surface is real and permanently unpatched.
Step-by-Step SCA EOL Blind Spot Detection Workflow
The following four-step workflow layers standard CVE scanning with dedicated EOL detection. It is designed for AppSec engineers and DevSecOps practitioners who already have Snyk or Dependabot running and need to close the gap without replacing their existing tooling.
Step 1 — pip-audit: CVE Coverage for Python with OSV Cross-Check
pip-audit is an open-source Python dependency auditor developed by Trail of Bits with Google support. It queries the PyPA Advisory Database and OSV.dev for known CVEs in installed packages or requirements files.
Install:
pip install pip-audit
Scan a requirements file against both the PyPI and OSV vulnerability services — the two databases sometimes list different affected version ranges for the same CVE, and running both catches discrepancies:
pip-audit -r requirements.txt -s osv -s pypi --output-format json -o audit-pip.json
Scan the live virtual environment (what is actually installed, which often diverges from what the requirements file declares on long-running projects):
pip-audit --local --output-format json -o audit-pip-env.json
The --output-format json flag produces machine-parseable output suitable for CI pipeline ingestion. The --fix flag automatically upgrades vulnerable packages to the lowest non-vulnerable version — useful in development environments, but review the upgrades before applying in production.
What pip-audit does not do: flag packages that have reached EOL without a corresponding CVE record. A library that went EOL two years ago and has accumulated untracked vulnerabilities appears clean in pip-audit output. That requires Step 3.
Step 2 — osv-scanner: Multi-Ecosystem CVE Coverage Including Transitive Dependencies
osv-scanner extends vulnerability coverage beyond Python to JavaScript, Go, Rust, Java, Ruby, and eight additional ecosystems. Its key advantage over pip-audit for multi-language projects: it consumes lockfiles directly — package-lock.json, poetry.lock, go.sum, Cargo.lock — rather than just top-level manifests. Lockfiles expose transitive dependencies (packages pulled in by your direct dependencies’ own dependencies), which is where the majority of supply-chain risk accumulates.
Install via the pre-compiled binary from the GitHub releases page, or via Go 1.21+:
go install github.com/google/osv-scanner/cmd/osv-scanner@latest
Scan a project directory recursively, picking up all lockfiles:
osv-scanner --recursive --format json . > audit-osv.json
Scan a specific lockfile:
# Node.js
osv-scanner --lockfile package-lock.json --format json
# Python (Poetry)
osv-scanner --lockfile poetry.lock --format json
# Go
osv-scanner --lockfile go.sum --format json
Scan an SBOM (Software Bill of Materials — a structured, machine-readable inventory of every component in your software, required under a growing number of compliance frameworks):
osv-scanner --sbom sbom.cdx.json --format json
For npm and Maven projects, osv-scanner v2 provides guided remediation: it analyses the full dependency graph and recommends the minimum set of version bumps ranked by severity and dependency depth:
osv-scanner fix --lockfile package-lock.json
Limitation: like pip-audit, osv-scanner only surfaces packages with CVE records in osv.dev. An EOL package with undocumented vulnerabilities produces no output.
Step 3 — xeol: Dedicated EOL Lifecycle Detection
xeol is the critical missing layer. Instead of querying vulnerability databases, it queries EOL lifecycle databases — tracking when each package version’s vendor support closes, independent of whether a CVE has ever been filed. It supports container images, filesystems, SBOMs, and package manifests across all major ecosystems.
Install via the provided script:
curl -sSfL https://raw.githubusercontent.com/xeol-io/xeol/main/install.sh | sh -s -- -b /usr/local/bin
Scan a project directory:
xeol dir:/path/to/project --output json > audit-xeol.json
Scan a Docker container image — essential for catching EOL OS base layers and language runtimes baked into images that standard SCA tools skip entirely:
xeol docker:your-image:latest --output json > audit-xeol.json
Scan an SBOM directly:
xeol sbom:sbom.cdx.json --output json > audit-xeol.json
xeol output includes the EOL date per component. A package that went EOL three years ago carries a fundamentally different risk profile than one that expired last month — the older the EOL, the more undiscovered vulnerabilities have accumulated with no chance of a vendor patch. Review output sorted by EOL age:
jq '.matches | sort_by(.eolDate) | .[] | "\(.artifact.name) \(.artifact.version) — EOL: \(.eolDate)"' audit-xeol.json
To fail a CI build immediately on any EOL detection:
xeol dir:. --fail-on-eol-found
Step 4 — HeroDevs EOLDS: JavaScript Framework Depth Coverage
HeroDevs EOLDS (End-of-Life Dependency Scanner) targets JavaScript and TypeScript framework ecosystems — Angular, React, Vue, Node.js, and their transitive dependency graphs — with more granular version-range intelligence than general-purpose EOL databases. EOLDS is free and complements xeol for teams with significant frontend surface area.
npx @herodevs/eolds scan
For a specific project path with JSON output:
npx @herodevs/eolds scan --path ./frontend --format json > audit-herodevs.json
EOLDS reports which dependencies are EOL, when they expired, and whether a HeroDevs NES (Never-Ending Support) product — a commercially maintained, security-patched drop-in fork — is available as a replacement.
Automating the Audit in GitHub Actions
The following workflow runs all four tools on every pull request and blocks merges that introduce new EOL components:
name: Dependency Security Audit
on:
pull_request:
branches: [main, develop]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: pip-audit (Python CVE scan)
run: |
pip install pip-audit
pip-audit -r requirements.txt -s osv -s pypi \
--output-format json -o audit-pip.json || true
- name: osv-scanner (multi-ecosystem CVE scan)
uses: google/osv-scanner-action@v2
with:
scan-args: |-
--recursive
--format=json
./
- name: xeol (EOL detection)
run: |
curl -sSfL \
https://raw.githubusercontent.com/xeol-io/xeol/main/install.sh \
| sh -s -- -b /usr/local/bin
xeol dir:. --fail-on-eol-found --output json > audit-xeol.json
- name: HeroDevs EOLDS (JavaScript framework EOL depth)
run: npx @herodevs/eolds scan --format json > audit-herodevs.json
- name: Upload audit artefacts
if: always()
uses: actions/upload-artifact@v4
with:
name: dependency-audit
path: audit-*.json
Threshold guidance for brownfield codebases: blocking builds on any EOL component is aggressive for projects carrying years of accumulated technical debt. A practical starting point is to remove --fail-on-eol-found from xeol for the first two-week sprint, generate the artefact reports, and baseline the full EOL inventory. Enable the gate after addressing the highest-severity items first — prioritise components that are both EOL and have at least one tracked CVE, since those represent confirmed risk rather than potential risk.
Remediation When Upgrade Is Not Immediate
Not every EOL dependency can be upgraded on the next sprint. Transitive dependencies, framework compatibility constraints, and enterprise procurement cycles all create lag. Options for each scenario:
| Situation | Remediation path | |—|—| | EOL Angular ≤1.x, React ≤16, Vue ≤2.x | HeroDevs NES — commercially patched, drop-in fork | | EOL Python 2.x library with no maintained fork | Migration sprint; or vendor-specific backport if available | | EOL OS layer in container base image | Rebuild on a maintained base (e.g. ubuntu:24.04 instead of ubuntu:18.04) | | Upgrade blocked by procurement or change-freeze | Add WAF (Web Application Firewall) rules targeting known exploitation patterns for that version; document compensating controls in your SBOM |
For compliance programs requiring a formal SBOM, tools like Syft generate CycloneDX or SPDX-format SBOMs that can be annotated with EOL metadata from xeol’s JSON output — giving auditors documented visibility into permanently unpatched risk, which is considerably more defensible than having no record of the exposure at all.
Conclusion
A green Snyk or Dependabot scan means your CVE-flagged components are clean. It says nothing about the EOL layers underneath. With 5.4 million EOL package versions across major ecosystems and a CVE feed that structurally stops covering packages once vendor support ends, the gap between “not flagged” and “not vulnerable” is where long-dwell breaches happen. The four-tool workflow above — pip-audit for Python CVE coverage, osv-scanner for multi-ecosystem lockfile depth, xeol for lifecycle-based EOL detection, and HeroDevs EOLDS for JavaScript framework granularity — closes that gap without replacing your existing pipeline. Add the GitHub Actions workflow and you catch EOL introductions at the PR boundary, before they compound into a multi-year AngularJS problem nobody wants to own.
For any query contact us at contact@cipherssecurity.com

