Monthly CVE statistics from the National Vulnerability Database, published on the first of every month.
An unattended pipeline that pulls the NVD feed, works out what actually changed last month, and commits a report, a set of social-sized charts, and post copy ready to paste. It shares its chart styling with CVEGraphs, so output from either repo sits together in a feed.
| Metric | Value |
|---|---|
| CVEs published, September 2026 | 14,943 |
| 2026 year to date | 72,812 |
| All time, since 1988 | 381,691 |
| Average per day, September | 498.1 |
| Mean CVSS, September | 6.98 |
| Median CVSS, September | 7.3 |
Data through September 30, 2026, excluding rejected CVEs.
This block is regenerated by the monthly run. Full reports live in outputs/, one directory per month.
pip install -r requirements.txt
python -m src.cli.main download-data # ~1.3 GB, resumes if interrupted
python -m src.cli.main run-monthly # report on the completed month
python -m src.cli.main generate-ytd-report # YTD charts + post copydownload-data writes data/nvd.jsonl, which is gitignored; every other command
reads from it. run-monthly and generate-ytd-report report on the previous
month when run on the 1st, and on the current month otherwise.
Other commands: generate-reports --year 2025 --month 1 for a specific month,
update-readme-stats to refresh the block above, validate to check paths and
data, check-timezone to confirm the scheduled run's timezone.
| File | Role |
|---|---|
src/config.py |
Paths, NVD source URL, which month to report on, CI detection |
src/data/downloader.py |
Downloads the NVD feed, resumable, with retries |
src/data/processor.py |
Flattens nested CVE JSON into a DataFrame, filters by date |
src/analysis/statistics.py |
CVSS distribution, CNA rankings, CWE counts, daily spread |
src/analysis/trends.py |
Month-over-month and year-over-year movement |
src/analysis/ytd_growth.py |
Year-to-date totals and the social post copy |
src/reports/generator.py |
Writes the Markdown, JSON, and CSV reports |
src/reports/style.py |
Chart house style: palette, fonts, header, date stamp |
src/reports/ytd_visualizer.py |
YTD growth and YoY charts, three ratios, two themes |
src/utils/readme_updater.py |
Regenerates the stats block above |
src/cli/main.py |
Typer CLI: every command listed here |
fonts/ |
Bundled typefaces, committed so CI renders in the right face |
tasks/ |
One-off maintenance scripts, not part of the monthly run |
docs/archive/ |
Superseded planning and migration notes |
STYLE.md |
The house style these reports and charts are written to |
monthly-update.yml runs on the 1st at 10:00 UTC, which is 5:00 AM Central in
summer and 4:00 AM in winter, and does the following:
download-datarefreshesdata/nvd.jsonlfrom the NVD mirror.run-monthlywritesoutputs/YYYY/Month/Month.{md,json}for the completed month, with the trend sections computed across the full year to date.generate-ytd-reportrenders the charts and writespost.txt,enriched_post.txt, andytd_summary.json.update-readme-statsregenerates the stats block in this README.- The run commits
outputs/andREADME.md, then cuts a GitHub release usingpost.txtas the body.
If any step fails the workflow opens an issue instead of failing quietly.
outputs/2026/
May/
May.md # the month's report
May.json # same data, machine readable
CVE_Growth_2026_{dark,light}_landscape.png
CVE_Growth_2026_{dark,light}_square.png
CVE_Growth_2026_{dark,light}_portrait.png
YOY_CVE_Comparison_2026_vs_2025.png
post.txt # the social post, also the release body
enriched_post.txt # longer version with CVSS and CWE detail
ytd_summary.json # YTD + all-time figures for the README block
alt_text.md # alt text for every chart, ready to paste
Each report covers CVSS scores and severity bands, the most active CNAs, the most common weakness types, the daily publication spread, and the year-to-date trend.
The year-to-date analysis also compares against prior complete years, not just the same point last year, so the post copy can call the moment the current year overtakes a previous year's full total. Through July 27, 2026 sits 4,295 CVEs short of all of 2025 and is on track to pass it around August 17.
Every chart renders in three aspect ratios, each in dark and light: wide
(1600×900, X landscape, filed under the historical landscape name), square
(1080×1080, LinkedIn/X/Mastodon/Bluesky/IG), and portrait (1080×1350, IG and
the LinkedIn feed).
Styling comes from src/reports/style.py, a port of CVEGraphs' style_social.py:
Host Grotesk headlines, Roboto body, Roboto Mono for the eyebrow and date stamp,
a deep-navy accent with red reserved for the highlighted series, and a hairline
grid. The typefaces are committed under fonts/ because these charts render on a
bare CI runner; without them matplotlib silently substitutes a fallback face.
pytest
black --check src/ tests/
flake8 src/ tests/ --max-complexity=10 --max-line-length=100
mypy src/ --ignore-missing-importstests.yml runs on push and pull request to main and develop, across Python
3.11, 3.12, and 3.13. Only pytest and the error-level flake8 pass are blocking;
black, mypy, and isort report without failing the build.
The monthly run is fully automated end to end, from download through release. Reports cover CVSS, CNA, CWE, and daily distribution for the month, plus month-over-month and year-over-year movement across the year to date.
Known rough edges, in rough order of how much they matter:
- The CNA table is keyed on NVD's
sourceIdentifier, which is an assigning source (often an email address) rather than a normalized CNA name. Useful for ranking, ugly to read. - Test coverage is thin and concentrated on configuration and the README updater. The analysis and chart paths are exercised only by the monthly run.
processor.pyloads the entire feed into memory before filtering, which is fine on a GitHub runner but wasteful for a single month.- CVSS figures cover v3.0 and v3.1 only. There is no v4 handling, and CVEs without a v3 score are counted but unscored.
Built on free CVE tooling from RogoLabs.