"""Scrape the CIS AWS Foundations Benchmark page. The page is a mapping table: | Control ID and title (FSBP-style) | CIS v5.0.0 | CIS v3.0.0 | CIS v1.4.0 | CIS v1.2.0 | We emit one CISRow per unified control. The `requirement` field summarises which CIS versions reference it. """ from __future__ import annotations import re from dataclasses import dataclass, field from bs4 import BeautifulSoup from scripts.control_resource_map import control_id_to_types CIS_URL = ( "https://docs.aws.amazon.com/securityhub/latest/userguide/" "cis-aws-foundations-benchmark.html" ) @dataclass class CISRow: control_id: str title: str severity: str requirement: str source_url: str resource_types: list[str] = field(default_factory=list) _TITLE_RE = re.compile(r"^\s*\[(?P[A-Za-z][A-Za-z0-9]*\.\d+)\]\s*(?P.+)$") _VERSION_RE = re.compile(r"CIS\s+v(?P<ver>[\d.]+)", re.IGNORECASE) def _extract_versions(headers: list[str]) -> list[str]: """From header cells, pull out version strings like '5.0.0' for each column. Non-version columns yield empty string.""" versions: list[str] = [] for h in headers: m = _VERSION_RE.search(h) versions.append(m.group("ver") if m else "") return versions def parse_cis_page(html: str, source_url: str) -> list[CISRow]: soup = BeautifulSoup(html, "html.parser") rows: list[CISRow] = [] for table in soup.find_all("table"): header_cells = table.find("tr").find_all(["th", "td"]) if table.find("tr") else [] header_texts = [c.get_text(strip=True) for c in header_cells] versions = _extract_versions(header_texts) if not any(versions): continue for tr in table.find_all("tr")[1:]: cells = tr.find_all("td") if len(cells) < 2: continue title_text = cells[0].get_text(strip=True) m = _TITLE_RE.match(title_text) if not m: continue unified_id = m.group("id") title = m.group("title").strip() version_refs: list[str] = [] for ver, cell in zip(versions[1:], cells[1:]): if not ver: continue num = cell.get_text(strip=True) if num: version_refs.append(f"v{ver} ยง{num}") requirement = "CIS " + ", ".join(version_refs) if version_refs else "" rows.append(CISRow( control_id=f"CIS {unified_id}", title=title, severity="medium", requirement=requirement, source_url=source_url, resource_types=control_id_to_types(f"CIS {unified_id}"), )) return rows