#!/usr/bin/env python3 """Render SM-021 from summary.json. python3 make_page.py summary.json git_check.json census.jsonl OUT.html""" import json, sys, os, html, math S = json.load(open(sys.argv[1])); G = json.load(open(sys.argv[2])); CEN = sys.argv[3]; OUT = sys.argv[4] HERE = os.path.dirname(os.path.abspath(__file__)); P = os.path.join(HERE, "page_parts") esc = html.escape n = lambda x: f"{x:,}" if isinstance(x, int) else str(x) days = lambda h: f"{h / 24:.0f} days" if h >= 96 else f"{round(h)} hours" Y, A, B, M = S["year"], S["jan_mar"], S["apr_on"], S["months"] RB = S["robustness_with_db_labels"]; NEG = S["negative_wait_excluded"]; X = S["excluded"] MN = {"01": "January", "02": "February", "03": "March", "04": "April", "05": "May", "06": "June", "07": "July", "08": "August", "09": "September", "10": "October"} SUP = {"npm", "pip", "maven", "nuget", "go", "rubygems", "composer", "rust", "erlang", "actions", "pub", "swift"} counted = set() for l in open(CEN): r = json.loads(l) if (r.get("repo_published_at") or "")[:4] == "2026" and set(r.get("repo_ecosystems") or []) & SUP: counted.add(r["ghsa"]) gd = [x["commit_minus_api_min"] for x in G["sample"] if x["commit_minus_api_min"] is not None] gin = sum(1 for x in G["sample"] if x["ghsa"] in counted); earlier = sum(1 for x in G["sample"] if x["unreviewed_commits_before"]) notin = Y["not_in_database"] _dep = os.path.expanduser("~/second-measurements/zenodo/sm-021/deposit.json") DOI = json.load(open(_dep))["metadata"]["prereserve_doi"]["doi"] if os.path.exists(_dep) else "pending" title = (f"Dependabot doesn’t alert on a security advisory until GitHub reviews it, which GitHub’s documentation says “can take up to 72 hours”. Since April, " f"{n(B['reviewed_over_72h'])} of {n(B['reviewed'])} reviewed advisories took longer, and the median wait was {days(B['median_wait_h'])}") desc = (f"When a project publishes a security advisory on GitHub, GitHub reviews it into the GitHub Advisory Database, and Dependabot does not alert on an advisory until it is reviewed. " f"GitHub’s documentation says the review “can take up to 72 hours”. We read both timestamps for every CVE GitHub issued that NVD published from 1 January to 16 October 2026; " f"{n(S['rows_counted'])} have a 2026 advisory naming a package in an ecosystem the database supports. Of those reviewed, {A['reviewed_over_72h_pct']}% of January to March advisories took longer than 72 hours; from April on, {B['reviewed_over_72h_pct']}%.") LATE = {"2026-08", "2026-09", "2026-10"} rows = "".join(f"{MN[m[5:]]} {m[:4]}{' †' if m in LATE else ''}{n(v['advisories'])}{n(v['reviewed'])}{n(v['reviewed_over_72h'])}" f"{v['reviewed_over_72h_pct']}%{days(v['median_wait_h'])}{days(v['median_wait_lower_bound_h'])}" f"{n(v['not_in_database'])}{v['median_repo_to_nvd_d']:g}" for m, v in M.items()) eco = "".join(f"{esc(e)}{n(v['reviewed'])}{v['reviewed_over_72h_pct']}%{days(v['median_wait_h'])}" for e, v in sorted(S["ecosystems"].items(), key=lambda kv: -kv[1]["reviewed"]) if v["reviewed"] >= 100) L = S["longest_reviewed"] LA = next(x for x in L if x["published"][:7] >= "2026-04") exs = (f"{esc(L[0]['repo'])} ({esc(L[0]['ghsa'])}), {L[0]['wait_d']:.0f} days; {esc(L[1]['repo'])} ({esc(L[1]['ghsa'])}), {L[1]['wait_d']:.0f} days; " f"and two {esc(L[2]['repo'])} advisories published the same day, {esc(L[2]['ghsa'])} and {esc(L[3]['ghsa'])}, {L[2]['wait_d']:.0f} days each") neg = f"{esc(NEG[0]['repo'])} ({esc(NEG[0]['ghsa'])})" if NEG else "" style = open(os.path.join(P, "style.html")).read() top = open(os.path.join(P, "top.html")).read().replace("SECOND MEASUREMENT SM-017", "SECOND MEASUREMENT SM-021") foot = open(os.path.join(P, "foot.html")).read() url = "https://markovianprotocol.com/measurements/sm-021.html" head = f""" SM-021 {title} — Markovian Protocol {style} """ body = f"""

{title}

Markovian ProtocolMeasured {S['read_start'][:10]}Status: draft, not publishedDOI: {DOI}

{desc}

The short version

Disclosure: Markovian Protocol holds no financial position in GitHub or Microsoft, was paid by no one for this work, and showed it to no one before publication. How we work.

Month by month

{rows}
Advisory publishedIn sampleReviewedOver 72 hShareMedian wait, reviewedMedian if all not in database are waitingNot in databaseDays to NVD (median)

