2025-12-17 13:59:41 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
2026-09-01 05:24:39 +02:00
|
|
|
import csv
|
|
|
|
|
import gzip
|
2026-10-21 20:15:22 +02:00
|
|
|
import html
|
2026-09-01 05:24:39 +02:00
|
|
|
import io
|
2025-12-18 07:13:08 +01:00
|
|
|
import json
|
2025-12-17 21:06:33 +01:00
|
|
|
import re
|
2026-09-01 05:24:39 +02:00
|
|
|
import urllib.request
|
2025-12-18 07:13:08 +01:00
|
|
|
from datetime import datetime, timezone
|
2026-09-01 19:01:07 +02:00
|
|
|
from functools import lru_cache
|
2025-12-18 07:13:08 +01:00
|
|
|
from pathlib import Path
|
2026-09-02 17:17:28 +02:00
|
|
|
from typing import Collection, Dict, Iterable, List, Optional
|
2026-09-04 02:32:22 +02:00
|
|
|
from urllib.parse import unquote, urlparse
|
2025-12-17 13:59:41 +01:00
|
|
|
|
2026-10-21 16:55:01 +02:00
|
|
|
from update_cves import markdown_record_status
|
2026-10-22 13:26:22 +02:00
|
|
|
import link_health
|
2026-10-21 16:55:01 +02:00
|
|
|
|
2026-08-30 21:09:37 +02:00
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
2026-09-01 14:43:06 +02:00
|
|
|
CVES = ROOT / "cves"
|
|
|
|
|
INDEX = ROOT / "index"
|
2026-08-30 21:09:37 +02:00
|
|
|
DOCS_DIR = ROOT / "docs"
|
2026-09-01 14:43:06 +02:00
|
|
|
BLACKLIST = INDEX / "blacklist.txt"
|
|
|
|
|
TRENDING_INPUT = INDEX / "trending.json"
|
2025-12-18 07:13:08 +01:00
|
|
|
CVE_OUTPUT = DOCS_DIR / "CVE_list.json"
|
|
|
|
|
TRENDING_OUTPUT = DOCS_DIR / "trending_poc.json"
|
2026-09-01 14:43:06 +02:00
|
|
|
REPO_META = INDEX / "repo_meta.json"
|
2026-08-30 21:53:19 +02:00
|
|
|
REPO_META_OUTPUT = DOCS_DIR / "repo_meta.json"
|
2026-09-01 14:43:06 +02:00
|
|
|
KEV_INPUT = INDEX / "kev.json"
|
|
|
|
|
DATES_INPUT = INDEX / "cve_dates.json"
|
2026-08-31 00:13:01 +02:00
|
|
|
KEV_OUTPUT = DOCS_DIR / "kev.json"
|
2026-09-01 14:43:06 +02:00
|
|
|
NUCLEI_INPUT = INDEX / "nuclei.json"
|
2026-09-01 05:11:31 +02:00
|
|
|
NUCLEI_OUTPUT = DOCS_DIR / "nuclei.json"
|
2026-09-01 19:01:07 +02:00
|
|
|
METADATA_INPUT = INDEX / "metadata"
|
|
|
|
|
METADATA_OUTPUT = DOCS_DIR / "cve_metadata.json"
|
2026-09-04 02:32:22 +02:00
|
|
|
# The same metadata in the two halves the page loads apart: the CVSS rows the
|
|
|
|
|
# severity filter needs before anything can be searched, and the advisories
|
|
|
|
|
# only a rendered row shows. cve_metadata.json stays whole for API readers.
|
|
|
|
|
CVSS_OUTPUT = DOCS_DIR / "cvss.json"
|
|
|
|
|
ADVISORIES_OUTPUT = DOCS_DIR / "advisories.json"
|
2026-09-01 19:01:07 +02:00
|
|
|
VERIFIED_REFERENCES = INDEX / "reference_pocs.txt"
|
2026-10-07 01:23:59 +02:00
|
|
|
REVIEWED_REFERENCES = INDEX / "reference_poc_verified.json"
|
2026-09-01 05:24:39 +02:00
|
|
|
EPSS_OUTPUT = DOCS_DIR / "epss.json"
|
|
|
|
|
EPSS_FEED = "https://epss.empiricalsecurity.com/epss_scores-current.csv.gz"
|
2026-09-01 22:08:20 +02:00
|
|
|
GITHUB_NON_REPOS = {
|
|
|
|
|
"advisories", "apps", "collections", "marketplace", "notifications",
|
|
|
|
|
"orgs", "security", "settings", "sponsors", "topics", "user-attachments",
|
|
|
|
|
}
|
|
|
|
|
CODE_SUFFIXES = {
|
|
|
|
|
".c", ".cc", ".cpp", ".cs", ".go", ".java", ".js", ".md", ".nse",
|
|
|
|
|
".php", ".pl", ".ps1", ".py", ".rb", ".rs", ".sh", ".ts", ".txt",
|
|
|
|
|
".yaml", ".yml", ".zip",
|
|
|
|
|
}
|
|
|
|
|
MEDIA_SUFFIXES = {
|
|
|
|
|
".bmp", ".gif", ".jpeg", ".jpg", ".mov", ".mp4", ".pdf", ".png",
|
|
|
|
|
".svg", ".webm", ".webp",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 21:09:37 +02:00
|
|
|
def load_blacklist() -> set[str]:
|
|
|
|
|
if not BLACKLIST.exists():
|
|
|
|
|
return set()
|
|
|
|
|
return {
|
|
|
|
|
line.strip().lower()
|
|
|
|
|
for line in BLACKLIST.read_text(encoding="utf-8").splitlines()
|
|
|
|
|
if line.strip() and not line.lstrip().startswith("#")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-12-18 07:13:08 +01:00
|
|
|
def normalise_block(text: str) -> str:
|
2026-10-21 20:15:22 +02:00
|
|
|
# CNA descriptions sometimes contain formatting HTML. Keep their text,
|
|
|
|
|
# including encoded payload examples, rather than displaying markup.
|
2026-10-21 20:18:38 +02:00
|
|
|
if re.match(r"\s*<(?:p|div|ul|ol|h[1-6])(?:\s|>)", text, flags=re.I):
|
|
|
|
|
text = re.sub(r"</?(?:p|div|br|ul|ol|li|h[1-6])(?:\s[^>]*)?/?>", "\n", text, flags=re.I)
|
|
|
|
|
text = re.sub(r"</?(?:a|strong|em|b|i|code|span)(?:\s[^>]*)?>", "", text, flags=re.I)
|
2026-10-21 20:52:21 +02:00
|
|
|
text = html.unescape(text)
|
2025-12-18 07:13:08 +01:00
|
|
|
text = text.replace("\r\n", "\n")
|
|
|
|
|
text = re.sub(r"\n{2,}", "\n", text.strip())
|
2026-08-31 00:22:44 +02:00
|
|
|
lines = [
|
|
|
|
|
re.sub(r"^#{1,6}\s+", "", line.lstrip("- ")).rstrip()
|
|
|
|
|
for line in text.split("\n")
|
|
|
|
|
]
|
2025-12-18 07:13:08 +01:00
|
|
|
return "\n".join(line for line in lines if line)
|
2025-12-17 13:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-09-01 05:24:39 +02:00
|
|
|
SECTION_HEADERS = ("### Description", "### POC", "#### Reference", "#### Github",
|
2026-09-01 19:01:07 +02:00
|
|
|
"#### Nuclei", "#### ExploitDB", "#### Metasploit", "#### Vulhub",
|
|
|
|
|
"#### Collections")
|
2026-09-01 05:24:39 +02:00
|
|
|
# Sections whose links are kept apart from the repository list, with the
|
|
|
|
|
# field each becomes in the published index.
|
2026-09-01 19:01:07 +02:00
|
|
|
CURATED = (
|
|
|
|
|
("#### Nuclei", "nuclei"),
|
|
|
|
|
("#### ExploitDB", "edb"),
|
|
|
|
|
("#### Metasploit", "msf"),
|
|
|
|
|
("#### Vulhub", "vulhub"),
|
|
|
|
|
("#### Collections", "collections"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
|
|
|
def load_metadata() -> Dict[str, object]:
|
|
|
|
|
"""Canonical NVD metadata assembled from the tracked year shards."""
|
|
|
|
|
entries: Dict[str, object] = {}
|
|
|
|
|
if not METADATA_INPUT.exists():
|
|
|
|
|
return entries
|
|
|
|
|
for path in sorted(METADATA_INPUT.glob("[12][0-9][0-9][0-9].json")):
|
|
|
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
if not isinstance(payload, dict):
|
|
|
|
|
raise ValueError(f"{path} is not a CVE metadata object")
|
|
|
|
|
entries.update(payload)
|
|
|
|
|
return entries
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
|
|
|
def load_verified_references() -> set[tuple[str, str]]:
|
2026-10-07 01:23:59 +02:00
|
|
|
"""Independent CVE Program and reviewed-content reference evidence."""
|
2026-09-01 19:01:07 +02:00
|
|
|
try:
|
|
|
|
|
lines = VERIFIED_REFERENCES.read_text(encoding="utf-8").splitlines()
|
|
|
|
|
except OSError:
|
2026-10-07 01:23:59 +02:00
|
|
|
lines = []
|
2026-09-01 19:01:07 +02:00
|
|
|
verified: set[tuple[str, str]] = set()
|
|
|
|
|
for line in lines:
|
|
|
|
|
if " - " not in line:
|
|
|
|
|
continue
|
|
|
|
|
cve_id, url = line.split(" - ", 1)
|
|
|
|
|
if cve_id and url:
|
|
|
|
|
verified.add((cve_id, link_key(url)))
|
2026-10-07 01:23:59 +02:00
|
|
|
if REVIEWED_REFERENCES.exists():
|
|
|
|
|
reviewed = json.loads(REVIEWED_REFERENCES.read_text(encoding="utf-8"))
|
|
|
|
|
if not isinstance(reviewed, dict) or any(not isinstance(urls, list) for urls in reviewed.values()):
|
|
|
|
|
raise ValueError("Invalid reviewed reference ledger")
|
|
|
|
|
verified.update((cve_id, link_key(url)) for cve_id, urls in reviewed.items() for url in urls)
|
2026-09-01 19:01:07 +02:00
|
|
|
return verified
|
2026-08-31 00:22:44 +02:00
|
|
|
|
|
|
|
|
|
2026-09-04 02:32:22 +02:00
|
|
|
BADGE = re.compile(r"!\[\]\(https://img\.shields\.io/static/v1\?label=(Vendor|Product)&message=([^&)]*)&")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_preamble(content: str) -> tuple[str, List[str], List[str]]:
|
|
|
|
|
"""The record's title and its vendor and product badges, from above the description."""
|
|
|
|
|
title = ""
|
|
|
|
|
vendors: List[str] = []
|
|
|
|
|
products: List[str] = []
|
|
|
|
|
for line in content.splitlines():
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
if stripped == "### Description":
|
|
|
|
|
break
|
|
|
|
|
if not title and len(stripped) > 4 and stripped.startswith("**") and stripped.endswith("**"):
|
|
|
|
|
title = " ".join(stripped[2:-2].split())
|
|
|
|
|
continue
|
|
|
|
|
match = BADGE.match(stripped)
|
|
|
|
|
if not match:
|
|
|
|
|
continue
|
|
|
|
|
value = " ".join(unquote(match.group(2)).split())
|
|
|
|
|
if value and value.lower() != "n/a":
|
|
|
|
|
(vendors if match.group(1) == "Vendor" else products).append(value)
|
|
|
|
|
return title, vendors, products
|
|
|
|
|
|
|
|
|
|
|
2025-12-18 07:13:08 +01:00
|
|
|
def parse_sections(content: str) -> Dict[str, str]:
|
2026-08-31 00:22:44 +02:00
|
|
|
"""Split an entry on its own headings only.
|
|
|
|
|
|
|
|
|
|
CVE text lifted from a GitHub advisory often opens with "### Impact" or
|
|
|
|
|
"### Summary"; treating any heading as a boundary let a description end
|
|
|
|
|
itself and vanish from the index.
|
|
|
|
|
"""
|
2025-12-18 07:13:08 +01:00
|
|
|
sections: Dict[str, str] = {}
|
|
|
|
|
current_header: Optional[str] = None
|
|
|
|
|
buffer: List[str] = []
|
2025-12-17 13:59:41 +01:00
|
|
|
|
2025-12-18 07:13:08 +01:00
|
|
|
for line in content.splitlines():
|
|
|
|
|
header = line.strip()
|
2026-08-31 00:22:44 +02:00
|
|
|
if header in SECTION_HEADERS:
|
2025-12-18 07:13:08 +01:00
|
|
|
if current_header is not None:
|
|
|
|
|
sections[current_header] = "\n".join(buffer).strip()
|
|
|
|
|
current_header = header
|
|
|
|
|
buffer = []
|
|
|
|
|
else:
|
|
|
|
|
buffer.append(line)
|
2025-12-17 13:59:41 +01:00
|
|
|
|
2025-12-18 07:13:08 +01:00
|
|
|
if current_header is not None:
|
|
|
|
|
sections[current_header] = "\n".join(buffer).strip()
|
|
|
|
|
|
|
|
|
|
return sections
|
2025-12-17 20:54:34 +01:00
|
|
|
|
|
|
|
|
|
2026-09-02 17:17:28 +02:00
|
|
|
GITHUB_HOSTS = {"github.com", "www.github.com"}
|
|
|
|
|
|
|
|
|
|
|
2025-12-18 07:13:08 +01:00
|
|
|
def repo_from_url(url: str) -> str:
|
2026-09-02 17:17:28 +02:00
|
|
|
"""owner/repo for a GitHub URL, matching repoFromUrl in logic.js exactly.
|
|
|
|
|
|
|
|
|
|
Two disagreements with the browser lived here. "github" as a substring of
|
|
|
|
|
the host accepted notgithub.example.com as a repository, and a clone URL
|
|
|
|
|
kept its .git suffix, so poc-rebar3.git and poc-rebar3 counted as two
|
|
|
|
|
sources on the page and one in search. 273 CVEs disagreed across the two.
|
|
|
|
|
"""
|
2025-12-18 07:13:08 +01:00
|
|
|
try:
|
|
|
|
|
parsed = urlparse(url)
|
2026-09-02 17:17:28 +02:00
|
|
|
host = (parsed.hostname or "").lower()
|
|
|
|
|
if host and host not in GITHUB_HOSTS:
|
2025-12-18 07:13:08 +01:00
|
|
|
return ""
|
|
|
|
|
path = parsed.path or url
|
|
|
|
|
except Exception:
|
|
|
|
|
path = url
|
|
|
|
|
parts = path.strip("/").split("/")
|
2026-10-07 01:23:59 +02:00
|
|
|
if (len(parts) < 2 or parts[0].lower() in GITHUB_NON_REPOS
|
|
|
|
|
or [part.lower() for part in parts[2:4]] == ["security", "advisories"]):
|
2026-09-01 22:08:20 +02:00
|
|
|
return ""
|
2026-09-02 17:17:28 +02:00
|
|
|
owner, repo = parts[0].lower(), re.sub(r"\.git$", "", parts[1], flags=re.I).lower()
|
|
|
|
|
return f"{owner}/{repo}" if repo else ""
|
2025-12-18 07:13:08 +01:00
|
|
|
|
|
|
|
|
|
2026-08-29 04:26:56 +02:00
|
|
|
def is_blacklisted(url: str, blacklist: Collection[str]) -> bool:
|
|
|
|
|
full_name = repo_from_url(url)
|
|
|
|
|
if not full_name:
|
2025-12-18 07:13:08 +01:00
|
|
|
return False
|
2026-08-29 04:26:56 +02:00
|
|
|
return full_name in blacklist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def link_key(url: str) -> str:
|
|
|
|
|
parsed = urlparse(url)
|
|
|
|
|
if (parsed.hostname or "").lower() not in {"github.com", "www.github.com"}:
|
2026-08-31 00:22:44 +02:00
|
|
|
return url.rstrip("/")
|
2026-08-29 04:26:56 +02:00
|
|
|
parts = [part for part in parsed.path.split("/") if part]
|
|
|
|
|
if len(parts) < 2:
|
|
|
|
|
return url
|
|
|
|
|
repo = f"{parts[0]}/{parts[1]}".lower()
|
|
|
|
|
suffix = "/" + "/".join(parts[2:]) if len(parts) > 2 else ""
|
|
|
|
|
return f"github:{repo}{suffix}?{parsed.query}#{parsed.fragment}"
|
2025-12-18 07:13:08 +01:00
|
|
|
|
|
|
|
|
|
2026-09-01 22:08:20 +02:00
|
|
|
def source_key(url: str) -> str:
|
|
|
|
|
"""Identity of one source, independent of a path inside its repository."""
|
|
|
|
|
repo = repo_from_url(url)
|
|
|
|
|
if repo and "/" in repo:
|
|
|
|
|
return f"github:{repo}"
|
|
|
|
|
return url.rstrip("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def poc_link_rank(url: str, cve_id: str) -> tuple[int, int]:
|
|
|
|
|
"""Choose the most useful path when one repository supplied several."""
|
|
|
|
|
if not repo_from_url(url):
|
|
|
|
|
return 0, len(url)
|
|
|
|
|
parsed = urlparse(url)
|
|
|
|
|
parts = [part for part in parsed.path.split("/") if part]
|
|
|
|
|
suffix = "/".join(parts[2:]).lower()
|
|
|
|
|
if not suffix:
|
|
|
|
|
return 0, len(url)
|
|
|
|
|
compact_cve = re.sub(r"[^a-z0-9]", "", cve_id.lower())
|
|
|
|
|
compact_path = re.sub(r"[^a-z0-9]", "", suffix)
|
|
|
|
|
if compact_cve and compact_cve in compact_path:
|
|
|
|
|
return 1, len(url)
|
|
|
|
|
extension = Path(parts[-1]).suffix.lower()
|
|
|
|
|
if extension in CODE_SUFFIXES or "/security/advisories/" in parsed.path.lower():
|
|
|
|
|
return 2, len(url)
|
|
|
|
|
if re.search(r"(?:^|/)(?:poc|exploit|reproducer)(?:/|$)", suffix):
|
|
|
|
|
return 3, len(url)
|
|
|
|
|
if extension in MEDIA_SUFFIXES:
|
|
|
|
|
return 6, len(url)
|
|
|
|
|
return 4, len(url)
|
|
|
|
|
|
|
|
|
|
|
2026-10-07 01:39:02 +02:00
|
|
|
@lru_cache(maxsize=1)
|
|
|
|
|
def reviewed_artifact_keys() -> Dict[str, set[str]]:
|
|
|
|
|
from source_artifacts import ArtifactEvidence, load_reviews
|
|
|
|
|
result: Dict[str, set[str]] = {}
|
|
|
|
|
for row in load_reviews():
|
|
|
|
|
artifact = ArtifactEvidence(row["cve"], row["repository"], row["path"], row["revision"],
|
|
|
|
|
row["sha256"], row["id"])
|
|
|
|
|
result.setdefault(row["cve"], set()).add(link_key(artifact.url))
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2026-10-21 18:31:27 +02:00
|
|
|
@lru_cache(maxsize=1)
|
|
|
|
|
def load_source_exclusions() -> set[tuple[str, str]]:
|
|
|
|
|
from source_artifacts import excluded_pairs
|
|
|
|
|
return excluded_pairs()
|
|
|
|
|
|
|
|
|
|
|
2026-10-06 23:49:47 +02:00
|
|
|
def dedupe_source_links(urls: Iterable[str], cve_id: str, *, preserve_paths: bool = False) -> List[str]:
|
2026-09-01 22:08:20 +02:00
|
|
|
selected: Dict[str, tuple[int, str]] = {}
|
|
|
|
|
order: Dict[str, int] = {}
|
2026-10-07 01:39:02 +02:00
|
|
|
reviewed = reviewed_artifact_keys().get(cve_id, set()) if not preserve_paths else set()
|
|
|
|
|
|
|
|
|
|
def rank(url: str) -> tuple:
|
|
|
|
|
if link_key(url) in reviewed:
|
|
|
|
|
return (-1, poc_link_rank(url, cve_id), link_key(url))
|
|
|
|
|
return (0, poc_link_rank(url, cve_id), "")
|
|
|
|
|
|
2026-09-01 22:08:20 +02:00
|
|
|
for index, url in enumerate(urls):
|
2026-10-21 18:31:27 +02:00
|
|
|
if (cve_id, repo_from_url(url)) in load_source_exclusions():
|
|
|
|
|
continue
|
2026-10-06 23:49:47 +02:00
|
|
|
key = link_key(url) if preserve_paths else source_key(url)
|
2026-09-01 22:08:20 +02:00
|
|
|
order.setdefault(key, index)
|
|
|
|
|
current = selected.get(key)
|
2026-10-07 01:39:02 +02:00
|
|
|
if current is None or rank(url) < rank(current[1]):
|
2026-09-01 22:08:20 +02:00
|
|
|
selected[key] = (index, url)
|
|
|
|
|
return [selected[key][1] for key in sorted(selected, key=order.get)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def advisory_rank(row: list, cve_id: str) -> tuple[int, int, int]:
|
|
|
|
|
url = str(row[0])
|
|
|
|
|
tags = set(row[1] if len(row) > 1 and isinstance(row[1], list) else [])
|
|
|
|
|
if "GitHub Advisory" in tags or "/security/advisories/" in url.lower():
|
|
|
|
|
tag_rank = 0
|
|
|
|
|
elif "Exploit" in tags:
|
|
|
|
|
tag_rank = 1
|
|
|
|
|
elif "Vendor Advisory" in tags:
|
|
|
|
|
tag_rank = 2
|
|
|
|
|
elif "Third Party Advisory" in tags or "Advisory" in tags:
|
|
|
|
|
tag_rank = 3
|
|
|
|
|
elif "Issue Tracking" in tags:
|
|
|
|
|
tag_rank = 4
|
|
|
|
|
elif "Patch" in tags:
|
|
|
|
|
tag_rank = 5
|
|
|
|
|
else:
|
|
|
|
|
tag_rank = 6
|
|
|
|
|
path_rank, length = poc_link_rank(url, cve_id)
|
|
|
|
|
return tag_rank, path_rank, length
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dedupe_advisories(rows: Iterable[list], cve_id: str) -> List[list]:
|
|
|
|
|
selected: Dict[str, list] = {}
|
|
|
|
|
order: Dict[str, int] = {}
|
|
|
|
|
for index, row in enumerate(rows):
|
|
|
|
|
if not isinstance(row, list) or not row:
|
|
|
|
|
continue
|
2026-10-07 01:39:02 +02:00
|
|
|
if (cve_id, link_key(str(row[0]))) in load_reference_exclusions():
|
|
|
|
|
continue
|
2026-09-01 22:08:20 +02:00
|
|
|
key = source_key(str(row[0]))
|
|
|
|
|
order.setdefault(key, index)
|
|
|
|
|
current = selected.get(key)
|
|
|
|
|
if current is None or advisory_rank(row, cve_id) < advisory_rank(current, cve_id):
|
|
|
|
|
selected[key] = row
|
|
|
|
|
return [selected[key] for key in sorted(selected, key=order.get)]
|
|
|
|
|
|
|
|
|
|
|
2026-09-01 23:16:51 +02:00
|
|
|
def reference_is_verified_poc(
|
|
|
|
|
cve_id: str,
|
|
|
|
|
url: str,
|
|
|
|
|
metadata: Dict[str, object],
|
|
|
|
|
verified: Collection[tuple[str, str]],
|
|
|
|
|
) -> bool:
|
|
|
|
|
key = link_key(url)
|
2026-10-07 01:23:59 +02:00
|
|
|
if (cve_id, key) in load_reference_exclusions():
|
|
|
|
|
return False
|
2026-09-01 23:16:51 +02:00
|
|
|
if (cve_id, key) in verified:
|
|
|
|
|
return True
|
2026-10-07 01:23:59 +02:00
|
|
|
if (cve_id, key) in load_managed_references():
|
|
|
|
|
return False
|
2026-09-01 23:16:51 +02:00
|
|
|
for row in (metadata.get(cve_id) or {}).get("advisories", []):
|
|
|
|
|
if not isinstance(row, list) or not row:
|
|
|
|
|
continue
|
|
|
|
|
tags = row[1] if len(row) > 1 and isinstance(row[1], list) else []
|
|
|
|
|
if "Exploit" in tags and link_key(str(row[0])) == key:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2026-10-07 01:23:59 +02:00
|
|
|
@lru_cache(maxsize=1)
|
|
|
|
|
def load_reference_exclusions() -> set[tuple[str, str]]:
|
|
|
|
|
import sync_reference_pocs
|
|
|
|
|
return sync_reference_pocs.load_exclusions()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
|
|
|
def load_managed_references() -> set[tuple[str, str]]:
|
|
|
|
|
import sync_reference_pocs
|
|
|
|
|
return {(row["cve"], link_key(row["url"])) for row in sync_reference_pocs.load_reviews()}
|
|
|
|
|
|
|
|
|
|
|
2026-08-29 04:26:56 +02:00
|
|
|
def collect_links(block: str, *, blacklist: Optional[Collection[str]] = None) -> List[str]:
|
2025-12-18 07:13:08 +01:00
|
|
|
links: List[str] = []
|
2026-08-29 04:26:56 +02:00
|
|
|
blacklist = blacklist or set()
|
2025-12-18 07:13:08 +01:00
|
|
|
for raw in block.splitlines():
|
|
|
|
|
entry = raw.strip()
|
|
|
|
|
if not entry or "No PoCs" in entry:
|
2025-12-17 21:06:33 +01:00
|
|
|
continue
|
2025-12-18 07:13:08 +01:00
|
|
|
if entry.startswith("- "):
|
|
|
|
|
entry = entry[2:].strip()
|
|
|
|
|
if not entry:
|
2025-12-17 21:06:33 +01:00
|
|
|
continue
|
2026-10-06 22:39:15 +02:00
|
|
|
try:
|
|
|
|
|
parsed = urlparse(entry)
|
|
|
|
|
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
|
|
|
continue
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
2026-10-22 13:26:22 +02:00
|
|
|
if is_blacklisted(entry, blacklist) or link_health.unavailable(entry):
|
2025-12-17 21:14:24 +01:00
|
|
|
continue
|
2025-12-18 07:13:08 +01:00
|
|
|
if entry not in links:
|
|
|
|
|
links.append(entry)
|
|
|
|
|
return links
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 21:53:19 +02:00
|
|
|
def build_cve_list(blacklist: Collection[str]) -> tuple[List[Dict[str, object]], int]:
|
2026-10-22 19:05:59 +02:00
|
|
|
import archive_pocs
|
|
|
|
|
archived = archive_pocs.archived_fallbacks(blacklist=blacklist, exclusions=load_source_exclusions())
|
2025-12-18 07:13:08 +01:00
|
|
|
cve_entries = []
|
2026-09-01 13:44:47 +02:00
|
|
|
dates = load_dates()
|
2026-09-01 19:01:07 +02:00
|
|
|
metadata = load_metadata()
|
|
|
|
|
verified_references = load_verified_references()
|
2026-08-30 21:53:19 +02:00
|
|
|
total = 0
|
2025-12-18 07:13:08 +01:00
|
|
|
|
2026-09-01 14:43:06 +02:00
|
|
|
for md_path in sorted(CVES.glob("[12][0-9][0-9][0-9]/CVE-*.md")):
|
2026-09-01 19:01:07 +02:00
|
|
|
cve_id = md_path.stem
|
|
|
|
|
if (metadata.get(cve_id) or {}).get("rejected"):
|
|
|
|
|
continue
|
2026-08-30 21:53:19 +02:00
|
|
|
total += 1
|
2025-12-18 07:13:08 +01:00
|
|
|
content = md_path.read_text(encoding="utf-8")
|
2026-10-21 16:55:01 +02:00
|
|
|
record_status = markdown_record_status(content)
|
2026-10-21 18:31:27 +02:00
|
|
|
if record_status in {"REJECTED", "ABSENT"}:
|
2026-10-21 16:55:01 +02:00
|
|
|
total -= 1
|
|
|
|
|
continue
|
2025-12-18 07:13:08 +01:00
|
|
|
sections = parse_sections(content)
|
|
|
|
|
description = normalise_block(sections.get("### Description", ""))
|
2026-08-29 04:26:56 +02:00
|
|
|
references = collect_links(sections.get("#### Reference", ""), blacklist=blacklist)
|
|
|
|
|
github_links = collect_links(sections.get("#### Github", ""), blacklist=blacklist)
|
2026-10-21 20:15:22 +02:00
|
|
|
# Keep artifact paths and their source attribution. Repository metadata
|
|
|
|
|
# describes the hosting project, not the individual artifact.
|
2026-09-01 05:24:39 +02:00
|
|
|
curated = {
|
2026-09-01 22:08:20 +02:00
|
|
|
field: dedupe_source_links(
|
|
|
|
|
collect_links(sections.get(header, ""), blacklist=blacklist),
|
|
|
|
|
cve_id,
|
2026-10-06 23:49:47 +02:00
|
|
|
preserve_paths=True,
|
2026-09-01 22:08:20 +02:00
|
|
|
)
|
2026-09-01 05:24:39 +02:00
|
|
|
for header, field in CURATED
|
|
|
|
|
}
|
2026-09-01 19:01:07 +02:00
|
|
|
curated_keys = {
|
2026-09-01 23:16:51 +02:00
|
|
|
source_key(url)
|
2026-09-01 19:01:07 +02:00
|
|
|
for links in curated.values()
|
|
|
|
|
for url in links
|
|
|
|
|
}
|
2026-09-01 22:08:20 +02:00
|
|
|
poc_candidates: List[str] = []
|
2026-09-01 19:01:07 +02:00
|
|
|
for link in references:
|
2026-09-01 23:16:51 +02:00
|
|
|
key = source_key(link)
|
|
|
|
|
if (
|
|
|
|
|
key not in curated_keys
|
|
|
|
|
and reference_is_verified_poc(cve_id, link, metadata, verified_references)
|
|
|
|
|
):
|
2026-09-01 22:08:20 +02:00
|
|
|
poc_candidates.append(link)
|
2026-09-01 19:01:07 +02:00
|
|
|
for link in github_links:
|
2026-09-01 23:16:51 +02:00
|
|
|
if source_key(link) not in curated_keys:
|
2026-09-01 22:08:20 +02:00
|
|
|
poc_candidates.append(link)
|
|
|
|
|
poc_entries = dedupe_source_links(poc_candidates, cve_id)
|
2026-10-22 19:05:59 +02:00
|
|
|
for field, url in archived.get(cve_id, []):
|
|
|
|
|
original = original_artifact(url)
|
|
|
|
|
if original in poc_entries or any(original in links for links in curated.values()):
|
|
|
|
|
continue
|
|
|
|
|
destination = curated.get(field, poc_entries)
|
|
|
|
|
if url not in destination:
|
|
|
|
|
destination.append(url)
|
2025-12-18 07:13:08 +01:00
|
|
|
|
2026-09-01 05:24:39 +02:00
|
|
|
if not poc_entries and not any(curated.values()):
|
2025-12-17 20:54:34 +01:00
|
|
|
continue
|
2025-12-18 07:13:08 +01:00
|
|
|
|
2026-09-01 05:11:31 +02:00
|
|
|
entry = {
|
2025-12-18 07:13:08 +01:00
|
|
|
"cve": cve_id,
|
|
|
|
|
"desc": description,
|
|
|
|
|
"poc": poc_entries,
|
2026-09-01 05:11:31 +02:00
|
|
|
}
|
2026-10-21 16:55:01 +02:00
|
|
|
if record_status != "PUBLISHED":
|
|
|
|
|
entry["metadata_pending"] = True
|
2026-09-04 02:32:22 +02:00
|
|
|
title, vendors, products = parse_preamble(content)
|
|
|
|
|
if title:
|
|
|
|
|
entry["title"] = title
|
|
|
|
|
if vendors:
|
|
|
|
|
entry["vendor"] = vendors
|
|
|
|
|
if products:
|
|
|
|
|
entry["product"] = products
|
2026-09-01 13:44:47 +02:00
|
|
|
published, modified = (dates.get(cve_id) or ["", ""])[:2]
|
|
|
|
|
if published:
|
|
|
|
|
entry["published"] = published
|
|
|
|
|
if modified and modified != published:
|
|
|
|
|
entry["modified"] = modified
|
2026-09-01 05:24:39 +02:00
|
|
|
entry.update({field: links for field, links in curated.items() if links})
|
2026-09-01 05:11:31 +02:00
|
|
|
cve_entries.append(entry)
|
2025-12-18 07:13:08 +01:00
|
|
|
|
2026-08-30 21:53:19 +02:00
|
|
|
return cve_entries, total
|
2025-12-17 13:59:41 +01:00
|
|
|
|
|
|
|
|
|
2026-10-21 20:52:21 +02:00
|
|
|
def add_observed_dates(cve_entries: list[dict], ledger: dict) -> None:
|
|
|
|
|
"""Date undated CVEs by their earliest retained PoC observation, explicitly."""
|
|
|
|
|
import releases
|
|
|
|
|
for entry in cve_entries:
|
|
|
|
|
if entry.get("published"):
|
|
|
|
|
continue
|
|
|
|
|
stamps = [
|
|
|
|
|
stamp for field in ["poc", *(field for _, field in CURATED)]
|
|
|
|
|
for url in entry.get(field, [])
|
2026-10-22 19:05:59 +02:00
|
|
|
if (stamp := releases.timestamp((ledger.get(releases.key(entry["cve"], original_artifact(url)))
|
|
|
|
|
or ledger.get(releases.key(entry["cve"], url)) or {}).get("seen")))
|
2026-10-21 20:52:21 +02:00
|
|
|
]
|
|
|
|
|
if stamps:
|
|
|
|
|
entry["poc_observed"] = min(stamps).strftime("%Y-%m-%d")
|
|
|
|
|
|
|
|
|
|
|
2026-10-22 19:05:59 +02:00
|
|
|
def original_artifact(url: str) -> str:
|
|
|
|
|
match = re.fullmatch(r'https://web\.archive\.org/web/\d{14}/(https?://.+)', url)
|
|
|
|
|
return match[1] if match else url
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 21:53:19 +02:00
|
|
|
def build_repo_meta(cve_entries: List[Dict[str, object]]) -> Dict[str, object]:
|
|
|
|
|
"""Star count and last-push date for the repositories the index actually links."""
|
2026-10-21 20:15:22 +02:00
|
|
|
stored = json.loads(REPO_META.read_text(encoding="utf-8"))
|
|
|
|
|
if not isinstance(stored, dict) or any(
|
|
|
|
|
not isinstance(row, list) or len(row) < 2 or type(row[0]) is not int or row[0] < 0
|
|
|
|
|
or not isinstance(row[1], str) for row in stored.values()
|
|
|
|
|
):
|
|
|
|
|
raise ValueError("Invalid repository metadata; refusing to publish missing star columns")
|
2026-08-30 21:53:19 +02:00
|
|
|
linked = {
|
2026-10-22 19:05:59 +02:00
|
|
|
repo_from_url(original_artifact(url))
|
2026-08-30 21:53:19 +02:00
|
|
|
for entry in cve_entries
|
2026-10-21 20:15:22 +02:00
|
|
|
for field in ["poc", *(field for _, field in CURATED)]
|
|
|
|
|
for url in entry.get(field, [])
|
2026-10-22 19:05:59 +02:00
|
|
|
if repo_from_url(original_artifact(url))
|
2026-08-30 21:53:19 +02:00
|
|
|
}
|
|
|
|
|
return {name: value for name, value in stored.items() if name in linked}
|
2025-12-18 07:13:08 +01:00
|
|
|
|
|
|
|
|
|
2026-09-01 13:44:47 +02:00
|
|
|
def load_dates() -> Dict[str, list]:
|
|
|
|
|
"""When the CVE Program first published each record and last changed it."""
|
2026-10-21 20:15:22 +02:00
|
|
|
return json.loads(DATES_INPUT.read_text(encoding="utf-8"))
|
2026-09-01 13:44:47 +02:00
|
|
|
|
|
|
|
|
|
2026-08-31 00:13:01 +02:00
|
|
|
def build_kev(cve_entries: List[Dict[str, object]]) -> Dict[str, object]:
|
|
|
|
|
"""CISA's known-exploited entries, limited to CVEs the index carries."""
|
2026-10-21 20:15:22 +02:00
|
|
|
stored = json.loads(KEV_INPUT.read_text(encoding="utf-8"))
|
2026-08-31 00:13:01 +02:00
|
|
|
have = {str(entry["cve"]) for entry in cve_entries}
|
|
|
|
|
return {cve: value for cve, value in stored.items() if cve in have}
|
|
|
|
|
|
|
|
|
|
|
2026-09-01 05:11:31 +02:00
|
|
|
def build_nuclei(cve_entries: List[Dict[str, object]]) -> Dict[str, object]:
|
2026-09-04 00:36:11 +02:00
|
|
|
"""Severity, CVSS and CWE per CVE, limited to the ones indexed."""
|
2026-10-21 20:15:22 +02:00
|
|
|
stored = json.loads(NUCLEI_INPUT.read_text(encoding="utf-8"))
|
2026-09-01 05:11:31 +02:00
|
|
|
have = {str(entry["cve"]) for entry in cve_entries}
|
2026-09-04 00:36:11 +02:00
|
|
|
keep = ("severity", "cvss", "cvss_vector", "cwe")
|
2026-09-01 05:11:31 +02:00
|
|
|
return {
|
|
|
|
|
cve: {field: value[field] for field in keep if field in value}
|
|
|
|
|
for cve, value in stored.items()
|
|
|
|
|
if cve in have
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-09-01 19:01:07 +02:00
|
|
|
def build_metadata(cve_entries: List[Dict[str, object]]) -> Dict[str, object]:
|
|
|
|
|
"""CVSS generations and vetted advisories for CVEs published on the site."""
|
|
|
|
|
have = {str(entry["cve"]) for entry in cve_entries}
|
2026-09-01 22:08:20 +02:00
|
|
|
published: Dict[str, object] = {}
|
|
|
|
|
for cve, value in load_metadata().items():
|
|
|
|
|
if cve not in have or not isinstance(value, dict):
|
|
|
|
|
continue
|
|
|
|
|
entry: Dict[str, object] = {}
|
|
|
|
|
if value.get("cvss"):
|
|
|
|
|
entry["cvss"] = value["cvss"]
|
2026-10-22 13:26:22 +02:00
|
|
|
advisories = [row for row in dedupe_advisories(value.get("advisories", []), cve)
|
|
|
|
|
if not link_health.unavailable(row[0])]
|
2026-09-01 22:08:20 +02:00
|
|
|
if advisories:
|
|
|
|
|
entry["advisories"] = advisories
|
|
|
|
|
if entry:
|
|
|
|
|
published[cve] = entry
|
|
|
|
|
return published
|
2026-09-01 19:01:07 +02:00
|
|
|
|
|
|
|
|
|
2026-09-01 05:24:39 +02:00
|
|
|
def build_epss(cve_entries: List[Dict[str, object]]) -> Dict[str, object]:
|
|
|
|
|
"""FIRST's exploitation probability, limited to the CVEs indexed.
|
|
|
|
|
|
|
|
|
|
EPSS answers the question the PoC count cannot: of everything with exploit
|
|
|
|
|
code published, which is actually being used. The feed covers essentially
|
|
|
|
|
the whole index, so it is fetched here rather than tracked in the
|
|
|
|
|
repository, where a daily rescore of 366,000 rows would be pure churn.
|
2026-09-02 17:17:28 +02:00
|
|
|
|
|
|
|
|
A failure raises rather than returning {}. Swallowing it published an empty
|
|
|
|
|
epss.json, which is not "EPSS is missing" to a reader: every score silently
|
|
|
|
|
disappears from 82,000 pages and the site looks like it never had them. A
|
|
|
|
|
build that cannot get the feed should not replace the one that did.
|
2026-09-01 05:24:39 +02:00
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(EPSS_FEED, timeout=180) as response:
|
|
|
|
|
raw = gzip.decompress(response.read()).decode("utf-8", "replace")
|
|
|
|
|
except Exception as problem:
|
2026-09-02 17:17:28 +02:00
|
|
|
raise RuntimeError(f"EPSS feed unavailable: {problem}") from problem
|
2026-09-01 05:24:39 +02:00
|
|
|
have = {str(entry["cve"]) for entry in cve_entries}
|
|
|
|
|
text = io.StringIO(raw)
|
|
|
|
|
text.readline() # the feed opens with a model version comment, not a header
|
|
|
|
|
scores: Dict[str, object] = {}
|
|
|
|
|
for row in csv.DictReader(text):
|
|
|
|
|
cve = (row.get("cve") or "").strip().upper()
|
|
|
|
|
if cve not in have:
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
scores[cve] = [round(float(row["epss"]), 5), round(float(row["percentile"]), 4)]
|
|
|
|
|
except (KeyError, TypeError, ValueError):
|
|
|
|
|
continue
|
2026-09-02 17:17:28 +02:00
|
|
|
# The feed normally covers the whole index. A truncated or reshaped one
|
|
|
|
|
# parses cleanly and yields a handful of rows, which would ship as a real
|
|
|
|
|
# build; anything below nine in ten CVEs is a broken feed, not news.
|
|
|
|
|
if have and len(scores) < 0.9 * len(have):
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"EPSS feed covered {len(scores):,} of {len(have):,} indexed CVEs; "
|
|
|
|
|
"refusing to publish a partial rescore"
|
|
|
|
|
)
|
2026-09-01 05:24:39 +02:00
|
|
|
return scores
|
|
|
|
|
|
|
|
|
|
|
2026-08-30 21:53:19 +02:00
|
|
|
def build_trending(blacklist: Collection[str]) -> List[Dict[str, object]]:
|
|
|
|
|
"""The trending job's own output, minus anything the blacklist covers."""
|
2026-10-21 20:15:22 +02:00
|
|
|
rows = json.loads(TRENDING_INPUT.read_text(encoding="utf-8"))
|
2026-08-30 21:53:19 +02:00
|
|
|
return [
|
|
|
|
|
row for row in rows
|
|
|
|
|
if row.get("url") and not is_blacklisted(str(row["url"]), blacklist)
|
2026-10-22 13:26:22 +02:00
|
|
|
and not link_health.unavailable(str(row["url"]))
|
2026-10-21 18:31:27 +02:00
|
|
|
and (row.get("cve"), repo_from_url(str(row["url"]))) not in load_source_exclusions()
|
2026-08-30 21:53:19 +02:00
|
|
|
]
|
2025-12-18 07:13:08 +01:00
|
|
|
|
2025-12-17 13:59:41 +01:00
|
|
|
|
2026-10-21 20:15:22 +02:00
|
|
|
def trending_score(row: dict, *, now: datetime | None = None) -> float:
|
|
|
|
|
import releases
|
|
|
|
|
stamp = releases.timestamp(row.get("released") or row.get("pushed"))
|
|
|
|
|
if stamp is None or row.get("shared_repository"):
|
|
|
|
|
return 0.0
|
|
|
|
|
hours = max(0, ((now or datetime.now(timezone.utc)) - stamp).total_seconds() / 3600)
|
|
|
|
|
return int(row.get("stars") or 0) / (hours + 24) ** 0.7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def enrich_trending(rows: list[dict], repo_meta: dict, kev: dict, indexed: set[str]) -> list[dict]:
|
|
|
|
|
items = []
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
for original in rows:
|
|
|
|
|
if not original.get("trending", True) or original.get("cve") not in indexed:
|
|
|
|
|
continue
|
|
|
|
|
item = dict(original)
|
|
|
|
|
item["page"] = f"/{item['cve']}"
|
|
|
|
|
item["kev"] = item["cve"] in kev
|
|
|
|
|
repository = repo_from_url(item["url"])
|
|
|
|
|
if repository in repo_meta:
|
|
|
|
|
item["stars"] = repo_meta[repository][0]
|
|
|
|
|
item["score"] = trending_score(item, now=now)
|
|
|
|
|
items.append(item)
|
|
|
|
|
return sorted(items, key=lambda row: (-row["score"], row["url"].lower()))
|
|
|
|
|
|
|
|
|
|
|
2026-10-07 01:23:59 +02:00
|
|
|
def build_landed(cve_payload: list[dict], repo_meta: dict, kev: dict,
|
|
|
|
|
ledger: dict | None = None, *, now: str | None = None, candidates: list[dict] | None = None) -> list[dict]:
|
|
|
|
|
import releases
|
|
|
|
|
ledger = releases.load_ledger() if ledger is None else ledger
|
|
|
|
|
entries = {entry["cve"]: entry for entry in cve_payload}
|
|
|
|
|
published = {releases.key(cve, url) for cve, url in releases.published_pairs(cve_payload)}
|
|
|
|
|
candidates = {releases.key(row["cve"], row["url"]): row for row in candidates or []}
|
|
|
|
|
rows = []
|
|
|
|
|
for artifact in releases.landed(ledger, now=now):
|
|
|
|
|
cve, url = artifact["cve"], artifact["url"]
|
|
|
|
|
if releases.key(cve, url) not in published:
|
|
|
|
|
continue
|
|
|
|
|
parsed = urlparse(url)
|
|
|
|
|
parts = unquote(parsed.path).strip("/").split("/")
|
|
|
|
|
repository = repo_from_url(url)
|
|
|
|
|
root_link = parsed.hostname == "github.com" and len(parts) == 2
|
2026-10-21 20:15:22 +02:00
|
|
|
meta = repo_meta.get(repository or "")
|
2026-10-07 01:23:59 +02:00
|
|
|
current = candidates.get(releases.key(cve, url), {})
|
2026-10-21 20:15:22 +02:00
|
|
|
stars = meta[0] if meta else current.get("stars") if repository else None
|
2026-10-07 01:23:59 +02:00
|
|
|
rows.append({
|
|
|
|
|
"year": int(cve.split("-")[1]), "cve": cve, "url": url,
|
|
|
|
|
"name": parts[-1] or cve, "stars": stars,
|
2026-10-21 20:15:22 +02:00
|
|
|
"desc": current.get("desc") or entries[cve].get("desc", ""), "released": artifact["released"],
|
2026-10-07 01:23:59 +02:00
|
|
|
"basis": artifact["basis"], "kev": cve in kev, "page": f"/{cve}",
|
|
|
|
|
"landed": True, "source": parsed.hostname, "artifact": not root_link,
|
|
|
|
|
})
|
|
|
|
|
return rows[:100]
|
|
|
|
|
|
|
|
|
|
|
2025-12-18 07:13:08 +01:00
|
|
|
def write_json(path: Path, data, *, indent: Optional[int] = None) -> None:
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
with path.open("w", encoding="utf-8") as handle:
|
2026-08-30 21:53:19 +02:00
|
|
|
json.dump(data, handle, ensure_ascii=False, indent=indent,
|
|
|
|
|
separators=None if indent else (",", ":"))
|
2025-12-17 13:59:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
2026-10-21 20:15:22 +02:00
|
|
|
parser = argparse.ArgumentParser(description="Build the CVE PoC site data files")
|
|
|
|
|
parser.add_argument("--offline", action="store_true", help="Validate tracked inputs without network or output writes")
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
if args.offline:
|
|
|
|
|
entries, total = build_cve_list(load_blacklist())
|
|
|
|
|
build_repo_meta(entries)
|
|
|
|
|
build_kev(entries)
|
|
|
|
|
build_nuclei(entries)
|
|
|
|
|
build_metadata(entries)
|
|
|
|
|
build_trending(load_blacklist())
|
|
|
|
|
print(f"Verified tracked data: {len(entries)} of {total} CVEs have public links")
|
|
|
|
|
return 0
|
2025-12-17 13:59:41 +01:00
|
|
|
|
2026-08-30 21:09:37 +02:00
|
|
|
DOCS_DIR.mkdir(parents=True, exist_ok=True)
|
2025-12-18 07:13:08 +01:00
|
|
|
blacklist = load_blacklist()
|
|
|
|
|
|
2026-10-21 20:52:21 +02:00
|
|
|
import releases
|
|
|
|
|
ledger = releases.load_ledger()
|
2026-08-30 21:53:19 +02:00
|
|
|
cve_payload, total_cves = build_cve_list(blacklist)
|
2026-10-21 20:52:21 +02:00
|
|
|
add_observed_dates(cve_payload, ledger)
|
2026-08-29 04:26:56 +02:00
|
|
|
write_json(CVE_OUTPUT, cve_payload)
|
2025-12-18 07:13:08 +01:00
|
|
|
|
2026-10-22 19:05:59 +02:00
|
|
|
import archive_pocs
|
|
|
|
|
archive_rows = archive_pocs.load_records()
|
|
|
|
|
allowed = {archive_pocs.identity(url) for entry in cve_payload
|
|
|
|
|
for field in ['poc', *(f for _, f in CURATED)] for url in entry.get(field, [])}
|
|
|
|
|
# Fallback entries point into Wayback; retain the original-to-capture map.
|
|
|
|
|
allowed.update(key for key, row in archive_rows.items()
|
|
|
|
|
if archive_pocs.valid_snapshot(row) and archive_pocs.identity(row['snapshot']['url']) in allowed)
|
|
|
|
|
snapshots = archive_pocs.published_snapshots(archive_rows, allowed)
|
|
|
|
|
aliases = {archive_pocs.identity(url): value for url, value in snapshots.items()}
|
|
|
|
|
for entry in cve_payload:
|
|
|
|
|
for field in ['poc', *(field for _, field in CURATED)]:
|
|
|
|
|
for url in entry.get(field, []):
|
|
|
|
|
if value := aliases.get(archive_pocs.identity(url)):
|
|
|
|
|
snapshots[url] = value
|
|
|
|
|
write_json(DOCS_DIR / 'archives.json', snapshots)
|
|
|
|
|
|
2026-08-30 21:53:19 +02:00
|
|
|
repo_meta = build_repo_meta(cve_payload)
|
|
|
|
|
write_json(REPO_META_OUTPUT, repo_meta)
|
|
|
|
|
|
2026-08-31 00:13:01 +02:00
|
|
|
kev = build_kev(cve_payload)
|
|
|
|
|
write_json(KEV_OUTPUT, kev)
|
|
|
|
|
|
2026-09-01 05:11:31 +02:00
|
|
|
nuclei = build_nuclei(cve_payload)
|
|
|
|
|
write_json(NUCLEI_OUTPUT, nuclei)
|
|
|
|
|
|
2026-09-01 19:01:07 +02:00
|
|
|
metadata = build_metadata(cve_payload)
|
|
|
|
|
write_json(METADATA_OUTPUT, metadata)
|
2026-09-04 02:32:22 +02:00
|
|
|
write_json(CVSS_OUTPUT, {cve: value["cvss"] for cve, value in metadata.items() if value.get("cvss")})
|
|
|
|
|
write_json(ADVISORIES_OUTPUT, {cve: value["advisories"] for cve, value in metadata.items() if value.get("advisories")})
|
2026-09-01 19:01:07 +02:00
|
|
|
|
2026-09-01 05:24:39 +02:00
|
|
|
epss = build_epss(cve_payload)
|
|
|
|
|
write_json(EPSS_OUTPUT, epss)
|
|
|
|
|
|
2026-10-06 21:51:21 +02:00
|
|
|
indexed = {entry["cve"] for entry in cve_payload}
|
2026-10-07 01:23:59 +02:00
|
|
|
frontpage_candidates = build_trending(blacklist)
|
2026-10-21 20:15:22 +02:00
|
|
|
trending_items = enrich_trending(frontpage_candidates, repo_meta, kev, indexed)
|
2026-10-21 20:52:21 +02:00
|
|
|
landed_items = build_landed(cve_payload, repo_meta, kev, ledger, candidates=frontpage_candidates)
|
2025-12-18 07:13:08 +01:00
|
|
|
write_json(
|
|
|
|
|
TRENDING_OUTPUT,
|
|
|
|
|
{
|
|
|
|
|
"generated": datetime.now(timezone.utc).isoformat(),
|
2026-08-30 21:53:19 +02:00
|
|
|
"total_cves": total_cves,
|
|
|
|
|
"with_pocs": len(cve_payload),
|
2025-12-18 07:13:08 +01:00
|
|
|
"items": trending_items,
|
2026-10-07 01:23:59 +02:00
|
|
|
"landed": landed_items,
|
|
|
|
|
"ranking": "stars_weighted_by_artifact_release_age",
|
2025-12-18 07:13:08 +01:00
|
|
|
},
|
|
|
|
|
indent=2,
|
2025-12-17 18:24:55 +01:00
|
|
|
)
|
2025-12-17 13:59:41 +01:00
|
|
|
|
2026-08-30 21:53:19 +02:00
|
|
|
print(
|
|
|
|
|
f"Wrote {CVE_OUTPUT.name} ({len(cve_payload)} of {total_cves} CVEs), "
|
2026-08-31 00:13:01 +02:00
|
|
|
f"{REPO_META_OUTPUT.name} ({len(repo_meta)} repositories), "
|
2026-09-01 05:11:31 +02:00
|
|
|
f"{KEV_OUTPUT.name} ({len(kev)} known-exploited), "
|
2026-09-01 05:24:39 +02:00
|
|
|
f"{NUCLEI_OUTPUT.name} ({len(nuclei)} rated), "
|
2026-09-01 19:01:07 +02:00
|
|
|
f"{METADATA_OUTPUT.name} ({len(metadata)} enriched), "
|
2026-09-01 05:24:39 +02:00
|
|
|
f"{EPSS_OUTPUT.name} ({len(epss)} scored) and {TRENDING_OUTPUT.name}"
|
2026-08-30 21:53:19 +02:00
|
|
|
)
|
2025-12-17 13:59:41 +01:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|