Discovery Got Cheap, Remediation Didn’t: A Pipeline View of Vulnerabilities in 2026
For years, the hard part of software security was finding the bug. You needed a skilled researcher, good tooling and a lot of time. That is no longer true, and I think many of us have not adjusted our mental model yet.
For years, the hard part of software security was finding the bug. You needed a skilled researcher, good tooling and a lot of time. That is no longer true, and I think many of us have not adjusted our mental model yet.
In this post I want to look at the problem the way an engineer would, as a pipeline with stages and a throughput at each stage. Then we will build a small script to measure where your own team sits.
The numbers that started this
| What | Figure | Source |
|---|---|---|
| Microsoft October 2026 Patch Tuesday | 1,400+ CVEs (tallies range 1,411 to 1,417), a record | Krebs on Security |
| Same month, one year earlier | about 175 CVEs | Computer Weekly |
| November 2026 Patch Tuesday | 1,094 CVEs, second largest ever | Splashtop |
| Exploitation as initial access (DBIR 2026) | 31%, up from 20% | Tenable summary |
| KEV vulnerabilities fully remediated | 26%, down from 38% | same |
| Median time to fully patch | 43 days, up from 32 | same |
Krebs reports that Microsoft credits large language models for the record volume. So discovery has clearly sped up. The interesting question is what happens after discovery.
The pipeline view
The Cloud Security Alliance published a whitepaper on November 6 that models vulnerability handling as a pipeline. I find the framing useful, so here is my condensed version with 2026 evidence at each stage.
| Stage | What limits it | 2026 evidence |
|---|---|---|
| Discovery | Compute and model access | Anthropic reports 129,000+ verified findings from Glasswing partners (self-reported, subset of partners). The AgentFlow paper found ten Chrome zero-days with an open-weight model. |
| Validation and triage | Skilled human reviewers | Anthropic calls independent human triage its limiting step. Claude rated about 91.5% of ledger findings critical or high, while maintainers rated fewer that way. |
| Intake | Maintainer security capacity | Torvalds described the kernel security list as unmanageable because of duplicate reports. Google paused product vulnerability intake in its open-source bounty on October 1. |
| Fix development | Maintainer engineering time | Anthropic's October 2 dashboard: 6,157 disclosed to maintainers, 516 known patched. |
| Publication and enrichment | Database analysts | NIST moved NVD to prioritized enrichment on April 15, after a 263% rise in CVE submissions from 2020 to 2025. |
| Downstream packaging | Distro and vendor release cycles | A fix released upstream does not mean it is installed anywhere. |
| Enterprise deployment | Testing, change management, asset visibility | Median 43 days to patch. Sonatype found about 13% of 2025 Log4j downloads still had Log4Shell. |
Source for the model and most stage evidence: CSA, The Disclosure Bottleneck. It is an industry whitepaper, not a peer-reviewed paper, and several of its inputs are self-reported by the organizations running the discovery programs.
The key point is simple. A pipeline can only run as fast as its slowest stage. Discovery used to be the slow stage, so the volume of serious bugs entering the pipeline was small. Now discovery is fast, and every stage after it is facing inflow it was never sized for.
Interactive version: Click through the pipeline and metrics in your browser
Be careful with the measurements
A few numbers get quoted a lot, and each needs a qualifier.
- Time-to-exploit of negative seven days (Mandiant M-Trends 2026) is a mean across vulnerabilities that were observed being exploited. It does not describe all vulnerabilities.
- Exploited on or before CVE publication: VulnCheck measured about 29% for vulnerabilities first exploited in 2025, so a large minority, not the majority.
- AI-found bugs and exploitation: VulnCheck's first-half 2026 data shows 14 of 1,061 AI-attributed discoveries (about 1.3%) confirmed exploited. Summary here. So the data does not support the claim that AI-found bugs are being exploited at unprecedented rates.
- Ledger accounting: VulnCheck's November review counts 37,966 Glasswing ledger findings with 217 marked fixed, and says the numbers do not fully reconcile. See VulnCheck's post. Anthropic's own figures are self-reported.
- Severity: AI-assigned severity ran well above maintainers' ratings, so sorting by AI severity alone will mislead your prioritization. My reading: the evidence strongly supports a capacity gap after discovery. It does not support a wave of weaponized AI-found bugs.
Patch consumption capacity
Most teams track mean time to remediate. That is an outcome metric, and it quietly assumes inflow is stable. The CSA paper suggests tracking how much inflow you can absorb, which it calls patch consumption capacity. Four indicators are enough to start:
- Inflow to closed ratio over a rolling window, split by risk tier. Persistently above 1 means the backlog is growing.
- Age of open vulnerabilities on internet-facing and high-impact systems.
- Share of vulnerabilities with missing metadata, meaning you cannot tell if you are affected. This matters more since NVD narrowed enrichment.
- Time to consume upstream fixes, from the upstream release to your deployment. Let us build the first two, plus a KEV check, in about 60 lines.
A small script
Input is a CSV export from whatever tracker you use. Dates are YYYY-MM-DD, and an empty closed_at means the item is still open.
cve_id,first_seen,closed_at
CVE-2021-44228,2026-10-05,
CVE-2026-0001,2026-10-25,2026-11-09
CVE-2026-0002,2026-10-30,
The script pulls CISA's Known Exploited Vulnerabilities catalog, then reports the inflow to closed ratio, the median age of open items, and how many open items are in KEV and older than your own target. Set KEV_FILE to a saved copy of the catalog if you want to run it offline or in CI.
TypeScript (Node 18 or newer)
// vuln-capacity.ts
// Run: npx tsx vuln-capacity.ts vulns.csv
import { readFileSync } from "node:fs";
type Row = { cveId: string; firstSeen: Date; closedAt: Date | null };
type Kev = { vulnerabilities: { cveID: string }[] };
const KEV_URL =
"https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
const WINDOW_DAYS = 30;
const KEV_SLA_DAYS = 7; // your own target, tune it
const DAY = 86_400_000;
function parseCsv(path: string): Row[] {
const [, ...lines] = readFileSync(path, "utf8").trim().split("\n");
return lines.map((line) => {
const [cveId, firstSeen, closedAt] = line.split(",").map((s) => s.trim());
return {
cveId,
firstSeen: new Date(firstSeen),
closedAt: closedAt ? new Date(closedAt) : null,
};
});
}
async function loadKev(): Promise<Set<string>> {
const raw = process.env.KEV_FILE
? readFileSync(process.env.KEV_FILE, "utf8")
: await fetch(KEV_URL).then((r) => {
if (!r.ok) throw new Error(`KEV fetch failed: ${r.status}`);
return r.text();
});
const kev = JSON.parse(raw) as Kev;
return new Set(kev.vulnerabilities.map((v) => v.cveID));
}
function median(xs: number[]): number {
if (xs.length === 0) return 0;
const s = [...xs].sort((a, b) => a - b);
const mid = Math.floor(s.length / 2);
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
}
async function main() {
const rows = parseCsv(process.argv[2] ?? "vulns.csv");
const kev = await loadKev();
const now = Date.now();
const since = now - WINDOW_DAYS * DAY;
const ageDays = (r: Row) => (now - r.firstSeen.getTime()) / DAY;
const inflow = rows.filter((r) => r.firstSeen.getTime() >= since).length;
const closed = rows.filter((r) => r.closedAt && r.closedAt.getTime() >= since).length;
const open = rows.filter((r) => !r.closedAt);
const openKev = open.filter((r) => kev.has(r.cveId));
const overdueKev = openKev.filter((r) => ageDays(r) > KEV_SLA_DAYS);
console.log(`Last ${WINDOW_DAYS} days`);
console.log(` New applicable vulns : ${inflow}`);
console.log(` Closed : ${closed}`);
console.log(` Inflow / closed : ${closed ? (inflow / closed).toFixed(2) : "inf"}`);
console.log(`Open backlog`);
console.log(` Total open : ${open.length}`);
console.log(` Median age (days) : ${median(open.map(ageDays)).toFixed(1)}`);
console.log(` Open and in KEV : ${openKev.length}`);
console.log(` KEV older than ${KEV_SLA_DAYS}d : ${overdueKev.length}`);
if (closed && inflow / closed > 1) {
console.log("\nRatio above 1: backlog is growing faster than you can close it.");
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
Python 3.9 or newer
# vuln_capacity.py
# Run: python vuln_capacity.py vulns.csv
import csv
import json
import os
import statistics
import sys
import urllib.request
from datetime import datetime, timedelta, timezone
KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
WINDOW_DAYS = 30
KEV_SLA_DAYS = 7 # your own target, tune it
def parse_day(value):
if not value:
return None
return datetime.fromisoformat(value).replace(tzinfo=timezone.utc)
def load_rows(path):
with open(path, newline="") as f:
return [
{
"cve": r["cve_id"].strip(),
"first_seen": parse_day(r["first_seen"].strip()),
"closed_at": parse_day(r["closed_at"].strip()),
}
for r in csv.DictReader(f)
]
def load_kev():
if os.environ.get("KEV_FILE"):
with open(os.environ["KEV_FILE"]) as f:
data = json.load(f)
else:
with urllib.request.urlopen(KEV_URL, timeout=30) as resp:
data = json.load(resp)
return {v["cveID"] for v in data["vulnerabilities"]}
def main():
rows = load_rows(sys.argv[1] if len(sys.argv) > 1 else "vulns.csv")
kev = load_kev()
now = datetime.now(timezone.utc)
since = now - timedelta(days=WINDOW_DAYS)
def age_days(r):
return (now - r["first_seen"]).total_seconds() / 86400
inflow = sum(1 for r in rows if r["first_seen"] >= since)
closed = sum(1 for r in rows if r["closed_at"] and r["closed_at"] >= since)
open_rows = [r for r in rows if not r["closed_at"]]
open_kev = [r for r in open_rows if r["cve"] in kev]
overdue = [r for r in open_kev if age_days(r) > KEV_SLA_DAYS]
print(f"Last {WINDOW_DAYS} days")
print(f" New applicable vulns : {inflow}")
print(f" Closed : {closed}")
print(f" Inflow / closed : {inflow / closed:.2f}" if closed else " Inflow / closed : inf")
print("Open backlog")
print(f" Total open : {len(open_rows)}")
median_age = statistics.median(age_days(r) for r in open_rows) if open_rows else 0
print(f" Median age (days) : {median_age:.1f}")
print(f" Open and in KEV : {len(open_kev)}")
print(f" KEV older than {KEV_SLA_DAYS}d : {len(overdue)}")
if closed and inflow / closed > 1:
print("\nRatio above 1: backlog is growing faster than you can close it.")
if __name__ == "__main__":
main()
Sample output on made-up data:
Last 30 days
New applicable vulns : 7
Closed : 2
Inflow / closed : 3.50
Open backlog
Total open : 6
Median age (days) : 6.8
Open and in KEV : 2
KEV older than 7d : 2
Ratio above 1: backlog is growing faster than you can close it.
What this script does not do
- It ignores reachability, so a vulnerable library you never call counts the same as one on a hot path.
- KEV only lists vulnerabilities with confirmed exploitation. An exploit-prediction score such as EPSS would add a forward-looking signal.
- It does not deduplicate. If your scanner reports the same CVE on 40 services, you will see 40 rows, which may or may not be what you want.
- The 7-day target is arbitrary. CISA's BOD 26-04 uses three days for its highest-risk combination, but that directive applies to US federal agencies and uses a richer risk model than a simple KEV match. Treat it as a starting point. Run it weekly from a scheduled CI job and watch the ratio over time. The trend matters more than any single number.
Practices that raise capacity
You cannot make upstream maintainers faster, but you can raise how much your own pipeline absorbs.
- Know your dependencies. An SBOM per service lets you match an upstream fix to your systems in minutes.
- Automate routine updates. Dependabot or Renovate, plus tests you trust, make patching a merge instead of a project.
- Make rollback cheap. Canary or progressive deploys let you ship fixes faster because a bad one is easy to undo.
- Use more than one data source. Since NVD narrowed enrichment, combine vendor advisories, the GitHub Advisory Database, KEV and exploit-prediction data.
- Contain what you cannot patch in time. Segmentation, least privilege, secret rotation, runtime protection and good detection reduce the blast radius of a late fix. Field note from a real team. Last month a critical PHP vulnerability surfaced that had sat in our codebase for over ten years, and a similar issue appeared in a Node.js dependency shortly after. Legacy systems were affected, and the fix pushed our regular release out by one sprint. The bottleneck was not writing the patch. It was the security review and testing around it, which is the same capacity gap this post measures.
Reading list
Papers:
- CyberGym (arXiv 2506.02548): AI agents found 35 zero-days and 17 incomplete patches in open-source projects.
- AgentFlow (arXiv 2604.20801): ten Chrome zero-days from a synthesized multi-agent harness. The authors call it a case study.
- ZeroDayBench (arXiv 2603.02297): a benchmark for finding and patching 22 novel critical vulnerabilities.
CyberGym-E2E (arXiv 2606.04460): end-to-end benchmark covering discovery, proof of concept and patching.
Code you can read and run:trailofbits/buttercup: AI-assisted fuzzing plus a multi-agent patcher, second place in DARPA's AI Cyber Challenge.
ossf/oss-crs: framework for running AIxCC cyber reasoning systems on real open-source projects.
o2lab/afc-crs-all-you-need-is-a-fuzzing-brain: open-source AIxCC system with a public LLM leaderboard.
google/oss-fuzz: the continuous fuzzing service several of these benchmarks are built on.
Analysis:-
Over to you
If you try the script, I would like to know what your inflow to closed ratio looks like. And if you track patch capacity in a different way, please share it in the comments. I am always looking for better indicators.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.
