Badge known-exploited CVEs in the repository itself

KEV reached the site three days ago but nothing in the markdown said a
vulnerability was under active attack, and the repository is what most
people actually read. 1,595 entries now carry a red CISA KEV shield beside
their product and vulnerability badges, marked for ransomware use where
CISA records it.

update_cves writes the badge on entries it creates, and the weekly audit
reconciles the whole set against the refreshed catalogue, so an entry CISA
adds gains the badge and one it withdraws loses it. Re-running the sync is
a no-op.
This commit is contained in:
0xMarcio committed 2026-08-31 04:02:59 +02:00
1 parent 8c739f44e5
commit 2b6547ef29
1597 files changed
+1670 -1

No files matched your search

+51
View File
@@ -17,6 +17,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
from update_cves import (
GITHUB_GRAPHQL_URL,
kev_badge,
USER_AGENT,
GitHubClient,
github_repo_from_url,
@@ -300,6 +301,54 @@ def refresh_kev(*, dry_run: bool) -> int:
return len(entries)
KEV_MARKER = "label=CISA%20KEV"
def sync_kev_badges(*, dry_run: bool) -> tuple[int, int]:
"""Put the known-exploited badge on the entries CISA lists, and only those.
The site shows KEV in a search result, but the repository is what most
people read, and nothing in the markdown said a vulnerability was under
active attack.
"""
try:
catalogue = json.loads(KEV_FILE.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return 0, 0
added = removed = 0
for path in sorted(ROOT.glob("[12][0-9][0-9][0-9]/CVE-*.md")):
cve_id = path.stem.upper()
wanted = kev_badge(cve_id, catalogue)
with path.open("r", encoding="utf-8", newline="") as handle:
text = handle.read()
has = KEV_MARKER in text
if not wanted and not has:
continue
newline = "\r\n" if "\r\n" in text else "\n"
lines = text.split(newline)
lines = [line for line in lines if KEV_MARKER not in line]
if wanted:
anchor = next((i for i, line in enumerate(lines) if line.strip() == "### Description"), None)
if anchor is None:
continue
insert = anchor
while insert > 0 and not lines[insert - 1].strip():
insert -= 1
lines.insert(insert, wanted)
updated = newline.join(lines)
if updated == text:
continue
added += 1 if wanted else 0
removed += 1 if has and not wanted else 0
if not dry_run:
with path.open("w", encoding="utf-8", newline="") as handle:
handle.write(updated)
return added, removed
def load_state() -> dict:
try:
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
@@ -380,6 +429,8 @@ def main() -> int:
save_state(state, dry_run=args.dry_run)
print(f"KEV catalogue: {refresh_kev(dry_run=args.dry_run)} known-exploited CVEs")
badged, unbadged = sync_kev_badges(dry_run=args.dry_run)
print(f"KEV badges: {badged} written, {unbadged} withdrawn")
return 0
+24 -1
View File
@@ -24,6 +24,7 @@ GITHUB_LIST = ROOT / "github.txt"
REFERENCE_LIST = ROOT / "references.txt"
BLACKLIST_FILE = ROOT / "blacklist.txt"
STATE_FILE = ROOT / ".github" / "cve_sync_state.json"
KEV_FILE = ROOT / "kev.json"
GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
CVE_API_URL = "https://cveawg.mitre.org/api/cve/{cve_id}"
@@ -907,6 +908,23 @@ def fetch_missing_records(
records.update(fetch_records(urls, allow_not_found=True))
def load_kev(path: Path = KEV_FILE) -> dict[str, list]:
"""CISA's known-exploited catalogue, as refreshed by the weekly audit."""
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
def kev_badge(cve_id: str, kev: dict[str, list]) -> str:
"""The one badge that says an attacker is using this today."""
entry = kev.get(cve_id.upper())
if not entry:
return ""
ransomware = len(entry) > 1 and entry[1]
return badge("CISA KEV", "Known Exploited · Ransomware" if ransomware else "Known Exploited", "red")
def badge(label: str, message: str, color: str) -> str:
return (
"![](https://img.shields.io/static/v1?"
@@ -919,6 +937,7 @@ def build_markdown(
details: CVEDetails,
github_links: list[str],
references: list[str],
kev: dict[str, list] | None = None,
) -> str:
description = "\n".join(
line.expandtabs(4).rstrip() for line in details.description.strip().splitlines()
@@ -932,6 +951,9 @@ def build_markdown(
lines.extend(badge("Product", value, "blue") for value in products)
lines.extend(badge("Version", value, version_color) for value in versions)
lines.extend(badge("Vulnerability", value, vuln_color) for value in vulnerabilities)
known_exploited = kev_badge(cve_id, kev or {})
if known_exploited:
lines.append(known_exploited)
lines.extend(["", "### Description", "", description, "", "### POC", "", "#### Reference"])
lines.extend((f"- {url}" for url in references) if references else ["No PoCs from references."])
lines.extend(["", "#### Github"])
@@ -1042,6 +1064,7 @@ def sync_markdown(
dry_run: bool,
) -> tuple[SyncStats, dict[str, list[str]], dict[str, list[str]]]:
stats = SyncStats()
kev = load_kev()
accepted_github: dict[str, list[str]] = {}
accepted_references: dict[str, list[str]] = {}
cve_ids = sorted(set(github) | set(references))
@@ -1073,7 +1096,7 @@ def sync_markdown(
if not dry_run:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
build_markdown(cve_id, details, github_links, reference_links),
build_markdown(cve_id, details, github_links, reference_links, kev),
encoding="utf-8",
)
stats.created.append(cve_id)