"""Shared front-matter reading for the register tools. The finding files are the source of truth. Nothing here writes to them. """ from __future__ import annotations import datetime as dt import pathlib import yaml REPO = pathlib.Path(__file__).resolve().parent.parent FINDINGS = REPO / "findings" NOTES = REPO / "notes" SEVERITIES = ["critical", "high", "medium", "low"] GRADED_FIELDS = ["severity", "disclosure", "escalation", "next_check", "cadence"] # A finding leaves the watch list only by being genuinely finished. Anything # else — including a word the register has not seen before — stays watched, and # the unknown word is reported. A finding must never fall out of the nag because # somebody used a status the tooling did not recognise. CLOSED_STATUSES = ("fixed", "withdrawn") KNOWN_STATUSES = ("open", "accepted", "mitigated") + CLOSED_STATUSES def watched(status: str | None) -> bool: return status not in CLOSED_STATUSES # The adaptive cadence ladder (operator ruling, 2026-08-20). A clean check # climbs one rung; anything wrong drops straight back to `instant`. The rung a # finding sits on is itself the signal: how stable this matter has been. CADENCE = [ ("instant", dt.timedelta(0)), ("1h", dt.timedelta(hours=1)), ("8h", dt.timedelta(hours=8)), ("24h", dt.timedelta(hours=24)), ("48h", dt.timedelta(hours=48)), ("96h", dt.timedelta(hours=96)), ("7d", dt.timedelta(days=7)), ("14d", dt.timedelta(days=14)), ("1mo", dt.timedelta(days=30)), ("1q", dt.timedelta(days=90)), ] CADENCE_NAMES = [name for name, _ in CADENCE] TOP_RUNG = CADENCE_NAMES[-1] def rung_index(name: str) -> int: return CADENCE_NAMES.index(name) if name in CADENCE_NAMES else 0 def interval(name: str) -> dt.timedelta: return CADENCE[rung_index(name)][1] def climb(name: str) -> str: """One clean check: up one rung, never past the quarter.""" return CADENCE_NAMES[min(rung_index(name) + 1, len(CADENCE) - 1)] def reset() -> str: """Anything wrong: back to the bottom.""" return CADENCE_NAMES[0] def load(path: pathlib.Path) -> dict: text = path.read_text(encoding="utf-8") if not text.startswith("---\n"): raise ValueError(f"{path.name}: no front-matter") _, fm, _body = text.split("---\n", 2) data = yaml.safe_load(fm) or {} data["_path"] = path return data def findings() -> list[dict]: items = [load(p) for p in sorted(FINDINGS.glob("RISK-F-*.md"))] return sorted(items, key=lambda f: f["id"], reverse=True) def regulatory() -> list[dict]: """Regulatory records expire, so they ride the same ladder as findings.""" d = REPO / "docs" / "regulatory" if not d.exists(): return [] return sorted((load(p) for p in d.glob("*.md") if p.name != "README.md"), key=lambda r: r.get("id", "")) def notes() -> list[dict]: if not NOTES.exists(): return [] return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True) def moment(value) -> dt.datetime | None: """Parse a date or datetime front-matter value as UTC.""" if isinstance(value, dt.datetime): return value if value.tzinfo else value.replace(tzinfo=dt.timezone.utc) if isinstance(value, dt.date): return dt.datetime.combine(value, dt.time(0, 0), tzinfo=dt.timezone.utc) if isinstance(value, str) and value not in ("", "unset"): return dt.datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(dt.timezone.utc) return None def now() -> dt.datetime: return dt.datetime.now(dt.timezone.utc) def sev_rank(sev: str) -> int: return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)