#!/usr/bin/env python3 """CNA Operational Rules 4.5.1.4: a CNA must publish the CVE record within 72 hours of publicly disclosing the CVE ID. For GitHub's CNA (assigner GitHub_M) the public disclosure is the GitHub Security Advisory (GHSA) that cites the CVE. Pull the newest reviewed advisories from GitHub's API, look each CVE up in CVE Services, and measure CVE datePublished minus GHSA published_at. Needs GITHUB_TOKEN for a usable rate limit. Output: cna72_results.json with per-advisory rows and the count over 72 hours, by assigner.""" import json, os, sys, time, urllib.request, datetime as dt, collections H = {"User-Agent": "Markovian Protocol hello@markovianprotocol.com", "Accept": "application/vnd.github+json"} if os.environ.get("GITHUB_TOKEN"): H["Authorization"] = "Bearer " + os.environ["GITHUB_TOKEN"] def get(url, headers=H): for a in range(4): try: return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60)) except Exception as e: err = e; time.sleep(3 * (a + 1)) raise err N = int(sys.argv[1]) if len(sys.argv) > 1 else 780 rows = []; page = 1 while len(rows) < N: adv = get(f"https://api.github.com/advisories?type=reviewed&per_page=100&page={page}&sort=published&direction=desc") if not adv: break for a in adv: if a.get("cve_id"): rows.append({"ghsa": a["ghsa_id"], "cve": a["cve_id"], "published_at": a["published_at"], "severity": a.get("severity")}) page += 1 rows = rows[:N] def p(s): return dt.datetime.fromisoformat(s.replace("Z", "+00:00")) for r in rows: try: c = get(f"https://cveawg.mitre.org/api/cve/{r['cve']}", {"User-Agent": H["User-Agent"]}) m = c["cveMetadata"]; r["assigner"] = m.get("assignerShortName"); r["cve_published"] = m.get("datePublished"); r["state"] = m.get("state") if r["cve_published"]: r["lag_hours"] = round((p(r["cve_published"]) - p(r["published_at"])).total_seconds() / 3600, 2) except Exception as e: r["error"] = repr(e) time.sleep(0.2) by = collections.defaultdict(list) for r in rows: if "lag_hours" in r: by[r["assigner"]].append(r["lag_hours"]) summary = {k: {"n": len(v), "over_72h": sum(1 for x in v if x > 72), "median_h": sorted(v)[len(v) // 2], "max_h": max(v)} for k, v in by.items()} json.dump({"summary": summary, "rows": rows}, open("cna72_results.json", "w"), indent=1) for k in sorted(summary, key=lambda k: -summary[k]["n"]): print(k, summary[k])