Repository advisories behind CVEs issued by GitHub (NVD source identifier security-advisories@github.com) and published by NVD 1 January to 16 October 2026, whose advisory names a package in npm, pip, Maven, NuGet, Go, RubyGems, Composer, Rust, Erlang, Actions, Pub or Swift, the ecosystems the database supports.3 Wait = github_reviewed_at minus the repository advisory’s published_at, compared to the second. The second median counts each advisory not in the database at its age when we read it. It is a floor only if every one of them is still waiting; some were probably declined (see the short version), and those pull it up. Read from {S['read_start'][11:16]} UTC on {S['read_start'][:10]}.

Why the recent months look better than they are (†). The sample is chosen by the date NVD published the CVE, and GitHub usually reviews the advisory before its CVE appears on NVD: {n(Y['reviewed_before_nvd'])} of the {n(Y['reviewed'])} reviewed advisories. As the queue grew, the gap from a project’s advisory to its CVE on NVD grew too, to a median of {M['2026-06']['median_repo_to_nvd_d']:g} days for June’s advisories. So an advisory published in August, September or October that is still waiting mostly has no CVE on NVD yet and is not in our sample at all. That makes the counts for those months too low and their medians too short, so the falling medians after July cannot be read as a recovery.

By ecosystem

{eco}
EcosystemReviewed in 2026Over 72 hMedian wait

Every ecosystem with 100 or more reviewed advisories, by the ecosystems the project named in its own advisory; one naming packages in two ecosystems counts in both. {n(RB['added_rows'])} reviewed advisories whose project left the ecosystem blank or unsupported, but which GitHub reviewed into a supported one, are not counted. Adding them changes little: {RB['jan_mar']['reviewed_over_72h_pct']}% over 72 hours for January to March, {RB['apr_on']['reviewed_over_72h_pct']}% from April.

Why the wait matters

A project that publishes an advisory has already told the world about the flaw. The projects that depend on it hear from Dependabot, and Dependabot does not alert on an unreviewed advisory.3 GitHub’s blog said so for the backlog: “New advisories may take longer to trigger, with critical issues prioritized.”2 Our numbers do not separate advisories by severity, so critical ones may move faster than the median. For the median advisory published since April that has been reviewed, the gap was {days(B['median_wait_h'])}.

A study of 2019 to 2025 found repository advisories were the database’s fast path.5 Our January to March numbers agree; the change starts in April.

How this ends

GitHub can close it either way: by clearing the queue until reviews are back inside 72 hours, or by changing the documentation to say what to expect now. The blog promised, “If things improve, we’ll tell you,” and the September staff reply, about CVE request turnaround, said “Feedback around updating our turnaround time was passed along to our curation team.”4 We recheck monthly and print the table here.

Our questions to the GitHub Advisory Database team

  1. Will the documentation’s “up to 72 hours” be updated to the current expectation, and is there a date by which reviews will be back inside it?
  2. Would you publish the queue: advisories waiting and the median wait, weekly?
  3. Of the {n(notin)} 2026 advisories in our sample that were not in the database on {S['read_start'][:10]}, how many were declined and how many are still waiting?

Their reply, scored

Not yet sent.

What we told whom, and when.

17 Oct 2026Measured. Nobody has seen the page.

What we can’t be sure of

Run it yourself

gunzip -k exhibits/census_2026-10-17.jsonl.gz
python3 analyze.py exhibits/census_2026-10-17.jsonl 2026-10-17T11:55:00Z summary_2026-10-17.json
python3 git_check.py exhibits/census_2026-10-17.jsonl 40 20261017 git_check_2026-10-17.json
python3 ghsa_census.py census_new.jsonl 2026-01-01 2026-10-16   # a fresh census, about four hours

Needs Python 3 and the GitHub CLI logged in; NVD’s API is anonymous and paced. The census makes about 24,000 GitHub API calls, roughly four hours at the standard rate limit. Scripts and exhibits: sm-021/.

References

  1. GitHub Docs, Repository security advisories. docs.github.com, saved copy github_docs_about_repository_security_advisories_2026-10-17.md.
  2. GitHub Blog, Inside the Advisory Database and what happens when vulnerability volume breaks records, 29 June 2026, updated 1 July. github.blog, saved copy github_blog_2026-06-29_advisory_database_volume.html.
  3. GitHub Docs, GitHub Advisory database. docs.github.com, saved copy github_docs_about_the_github_advisory_database_2026-10-17.md.
  4. GitHub Community discussion 205706, CVE not assigned for 24 days?, staff reply of 8 September 2026. github.com, saved copy github_community_discussion_205706_2026-10-17.json; the replier’s GitHub profile (company “GitHub”), github_user_ghostinhershell_2026-10-18.json.
  5. C. Segal, P. Segal, C. E. Banjar et al., Characterizing and Modeling the GitHub Security Advisories Review Pipeline, MSR 2026. arXiv:2602.06009.
  6. NVD CVE API 2.0, sourceIdentifier=security-advisories@github.com; GitHub REST API, repository security advisories and global advisories; github/advisory-database commit history.
""" open(OUT, "w").write(head + top + body + foot) print("wrote", OUT, len(head + top + body + foot))