#!/usr/bin/env python3 """Render the PoC Index hero banner and social card as self-contained SVG. Both cards are plain SVG with no external font, image or stylesheet reference: GitHub proxies README images through camo, which fetches the file on its own and would drop anything it has to resolve afterwards. """ from __future__ import annotations import importlib.util import json import os import sys from urllib import request ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)) BUILD_SITE = os.path.join(ROOT, "scripts", "build_site.py") sys.path.insert(0, os.path.join(ROOT, "scripts")) from brand import BRAND, SITE, SUBTITLE, host LIVE_STATS = f"{SITE}/trending_poc.json" LIVE_KEV = f"{SITE}/kev.json" HERO = os.path.join(ROOT, "docs", "hero.svg") SOCIAL = os.path.join(ROOT, "docs", "social-card.svg") STATS = os.path.join(ROOT, "docs", "stats.json") KEV_PILL = os.path.join(ROOT, "docs", "kev.svg") CANVAS = "#0d1117" SUBTLE = "#161b22" BORDER = "#30363d" FG = "#e6edf3" MUTED = "#8b949e" NEUTRAL = "#6e7681" ACCENT = "#2f81f7" DANGER = "#f85149" MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" def corpus_counts() -> dict | None: """Count the index the same way the site build does. The banner and the site header have to agree, so this reuses build_site's own parser instead of a second guess at what counts as a PoC. Roughly fifteen seconds over the whole corpus. """ try: spec = importlib.util.spec_from_file_location("build_site", BUILD_SITE) build_site = importlib.util.module_from_spec(spec) spec.loader.exec_module(build_site) entries, total = build_site.build_cve_list(build_site.load_blacklist()) except Exception as problem: print(f"corpus count failed: {problem}") return None if not total: return None return { "total_cves": total, "with_pocs": len(entries), "kev": len(build_site.build_kev(entries)), } def live_stats() -> dict | None: """What the deployed site is currently serving. Only reached when the corpus itself will not parse, which on a normal checkout means something is broken rather than something is missing. """ try: with request.urlopen(LIVE_STATS, timeout=20) as response: data = json.load(response) total = int(data.get("total_cves") or 0) pocs = int(data.get("with_pocs") or 0) except Exception as problem: print(f"live stats failed: {problem}") return None if not total or not pocs: return None try: # the site's copy, already narrowed to CVEs the index carries, so this # path reports the same figure the corpus count would with request.urlopen(LIVE_KEV, timeout=20) as response: kev = len(json.load(response)) except Exception: kev = 0 return {"total_cves": total, "with_pocs": pocs, "kev": kev} def group(value: int) -> str: return f"{value:,}" def sweep(cx: float, cy: float, radius: float, opacity: float) -> str: """Radar rings with a hand rotating over them once every eight seconds.""" rings = "".join( f'' for scale, fade in ((0.35, 0.9), (0.62, 0.65), (0.85, 0.45), (1.0, 0.3)) ) return f""" {rings} """ def stat_block(x: float, y: float, value: str, label: str, colour: str, size: int) -> str: return f"""{value} {label}""" def hero(counts: dict) -> str: blocks = "".join( stat_block(x, 96, value, label, colour, 34) for x, value, label, colour in ( (620, group(counts["total_cves"]), "CVES INDEXED", FG), (840, group(counts["with_pocs"]), "WITH POCS", ACCENT), (1010, group(counts["kev"]), "KNOWN EXPLOITED", DANGER), ) ) return f""" {sweep(1090, 90, 160, 0.3)} {BRAND} {SUBTITLE} {host()} {blocks} """ def social(counts: dict) -> str: """1280x640 card for the repository social preview and the site og:image.""" blocks = "".join( stat_block(x, 430, value, label, colour, 46) for x, value, label, colour in ( (96, group(counts["total_cves"]), "CVES INDEXED", FG), (500, group(counts["with_pocs"]), "WITH POCS", ACCENT), (880, group(counts["kev"]), "KNOWN EXPLOITED", DANGER), ) ) return f""" {sweep(1000, 300, 220, 0.26)} {BRAND} {SUBTITLE} {blocks} {host()} """ def kev_pill() -> str: """The KEV marker the README table puts beside a row. Every flagged row points at this one file, so GitHub's image proxy fetches it once and the table stays cheap however many rows carry it. """ return ( '' f'' f'KEV' "\n" ) def render(counts: dict) -> None: """Draw both cards and leave the figures behind for the README build.""" for path, markup in ((HERO, hero(counts)), (SOCIAL, social(counts)), (KEV_PILL, kev_pill())): with open(path, "w", encoding="utf-8") as handle: handle.write(markup) # No timestamp: this file should only change when the figures do, so the # README build can read the same numbers the banner is drawn from. with open(STATS, "w", encoding="utf-8") as handle: json.dump(counts, handle, indent=1, sort_keys=True) handle.write("\n") def main() -> int: counts = corpus_counts() or live_stats() if not counts: # Drawing zeros would be worse than leaving the previous banner in place. print("no usable counts, leaving the existing banner alone") return 1 render(counts) print(f"Wrote {HERO}, {SOCIAL} and {STATS}: {group(counts['total_cves'])} CVEs, " f"{group(counts['with_pocs'])} with PoCs, {group(counts['kev'])} known exploited") return 0 if __name__ == "__main__": raise SystemExit(main())