CVE Program Resilience: Building Beyond NVD Dependency

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