mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-27 16:51:03 +00:00
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:
1 parent
8c739f44e5
commit
2b6547ef29
1597 files changed
+1670
-1
No files matched your search
@@ -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
@@ -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 (
|
||||
" -> 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)
|
||||
|
||||
Reference in new issue
Block a user