CVE Program Resilience: Building Beyond NVD Dependency
Problem
In April 2025, MITRE announced that its US government contract to operate the CVE program was expiring, raising the possibility that the CVE numbering authority (CNA) infrastructure would be defunded. The contract was renewed after significant industry pressure, but the near-shutdown exposed a critical dependency: the entire global vulnerability tracking ecosystem — from commercial scanners to SIEM rules to patch management tooling — is built on the assumption that NVD/CVE data is continuously available and enriched.
The CVE program near-shutdown was not the only problem. NIST’s NVD had already been experiencing an enrichment backlog since early 2024, with over 13,000 CVEs awaiting CVSS scoring and CPE mapping at peak. Vulnerability scanners that depend on NVD data were silently under-reporting findings. Security teams believed their environments were assessed, while unscored CVEs were not being matched to vulnerable software.
What the fragility looks like in practice:
- A critical CVE is published as a CVE ID with minimal description. NVD takes 30–90 days to enrich it with CVSS score and CPE mapping. Your scanner uses CPE matching. The CVE is not matched to any of your software for 90 days.
- Your SIEM rule “alert on CVSS Critical CVEs in your asset inventory” does not fire because the CVE is awaiting analysis.
- Your patch SLA tracking system shows this CVE as not applicable. It is.
- Meanwhile, OSV (the Open Source Vulnerabilities database, maintained by Google), GitHub’s Advisory Database, and the vendor’s own advisory all have the information — but your tooling is not configured to use them.
The alternative sources exist and are production-ready. The difference between organisations that were affected by the NVD backlog and those that were not is that the unaffected organisations had already built diversified CVE pipelines using OSV, GHSA, CISA KEV, and vendor feeds as primary sources. NVD was supplementary, not singular.
Target systems: any organisation with a vulnerability management programme; security teams responsible for CVE coverage SLAs; organisations that discovered during the NVD backlog that their scanner was missing CVEs.
Threat Model
Risk 1 — NVD outage leaves CVE tracking blind. NVD becomes unavailable for 48 hours (planned maintenance, unplanned outage, or funding disruption). All scanners that pull from NVD stop receiving new CVE data. New disclosures are not tracked. Organisations operating solely on NVD are blind to new vulnerabilities for the duration.
Risk 2 — NVD enrichment lag masks critical CVEs. A CVE with eventual CVSS 9.8 is published but takes 60 days to receive NVD enrichment. Your CVSS-filtered scanner does not surface it. Your patch SLA never triggers. The CVE enters the CISA KEV catalog while still unenriched in NVD — you have no automated process to catch KEV entries that are not yet scored.
Risk 3 — CPE mismatch causes scanner miss. NVD’s CPE mapping for a CVE incorrectly names the affected package (a known data quality issue). Your scanner’s CPE-based matching does not match the CVE to your software. OSV and GHSA use package-ecosystem matching (PyPI, npm, Debian) that does not depend on CPE — they would have caught it.
Configuration / Implementation
Step 1 — Map your current CVE pipeline dependencies
#!/bin/bash
# scripts/audit-cve-pipeline-dependencies.sh
# Identify which CVE data sources your tooling currently uses
echo "=== CVE Pipeline Dependency Audit ==="
# Vulnerability scanners and their data sources
echo ""
echo "--- Trivy ---"
trivy --version 2>/dev/null
trivy image --list-all-pkgs --format json some-image:latest 2>/dev/null | \
jq '.Results[0].Vulnerabilities[0]' 2>/dev/null | grep -i "datasource\|source"
# Trivy uses: GitHub Advisory DB, OSV, NVD
echo ""
echo "--- Grype ---"
grype --version 2>/dev/null
# Grype uses: NVD, GHSA, OSV, distribution advisories
echo ""
echo "--- OpenVAS/GVM data feeds ---"
greenbone-nvt-sync --version 2>/dev/null
# OpenVAS primarily uses NVD/CVSS
echo ""
echo "--- Checking for NVD API direct usage in custom scripts ---"
grep -r "nvd.nist.gov\|nvdcve" /usr/local/lib /opt /home 2>/dev/null | \
grep -v ".pyc\|node_modules" | head -20
echo ""
echo "=== Recommendation: If NVD is the only source, diversify ==="
Step 2 — OSV as primary vulnerability database
#!/usr/bin/env python3
# scripts/osv-primary-feed.py
# Use OSV as the primary vulnerability database, with NVD as supplement
import json
import urllib.request
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class OSVVulnerability:
osv_id: str
cve_aliases: list[str]
summary: str
severity_scores: list[dict]
affected_packages: list[dict]
published: str
modified: str
references: list[str] = field(default_factory=list)
def query_osv_by_package(package_name: str, ecosystem: str, version: str) -> list[OSVVulnerability]:
"""Query OSV for vulnerabilities affecting a specific package version."""
url = "https://api.osv.dev/v1/query"
payload = json.dumps({
"version": version,
"package": {"name": package_name, "ecosystem": ecosystem}
}).encode()
req = urllib.request.Request(url, data=payload,
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
except Exception as e:
print(f"OSV query failed: {e}")
return []
result = []
for vuln in data.get("vulns", []):
result.append(OSVVulnerability(
osv_id=vuln.get("id", ""),
cve_aliases=[a for a in vuln.get("aliases", []) if a.startswith("CVE-")],
summary=vuln.get("summary", ""),
severity_scores=vuln.get("severity", []),
affected_packages=vuln.get("affected", []),
published=vuln.get("published", ""),
modified=vuln.get("modified", ""),
references=[r.get("url", "") for r in vuln.get("references", [])]
))
return result
def get_severity_from_osv(vuln: OSVVulnerability) -> tuple[float, str]:
"""Extract the highest severity score from an OSV entry."""
for score_entry in vuln.severity_scores:
if score_entry.get("type") == "CVSS_V3":
vector = score_entry.get("score", "")
# Extract base score from CVSS vector string
# CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
# Calculate approximate base score from vector components
# (Full CVSS calculator logic would go here)
# For now, use the score field if present
if "score" in score_entry:
try:
score = float(score_entry.get("baseScore", 0))
if score >= 9.0:
return score, "CRITICAL"
elif score >= 7.0:
return score, "HIGH"
elif score >= 4.0:
return score, "MEDIUM"
else:
return score, "LOW"
except (ValueError, TypeError):
pass
return 0.0, "UNKNOWN"
def bulk_query_osv(packages: list[dict]) -> list[dict]:
"""
Bulk query OSV for multiple packages.
packages: list of {"name": ..., "ecosystem": ..., "version": ...}
"""
all_findings = []
for pkg in packages:
vulns = query_osv_by_package(
pkg["name"], pkg["ecosystem"], pkg["version"]
)
for vuln in vulns:
score, severity = get_severity_from_osv(vuln)
all_findings.append({
"package": pkg["name"],
"version": pkg["version"],
"ecosystem": pkg["ecosystem"],
"osv_id": vuln.osv_id,
"cve_ids": vuln.cve_aliases,
"severity": severity,
"score": score,
"summary": vuln.summary[:200],
"has_fix": any(
a.get("ranges", [{}])[0].get("events", [{}])[-1].get("fixed")
for a in vuln.affected_packages
if a.get("ranges")
)
})
return sorted(all_findings, key=lambda x: x["score"], reverse=True)
Step 3 — GitHub Advisory Database integration
#!/bin/bash
# scripts/ghsa-monitor.sh
# Monitor GitHub Security Advisories as an NVD-independent CVE source
GITHUB_TOKEN="${GITHUB_TOKEN:?Set GITHUB_TOKEN}"
STATE_FILE="/var/lib/ghsa-monitor/last-seen.json"
mkdir -p "$(dirname "$STATE_FILE")"
# Fetch recent advisories via GraphQL
ADVISORIES=$(curl -s -X POST "https://api.github.com/graphql" \
-H "Authorization: bearer $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query {
securityAdvisories(first: 50, orderBy: {field: PUBLISHED_AT, direction: DESC}) {
nodes {
ghsaId
publishedAt
updatedAt
severity
summary
cvss { score vectorString }
identifiers { type value }
references { url }
vulnerabilities(first: 10) {
nodes {
package { ecosystem name }
vulnerableVersionRange
firstPatchedVersion { identifier }
}
}
}
}
}"
}')
# Process new advisories
echo "$ADVISORIES" | python3 - << 'PYEOF'
import json, sys, os
from datetime import datetime, timezone, timedelta
data = json.loads(sys.stdin.read())
advisories = data.get("data", {}).get("securityAdvisories", {}).get("nodes", [])
# Load last seen state
state_file = os.environ.get("STATE_FILE", "/tmp/ghsa-state.json")
try:
with open(state_file) as f:
last_seen = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
last_seen = {}
cutoff = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
new_count = 0
for adv in advisories:
ghsa_id = adv["ghsaId"]
published = adv["publishedAt"]
if published > cutoff and ghsa_id not in last_seen:
cve_ids = [id["value"] for id in adv["identifiers"] if id["type"] == "CVE"]
severity = adv["severity"]
print(f"NEW GHSA: {ghsa_id} [{severity}]")
if cve_ids:
print(f" CVEs: {', '.join(cve_ids)}")
print(f" Summary: {adv['summary'][:100]}")
for vuln in adv.get("vulnerabilities", {}).get("nodes", []):
pkg = vuln["package"]
fixed = vuln.get("firstPatchedVersion", {})
print(f" Package: {pkg['ecosystem']}/{pkg['name']}")
if fixed:
print(f" Fixed in: {fixed.get('identifier', 'N/A')}")
last_seen[ghsa_id] = published
new_count += 1
# Save updated state
with open(state_file, "w") as f:
json.dump(last_seen, f)
print(f"\nTotal new GHSA advisories in last 24h: {new_count}")
PYEOF
Step 4 — Vendor advisory feeds for critical components
# vendor-advisory-feeds.yaml
# Direct vendor feeds that bypass NVD/CVE infrastructure entirely
critical_components:
- name: Linux Kernel
advisory_url: "https://www.kernel.org/doc/html/latest/admin-guide/security-bugs.html"
rss_feed: "https://www.kernel.org/feeds/kdist.xml"
mailing_list: "linux-kernel-announce@vger.kernel.org"
- name: OpenSSL
advisory_url: "https://www.openssl.org/news/vulnerabilities.html"
rss_feed: "https://www.openssl.org/news/vulnerabilities.xml"
github_releases: "https://github.com/openssl/openssl/releases"
- name: NGINX
advisory_url: "https://nginx.org/en/security_advisories.html"
rss_feed: "https://nginx.org/en/security_advisories.rss"
- name: Kubernetes
advisory_url: "https://github.com/kubernetes/kubernetes/issues?label=area%2Fsecurity"
github_security: "https://github.com/advisories?query=kubernetes"
slack: "kubernetes-security-announce@googlegroups.com"
- name: containerd
advisory_url: "https://github.com/containerd/containerd/security/advisories"
- name: Docker
advisory_url: "https://docs.docker.com/security/"
- name: Go
advisory_url: "https://go.dev/doc/security/vuln/"
osv_feed: "https://vuln.go.dev/"
- name: Python
advisory_url: "https://python-security.readthedocs.io/"
osv_ecosystem: "PyPI"
monitoring_config:
check_interval: "6h"
alert_on_severity: ["critical", "high"]
store_state: "/var/lib/vendor-advisory-monitor/state.json"
Step 5 — Build a CVE pipeline resilience dashboard
#!/bin/bash
# /etc/node_exporter/textfile_collector/cve_source_health.sh
# Monitor the health of all CVE data sources
check_source() {
local name="$1"
local url="$2"
local timeout="${3:-10}"
if curl -s --max-time "$timeout" -o /dev/null -w "%{http_code}" "$url" | grep -q "^2"; then
echo 1
else
echo 0
fi
}
NVD_HEALTH=$(check_source "NVD" "https://services.nvd.nist.gov/rest/json/cves/2.0?resultsPerPage=1")
OSV_HEALTH=$(check_source "OSV" "https://api.osv.dev/v1/query" 10)
GHSA_HEALTH=$(check_source "GHSA" "https://api.github.com/advisories?per_page=1" 10)
KEV_HEALTH=$(check_source "KEV" "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json")
# Count unenriched CVEs in NVD (requires NVD API key for efficient querying)
NVD_BACKLOG=$(curl -s --max-time 15 \
"https://services.nvd.nist.gov/rest/json/cves/2.0?cvssV3Severity=CRITICAL&noRejected" \
-H "apiKey: ${NVD_API_KEY:-}" 2>/dev/null | \
jq '[.vulnerabilities[] | select(.cve.vulnStatus == "Awaiting Analysis")] | length' \
2>/dev/null || echo -1)
cat << EOF
# HELP cve_source_available Whether each CVE data source is reachable (1=yes, 0=no)
# TYPE cve_source_available gauge
cve_source_available{source="nvd"} $NVD_HEALTH
cve_source_available{source="osv"} $OSV_HEALTH
cve_source_available{source="ghsa"} $GHSA_HEALTH
cve_source_available{source="kev"} $KEV_HEALTH
# HELP nvd_critical_awaiting_analysis_total CVEs awaiting NVD enrichment
# TYPE nvd_critical_awaiting_analysis_total gauge
nvd_critical_awaiting_analysis_total $NVD_BACKLOG
EOF
Expected Behaviour
| Scenario | NVD-only pipeline | Resilient multi-source pipeline |
|---|---|---|
| NVD enrichment backlog: CVE unenriched 60 days | CVE not scored; scanner misses it | OSV and GHSA surface it with package-level matching |
| NVD API outage for 48 hours | No new CVE data; scanner shows stale results | OSV and GHSA continue operating; KEV feed independent of NVD |
| MITRE CVE program disruption | CVE IDs not assigned; tracking impossible | OSV IDs (GHSA, GO-*, etc.) provide alternative identifiers; tracking continues |
| CPE mismatch in NVD causes scanner miss | Vulnerable package not matched | OSV ecosystem/package matching catches it regardless of CPE |
| New CVE added to KEV before NVD enrichment | KEV not correlated to unscored CVE | KEV integration runs independently; assets checked against KEV by CVE ID directly |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| Multi-source aggregation | Resilience; earlier detection | Duplicate findings; inconsistent severity scores | Deduplicate by CVE ID; use highest score across sources |
| OSV as primary source | Fast enrichment; package-level matching | Not all software is in OSV (closed-source, hardware) | Use vendor advisories for software not in OSV |
| GHSA as supplement | Often earliest OSS advisory | Requires GitHub token; rate limits | Use a dedicated bot token; cache responses aggressively |
| Vendor feeds | Authoritative for that vendor | Manual subscription management; inconsistent formats | Use an RSS aggregator or dedicated feed monitoring tool |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| All sources become unavailable simultaneously | No CVE data; false sense of security | Alert when source health drops to zero across all sources | Maintain a cached copy of CVE data (7-day rolling window) for offline operation |
| OSV ecosystem mismatch | CVE not found because wrong ecosystem specified | Findings count drops unexpectedly | Test with known-vulnerable package/version combinations |
| Vendor feed URL changes | Feed monitoring stops receiving advisories | Feed monitoring alert on silence > 48h | Monitor feed health; subscribe to vendor announcement lists as backup |
| Duplicate CVEs from multiple sources create ticket noise | Multiple tickets for same CVE | Ticket count spikes; team complains of duplicates | Implement CVE ID deduplication in the ticket creation pipeline |
Related Articles
- NVD Enrichment Lag Scanner Compensation — tactical response to NVD lag using alternative data sources
- CISA KEV Alerting Integration — integrating the KEV catalog as an independent CVE signal
- EPSS-Driven Patch Prioritization — prioritization using EPSS, which is independent of NVD
- Vulnerability Management Program — the broader programme that CVE source resilience is part of
- Cyber Insurance Technical Requirements — insurers require demonstrated CVE tracking capability; multi-source approach satisfies this