Safeguard
Compliance•

CISA's CVE Quality Era Framework, Measured: What 81,803 CVE Records From 2026 Actually Contain

CISA's CVE Quality Era framework sets no record criteria yet. We checked 81,803 2026 CVE records: 22% have score, CWE, versions and a fix link.

Safeguard Security Team
Security Research
Updated 13 min read

CISA wants the CVE Program to move from what it calls a Growth Era to a Quality Era. Its framework whitepaper, published on 22 September, names the success metric that matters most to anyone consuming CVE data: the percentage of CVE records that meet defined quality criteria. It doesn't define those criteria yet. So we measured the basics ourselves across every CVE record published in 2026.

The short version: records are fuller than they were, but most still aren't complete in the structured fields that scanners read. This explainer is for vulnerability management and product security teams that rely on CVE data, and for CNAs that publish it. It covers what the framework says and doesn't say, what 81,803 records from 2026 contain, the gap between a record a person can read and one a tool can use, and a script that checks the records in your own backlog.

The short answer

CISA's "CVE Program: Establishing a Quality Era Framework" (22 September 2026) sets four quality dimensions (program governance, ecosystem participation, data infrastructure and CVE record content) and lists candidate success metrics, but it doesn't set record requirements. Those are promised in later publications. In the 2026 records we checked, the assigning CNA supplied a CVSS score in 80%, a CWE in 76% and a structured upper version bound in 61%. Only 22% had all four of a score, a CWE, an affected version and a tagged patch or advisory link, up from 11% in 2024. CISA's own enrichment raises CVSS coverage to 95%. Until criteria exist, check the fields your process depends on, and for open-source CVEs take version ranges from GitHub's advisory database or OSV rather than the CVE record.

What the framework says

The whitepaper is four pages. It builds on CISA's earlier strategy document, "Strategic Focus: CVE Quality for a Cyber Secure Future", which set six lines of effort, and maps those lines onto four dimensions:

DimensionFocusExample success metrics in the paper
Program governanceTransparent stewardship and decision-makingTimeliness of decisions, conflicts of interest identified and resolved
Ecosystem participationBroad, active, global CNA and community involvementNumber and diversity of active CNAs, working group participation
Data infrastructureAPIs, schemas, validation libraries and cve.orgUptime, API throughput, validation error frequency
CVE record contentComplete, accurate, timely, actionable recordsShare of records meeting defined quality criteria, rate of post-publication corrections

It sets out the pressure clearly. CISA says more than 67,000 new CVEs had been published in 2026 by 18 September, cites a projection of 96,000 for the year, and notes that NIST's NVD reported a 263% increase in CVE submissions between 2020 and 2025. It names AI-enabled discovery as one of the forces behind the rise.

What it doesn't do is just as important. It sets no required fields, no deadlines and no enforcement for CNAs. The next step it announces is a blog series on cve.org about infrastructure and data modernization, and "future CVE Quality Era publications" with detail. For now it's a direction, not a rulebook.

What 2026 CVE records contain

We read every record in the CVE Program's bulk export (the CVEProject/cvelistV5 release of 17 October) and counted records first published in each year. Our 2026 count, 81,803, is higher than CISA's September figure because it runs a month later and includes records with older CVE IDs published this year:

#!/usr/bin/env python3
"""Measure what 2026 CVE records actually contain, from the CVE List bulk export.

Reads cves.zip from a CVEProject/cvelistV5 release and checks, for each record
published in the given year, whether the CNA supplied a CVSS score, a CWE, a
machine-readable affected version range, and a patch or advisory reference, and
whether CISA's ADP container (Vulnrichment) added data.
Usage: cve_record_quality.py cves.zip 2026
"""
import collections, json, sys, zipfile

def has_range(affected):
    for a in affected:
        for v in a.get("versions", []):
            if v.get("status") == "affected" and (v.get("lessThan") or v.get("lessThanOrEqual")
                                                  or str(v.get("version", "")).lower() not in ("", "n/a", "0", "*", "-", "unspecified", "all", "unknown")):
                return True
    return False

