diff --git a/.gitignore b/.gitignore index c0bb182..4fc7791 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ master-age.txt # Python bytecode cache __pycache__/ *.pyc + +# Local API tokens — never committed (advisory-scan / hetzner recovery) +.github-token +.hcloud-token diff --git a/cc-ci-plan/advisory-scan.py b/cc-ci-plan/advisory-scan.py index 485ae16..0494d8b 100755 --- a/cc-ci-plan/advisory-scan.py +++ b/cc-ci-plan/advisory-scan.py @@ -59,6 +59,27 @@ OSV_PACKAGES: dict[str, tuple[str, str]] = { } +def _github_token() -> str | None: + """Read-only GitHub token, for the API rate limit ONLY (60/hr anonymous → 5000/hr with a token). + + Env `GITHUB_TOKEN` wins; otherwise the file at `GITHUB_TOKEN_FILE` (default + /srv/cc-ci/.github-token, chmod 600, never in git). Reading PUBLIC security advisories needs NO + scopes at all — create a classic PAT with every box unticked, or a fine-grained token limited to + "Public repositories: read". Do NOT grant repo/write scopes: this tool only ever GETs advisories. + A missing token is not an error — the scan simply runs anonymously and will report sources as + failed once the 60/hr limit bites, which is visible rather than silent. + """ + tok = os.environ.get("GITHUB_TOKEN") + if tok: + return tok.strip() + path = os.environ.get("GITHUB_TOKEN_FILE", "/srv/cc-ci/.github-token") + try: + with open(path) as f: + return f.read().strip() or None + except OSError: + return None + + def _fetch(url: str, headers: dict | None = None) -> str: h = {"User-Agent": UA, "Accept-Encoding": "gzip"} h.update(headers or {}) @@ -112,7 +133,7 @@ def github_advisories(urls: list[str]) -> list[dict]: seen.add((owner, repo)) api = f"https://api.github.com/repos/{owner}/{repo}/security-advisories?per_page=100" hdrs = {"Accept": "application/vnd.github+json"} - tok = os.environ.get("GITHUB_TOKEN") + tok = _github_token() if tok: hdrs["Authorization"] = f"Bearer {tok}" entry = {"source": f"github-advisories:{owner}/{repo}", "status": "ok", "advisories": []}