mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-27 16:51:03 +00:00
Four gaps, measured against the index before any of this was written. ExploitDB and Metasploit are folded in the way nuclei templates were. ExploitDB covers 25,066 CVEs and adds 30,327 links across 17,237 entries; Metasploit covers 3,178 and adds 3,520 across 2,550. Together with the templates the index goes from 101,809 CVEs carrying a PoC to 103,945. Neither archive skews recent, which is why nuclei went first, but both are the difference between an old CVE having nothing and having something that runs. Each keeps its own section and its own field, and each leads the list ahead of the repositories: a module, an entry and a template all run as they stand, without reading somebody's code. EPSS now covers the index rather than the 4,274 CVEs a template happened to rate. FIRST's daily feed scores 103,783 of 103,945, so a result can say how likely a flaw is to be exploited in the next thirty days, not only that code exists for it. It is fetched during the site build and never tracked here: a daily rescore of 366,000 rows would be nothing but churn. The trending tables also stop hiding what they exist to show. The star floor excluded 1,059 of the 1,312 2026 PoC repositories pushed this quarter, and a repository published this morning has no stars, so the newest row on the front page was six days old. A separate lane drops the floor entirely and pays for it with a ten day window, a cap of ten rows, and the same paperwork check the rest of the table uses. The newest row is now hours old.
141 lines
5.1 KiB
Python
141 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Fold the two curated exploit archives into the index.
|
|
|
|
ExploitDB and Metasploit both publish a single machine readable file mapping
|
|
their entries to CVE ids. Neither is a GitHub repository, so their links are
|
|
kept in their own sections rather than being counted and starred as one.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import gzip
|
|
import io
|
|
import json
|
|
import re
|
|
import urllib.request
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EDB_CSV = "https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv"
|
|
MSF_JSON = ("https://raw.githubusercontent.com/rapid7/metasploit-framework/"
|
|
"master/db/modules_metadata_base.json")
|
|
EDB_ENTRY = "https://www.exploit-db.com/exploits/"
|
|
MSF_BLOB = "https://github.com/rapid7/metasploit-framework/blob/master"
|
|
CVE = re.compile(r"CVE-\d{4}-\d{4,7}", re.I)
|
|
USER_AGENT = "0xMarcio-cve-exploits"
|
|
# One CVE can carry dozens of ExploitDB rows for the same flaw on different
|
|
# platforms. Enough to be useful, not enough to bury the repositories below.
|
|
PER_CVE = 8
|
|
|
|
SOURCES = (
|
|
("#### ExploitDB", "No ExploitDB entry."),
|
|
("#### Metasploit", "No Metasploit module."),
|
|
)
|
|
|
|
|
|
def fetch(url: str) -> bytes:
|
|
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
with urllib.request.urlopen(request, timeout=180) as response:
|
|
raw = response.read()
|
|
return gzip.decompress(raw) if url.endswith(".gz") else raw
|
|
|
|
|
|
def exploitdb() -> dict[str, list[str]]:
|
|
"""CVE to ExploitDB entry links, verified entries first, newest first."""
|
|
text = fetch(EDB_CSV).decode("utf-8", "replace")
|
|
rows: dict[str, list[tuple[int, int]]] = defaultdict(list)
|
|
for row in csv.DictReader(io.StringIO(text)):
|
|
codes = row.get("codes") or ""
|
|
if "CVE-" not in codes.upper():
|
|
continue
|
|
try:
|
|
entry = int(row["id"])
|
|
except (KeyError, TypeError, ValueError):
|
|
continue
|
|
verified = 1 if str(row.get("verified") or "").strip() == "1" else 0
|
|
for cve in {c.upper() for c in CVE.findall(codes)}:
|
|
rows[cve].append((verified, entry))
|
|
return {
|
|
cve: [f"{EDB_ENTRY}{entry}" for _, entry in
|
|
sorted(found, key=lambda item: (-item[0], -item[1]))[:PER_CVE]]
|
|
for cve, found in rows.items()
|
|
}
|
|
|
|
|
|
def metasploit() -> dict[str, list[str]]:
|
|
"""CVE to module source links, the better ranked module first.
|
|
|
|
A module is written to be run, so rank is the useful order here: excellent
|
|
beats manual, whatever order the database happens to list them in.
|
|
"""
|
|
data = json.loads(fetch(MSF_JSON).decode("utf-8", "replace"))
|
|
rows: dict[str, list[tuple[int, str]]] = defaultdict(list)
|
|
for module in data.values():
|
|
path = str(module.get("path") or "")
|
|
if not path.endswith(".rb"):
|
|
continue
|
|
rank = int(module.get("rank") or 0)
|
|
found: set[str] = set()
|
|
for reference in module.get("references") or []:
|
|
found |= {c.upper() for c in CVE.findall(str(reference))}
|
|
for cve in found:
|
|
rows[cve].append((rank, MSF_BLOB + path))
|
|
return {
|
|
cve: [url for _, url in
|
|
sorted(found, key=lambda item: (-item[0], item[1]))[:PER_CVE]]
|
|
for cve, found in rows.items()
|
|
}
|
|
|
|
|
|
def section_bounds(text: str, header: str) -> tuple[int, int] | None:
|
|
start = text.find("\n" + header)
|
|
if start < 0:
|
|
return None
|
|
body = start + 1 + len(header)
|
|
nxt = text.find("\n#", body)
|
|
return body, len(text) if nxt < 0 else nxt
|
|
|
|
|
|
def apply_to_markdown(cve: str, header: str, urls: list[str], dry_run: bool) -> str:
|
|
path = ROOT / cve.split("-")[1] / f"{cve}.md"
|
|
if not path.exists():
|
|
return "absent"
|
|
original = path.read_text(encoding="utf-8")
|
|
bounds = section_bounds(original, header)
|
|
block = "\n" + "\n".join(f"- {url}" for url in urls) + "\n"
|
|
if bounds:
|
|
start, end = bounds
|
|
if original[start:end] == block:
|
|
return "unchanged"
|
|
updated = original[:start] + block + original[end:]
|
|
else:
|
|
updated = original.rstrip("\n") + f"\n\n{header}\n" + block.lstrip("\n")
|
|
if not dry_run:
|
|
path.write_text(updated, encoding="utf-8")
|
|
return "added"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Sync ExploitDB and Metasploit into the index")
|
|
parser.add_argument("--dry-run", action="store_true", help="report without writing")
|
|
args = parser.parse_args()
|
|
|
|
for (header, _), collect in zip(SOURCES, (exploitdb, metasploit)):
|
|
found = collect()
|
|
links = sum(len(urls) for urls in found.values())
|
|
tally = {"added": 0, "unchanged": 0, "absent": 0}
|
|
for cve, urls in sorted(found.items()):
|
|
tally[apply_to_markdown(cve, header, urls, args.dry_run)] += 1
|
|
name = header.replace("#", "").strip()
|
|
print(f"{name}: {len(found):,} CVEs, {links:,} links | "
|
|
f"{tally['added']:,} entries written, {tally['unchanged']:,} unchanged, "
|
|
f"{tally['absent']:,} for CVEs not in the index")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|