def main(path, year):
    z = zipfile.ZipFile(path)
    c = collections.Counter()
    cnas = collections.Counter()
    per = collections.defaultdict(list)
    for n in z.namelist():
        if not n.endswith(".json") or "/delta" in n:
            continue
        d = json.loads(z.read(n))
        meta = d.get("cveMetadata", {})
        if not meta.get("datePublished", "").startswith(year):
            continue
        if meta.get("state") == "REJECTED":
            c["rejected"] += 1
            continue
        cna = d.get("containers", {}).get("cna", {})
        adp = d.get("containers", {}).get("adp", [])
        c["published"] += 1
        cnas[meta.get("assignerShortName", "?")] += 1
        cvss = any(k.startswith("cvss") for m in cna.get("metrics", []) for k in m)
        cwe = any(x.get("cweId") for p in cna.get("problemTypes", []) for x in p.get("descriptions", []))
        rng = has_range(cna.get("affected", []))
        tags = {t for r in cna.get("references", []) for t in r.get("tags", [])}
        fixref = bool(tags & {"patch", "vendor-advisory", "release-notes"})
        cisa = [a for a in adp if a.get("providerMetadata", {}).get("shortName") == "CISA-ADP"]
        cisa_cvss = any(k.startswith("cvss") for a in cisa for m in a.get("metrics", []) for k in m)
        cisa_cwe = any(x.get("cweId") for a in cisa for p in a.get("problemTypes", []) for x in p.get("descriptions", []))
        vers = [v for a in cna.get("affected", []) for v in a.get("versions", []) if v.get("status") == "affected"]
        lt = any(v.get("lessThan") or v.get("lessThanOrEqual") for v in vers)
        c["cna_lessthan"] += lt
        c["range_as_text"] += (not lt) and any("<" in str(v.get("version", "")) for v in vers)
        c["legacy_tags_only"] += (not fixref) and any(t.startswith("x_refsource") for t in tags)
        per[meta.get("assignerShortName", "?")].append(cvss and cwe and rng and fixref)
        c["cna_cvss"] += cvss; c["cna_cwe"] += cwe; c["cna_range"] += rng; c["cna_fixref"] += fixref
        c["all_four"] += cvss and cwe and rng and fixref
        c["cisa_adp"] += bool(cisa)
        c["cvss_any"] += cvss or cisa_cvss; c["cwe_any"] += cwe or cisa_cwe
    p = c["published"]
    pct = lambda k: f"{c[k]:>6}  {100 * c[k] / p:5.1f}%"
    print(f"{year} CVE records published: {p} (plus {c['rejected']} rejected), from {len(cnas)} CNAs")
    print("CNA supplied a CVSS score        ", pct("cna_cvss"))
    print("CNA supplied a CWE               ", pct("cna_cwe"))
    print("CNA supplied an affected range   ", pct("cna_range"))
    print("CNA tagged a patch/advisory link ", pct("cna_fixref"))
    print("Structured upper bound (lessThan*)", pct("cna_lessthan"))
    print("Range only as text, e.g. '< 2.8.0'", pct("range_as_text"))
    print("Only legacy x_refsource link tags", pct("legacy_tags_only"))
    print("CNA supplied all four            ", pct("all_four"))
    print("CISA ADP container present       ", pct("cisa_adp"))
    print("CVSS from CNA or CISA ADP        ", pct("cvss_any"))
    print("CWE from CNA or CISA ADP         ", pct("cwe_any"))
    big = {k: v for k, v in per.items() if len(v) >= 50}
    share = lambda lo, hi: sum(1 for v in big.values() if lo <= sum(v) / len(v) < hi)
    print(f"CNAs with 50+ records: {len(big)}; all four in >=90% of records: {share(0.9, 1.01)}, "
          f"in under 10%: {share(0, 0.1)}")
    top = cnas.most_common(10)
    print(f"top 10 CNAs by volume issued {sum(v for _, v in top)} records ({100 * sum(v for _, v in top) / p:.0f}%)")

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "2026")
$ python3 cve_record_quality.py cves.zip 2026
2026 CVE records published: 81803 (plus 293 rejected), from 392 CNAs
CNA supplied a CVSS score          65272   79.8%
CNA supplied a CWE                 62267   76.1%
CNA supplied an affected range     76156   93.1%
CNA tagged a patch/advisory link   24082   29.4%
Structured upper bound (lessThan*)  49576   60.6%
Range only as text, e.g. '< 2.8.0'  11764   14.4%
Only legacy x_refsource link tags  12369   15.1%
CNA supplied all four              18272   22.3%
CISA ADP container present         75427   92.2%
CVSS from CNA or CISA ADP          77383   94.6%
CWE from CNA or CISA ADP           73786   90.2%
CNAs with 50+ records: 107; all four in >=90% of records: 18, in under 10%: 58
top 10 CNAs by volume issued 55716 records (68%)

