Files
PEASS-ng/build_lists/update_windows_version_defs.py

118 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Build a compact WinPEAS definitions file from WES-NG definitions.zip."""
from __future__ import annotations
import argparse
import csv
import io
import json
import tempfile
import zipfile
from collections import defaultdict
from pathlib import Path
from urllib.request import urlretrieve
DEFAULT_DEFINITIONS_URL = "https://raw.githubusercontent.com/bitsadmin/wesng/master/definitions.zip"
def _read_csv_from_zip(zip_file: zipfile.ZipFile, prefix: str) -> list[dict]:
target = next((name for name in zip_file.namelist() if name.startswith(prefix)), None)
if not target:
return []
with zip_file.open(target, "r") as raw:
return list(csv.DictReader(io.TextIOWrapper(raw, encoding="utf-8-sig")))
def build_definitions(definitions_zip: Path) -> dict:
with zipfile.ZipFile(definitions_zip, "r") as zf:
cve_file = next(name for name in zf.namelist() if name.startswith("CVEs_"))
generated = cve_file.split("_", 1)[1].split(".", 1)[0]
rows = _read_csv_from_zip(zf, "CVEs_")
rows.extend(_read_csv_from_zip(zf, "Custom_"))
products: dict[str, dict[str, dict[str, str]]] = defaultdict(dict)
kb_supersedes: dict[str, set[str]] = defaultdict(set)
for row in rows:
product = (row.get("AffectedProduct") or "").strip()
if "windows" not in product.lower():
continue
kb = (row.get("BulletinKB") or "").strip()
supersedes = (row.get("Supersedes") or "").strip()
if kb and supersedes:
for item in supersedes.split(";"):
item = item.strip()
if item:
kb_supersedes[kb].add(item)
if not (row.get("Exploits") or "").strip():
continue
if not product:
continue
cve = (row.get("CVE") or "").strip()
vuln_key = cve or f"KB{kb}"
if not vuln_key:
continue
if vuln_key in products[product]:
continue
products[product][vuln_key] = {
"cve": cve,
"kb": kb,
"severity": (row.get("Severity") or "").strip(),
"impact": (row.get("Impact") or "").strip(),
}
data = {"generated": generated, "products": {}, "kb_supersedes": {}}
for product in sorted(products):
entries = [products[product][key] for key in sorted(products[product])]
data["products"][product] = entries
for kb in sorted(kb_supersedes):
data["kb_supersedes"][kb] = sorted(kb_supersedes[kb])
return data
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--definitions-url", default=DEFAULT_DEFINITIONS_URL)
parser.add_argument("--definitions-zip", default="")
parser.add_argument(
"--output",
default=str(Path("build_lists") / "windows_version_exploits.json"),
)
args = parser.parse_args()
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
if args.definitions_zip:
definitions_zip = Path(args.definitions_zip)
if not definitions_zip.exists():
raise FileNotFoundError(f"Definitions zip not found: {definitions_zip}")
else:
with tempfile.TemporaryDirectory(prefix="wesng_defs_") as temp_dir:
temp_zip = Path(temp_dir) / "definitions.zip"
urlretrieve(args.definitions_url, str(temp_zip))
data = build_definitions(temp_zip)
output_path.write_text(json.dumps(data, separators=(",", ":")) + "\n", encoding="utf-8")
if args.definitions_zip:
data = build_definitions(definitions_zip)
output_path.write_text(json.dumps(data, separators=(",", ":")) + "\n", encoding="utf-8")
total_products = len(data["products"])
total_entries = sum(len(v) for v in data["products"].values())
print(
f"Generated {output_path} (date={data['generated']}, products={total_products}, vulnerabilities={total_entries})"
)
if __name__ == "__main__":
main()