File size: 3,133 Bytes
a8943e8 081f1c7 a8943e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | """Render the security addendum's host table from config, so the addendum cannot drift from the code.
The table between the ``host-table`` markers in ``docs/security/addendum-knowledge-specialist.md`` is
generated from ``config/sources.yaml`` (hosts, licences) and ``config/tools_allowlist.yaml`` (which
endpoints call which source). ``tests/test_sources_hosts.py`` already proves every host a loaded tool
can reach is in ``sources.yaml``; this script carries that same list into the document the client's security reviewers read.
python scripts/render_host_table.py # rewrite the block in place
python scripts/render_host_table.py --check # exit 1 if the committed block is stale
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from src import config
ADDENDUM = ROOT / "docs" / "security" / "addendum-knowledge-specialist.md"
START = "<!-- host-table:start (generated by scripts/render_host_table.py; do not edit by hand) -->"
END = "<!-- host-table:end -->"
def endpoints_by_source() -> dict[str, list[str]]:
used: dict[str, set[str]] = {}
for tool in config.allowlist()["tools"].values():
used.setdefault(tool["source"], set()).update(tool["endpoints"])
return {sid: [e for e in config.ENDPOINTS if e in eps] for sid, eps in used.items()}
def render() -> str:
by_source = endpoints_by_source()
rows = [
"| Source | Hosts called | Licence carried on every row | Called in Phase 1 by | Key needed |",
"|---|---|---|---|---|",
]
for sid, src in config.sources().items():
eps = by_source.get(sid) or []
called = ", ".join(f"`{e}`" for e in eps) if eps else "not called (declared ahead of use)"
hosts = ", ".join(f"`{h}`" for h in src["hosts"])
rows.append(f"| {src['name']} | {hosts} | {src['licence']} | {called} | none |")
n_hosts = sum(len(s["hosts"]) for s in config.sources().values())
n_called = sum(len(s["hosts"]) for sid, s in config.sources().items() if by_source.get(sid))
rows.append("")
rows.append(
f"{n_hosts} hosts in all; {n_called} are called by a Phase 1 endpoint. Source of truth: "
"`config/sources.yaml`, checked against the installed ToolUniverse by "
"`tests/test_sources_hosts.py`."
)
return "\n".join([START, *rows, END])
def main(argv: list[str]) -> int:
text = ADDENDUM.read_text(encoding="utf-8")
if START not in text or END not in text:
print(f"{ADDENDUM.name}: host-table markers missing", file=sys.stderr)
return 2
before, rest = text.split(START, 1)
_, after = rest.split(END, 1)
updated = before + render() + after
if "--check" in argv:
if updated != text:
print(
f"{ADDENDUM.name}: host table is stale; run scripts/render_host_table.py",
file=sys.stderr,
)
return 1
return 0
ADDENDUM.write_text(updated, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
|