The same script on earlier years shows the trend:

CNA-supplied field202420252026
CVSS score70.0%76.0%79.8%
CWE64.2%72.2%76.1%
Affected version83.7%88.7%93.1%
Structured upper bound (lessThan or lessThanOrEqual)56.1%59.0%60.6%
Patch, vendor advisory or release notes link, tagged as such13.3%12.2%29.4%
All four of score, CWE, version and tagged link10.7%10.4%22.3%

Two things stand out. Every field is improving. And the share of records with everything a scanner or triage process needs is still under a quarter. CISA's Authorized Data Publisher container, also called Vulnrichment, fills much of the gap for scores and weaknesses: with it, 94.6% of 2026 records have a CVSS score and 90.2% have a CWE. It mostly adds scores, weaknesses and SSVC decision points, not fix links.

Quality is also concentrated. Of the 107 CNAs that published at least 50 records in 2026, 18 supplied all four fields in at least 90% of their records, and 58 did so in fewer than 10%.

Readable is not the same as machine-readable

The biggest single gap in the 2026 data is about format, not missing facts. In 11,764 records, the affected range appears only as text inside the version field, such as ">= 1.0.0, < 2.8.0", with no structured lessThan value. 98.5% of those come from one CNA, GitHub, which published 11,741 records this year. Many of the same records tag their links with the legacy x_refsource_CONFIRM and x_refsource_MISC labels rather than patch or vendor-advisory.

A person reading the record sees the fixed version. A tool that reads the CVE 5 schema's range fields doesn't. To be fair to GitHub, its own advisory database and the OSV copy of it carry fully structured ranges. The CVE record is the lossy copy. In practice that means for open-source packages, the CVE record isn't the best source of version data, and tools that rely on it alone will miss or misjudge fixes for a large share of this year's CVEs.

This is exactly the kind of thing a "defined quality criteria" metric should catch. A record can be complete in the human sense and still fail the tools that consume it.

Check the records in your backlog

This script takes CVE IDs and reports which of the four fields the CNA left out, plus CISA's SSVC exploitation value where its enrichment has one. It reads from the CVE Services API and uses only the Python standard library:

#!/usr/bin/env python3
"""Check what the CVE records in your backlog actually tell you.

For each CVE ID, fetches the record from the CVE Services API and reports
whether the CNA gave a CVSS score, a CWE, an affected range with a fix
boundary, and a patch or advisory link, plus CISA's SSVC exploitation value if
its ADP container has one. Standard library only.
Usage: cve_record_check.py CVE-ID [CVE-ID...]   (or IDs on stdin, one per line)
"""
import json, sys, urllib.request

def fetch(cve):
    req = urllib.request.Request(f"https://cveawg.mitre.org/api/cve/{cve}", headers={"User-Agent": "cve-record-check"})
    return json.load(urllib.request.urlopen(req, timeout=30))

def check(cve):
    d = fetch(cve)
    cna = d["containers"]["cna"]
    adp = [a for a in d["containers"].get("adp", []) if a.get("providerMetadata", {}).get("shortName") == "CISA-ADP"]
    versions = [v for a in cna.get("affected", []) for v in a.get("versions", []) if v.get("status") == "affected"]
    tags = {t for r in cna.get("references", []) for t in r.get("tags", [])}
    ssvc = next((o.get("Exploitation") for a in adp for m in a.get("metrics", [])
                 for o in m.get("other", {}).get("content", {}).get("options", []) if "Exploitation" in o), None)
    missing = [name for name, ok in [
        ("CVSS", any(k.startswith("cvss") for m in cna.get("metrics", []) for k in m)),
        ("CWE", any(x.get("cweId") for p in cna.get("problemTypes", []) for x in p.get("descriptions", []))),
        ("fix boundary", any(v.get("lessThan") or v.get("lessThanOrEqual") for v in versions)),
        ("patch/advisory tag", bool(tags & {"patch", "vendor-advisory", "release-notes"})),
    ] if not ok]
    who = d["cveMetadata"].get("assignerShortName")
    return f"{cve:17} CNA {who:12} missing: {', '.join(missing) or 'nothing'} | CISA SSVC exploitation: {ssvc or 'none'}"

if __name__ == "__main__":
    ids = sys.argv[1:] or [l.strip() for l in sys.stdin if l.strip()]
    for i in ids:
        print(check(i))

On eight CVEs we've written about this month:

$ python3 cve_record_check.py CVE-2026-105192 CVE-2026-59180 CVE-2026-103030 CVE-2025-61624 \
    CVE-2026-96627 CVE-2026-94603 CVE-2026-102764 CVE-2026-0322
CVE-2026-105192   CNA JFROG        missing: patch/advisory tag | CISA SSVC exploitation: none
CVE-2026-59180    CNA GitHub_M     missing: fix boundary, patch/advisory tag | CISA SSVC exploitation: poc
CVE-2026-103030   CNA GitHub_M     missing: fix boundary, patch/advisory tag | CISA SSVC exploitation: poc
CVE-2025-61624    CNA fortinet     missing: patch/advisory tag | CISA SSVC exploitation: active
CVE-2026-96627    CNA GitHub_M     missing: fix boundary, patch/advisory tag | CISA SSVC exploitation: none
CVE-2026-94603    CNA GitHub_M     missing: fix boundary, patch/advisory tag | CISA SSVC exploitation: none
CVE-2026-102764   CNA BT           missing: patch/advisory tag | CISA SSVC exploitation: active
CVE-2026-0322     CNA palo_alto    missing: nothing | CISA SSVC exploitation: active

Only one of the eight is complete by these four checks. The GitHub-assigned ones show the text-range pattern: the fix version is in the record, but not where a parser expects it. The three with active exploitation in CISA's SSVC data are the three that are in its Known Exploited Vulnerabilities catalog. Run it over the CVEs in your open findings and you'll see which ones your tooling is likely guessing about.

What to do

  1. Know which fields your process depends on. If your SLA keys on CVSS, 5% of 2026 records still have no score from either the CNA or CISA. If your scanner needs a fixed version, check how it reads ranges.
  2. Use the source closest to the package. For open-source CVEs, take versions and fixes from the GitHub advisory database or OSV. For vendor products, use the vendor advisory. Treat the CVE record as the identifier and summary.
  3. Use CISA's enrichment. The ADP container adds CVSS, CWE and SSVC exploitation status to most records. Our post on CNA status covers why who assigns a CVE matters.
  4. If you're a CNA, fill the structured fields. lessThan for the fixed version and a patch or vendor-advisory tag on the link cost little and make records usable by tools.
  5. Watch for the criteria. When CISA defines record quality criteria, they'll likely shape what CNAs are expected to supply, and what you can expect to receive.

The fair view

The CVE Program's annual volume has roughly doubled since 2024 under a federated, largely volunteer model, and the data shows records getting steadily better through it. CNA-supplied scores and weaknesses are up about 10 and 12 percentage points since 2024. CISA's enrichment covers much of what CNAs leave out. Our four checks are also a blunt instrument: a record can pass them and still be wrong, or fail them and still be useful to a person.

But consumers don't read 81,803 records by hand. They depend on fields. A quality framework that starts with governance and participation is reasonable, because those are what make records better at the source. The test will be whether the criteria, when they arrive, measure what tools need. Until then, the gap is yours to close, and the script above shows where it is.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.