#!/usr/bin/env python3 """檢查公開前端 env 範例不可洩漏內網拓樸。""" from __future__ import annotations import argparse import re from pathlib import Path PRIVATE_IP_PATTERN = re.compile( r"\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b" ) RETIRED_PUBLIC_TOPOLOGY_KEYS = { "NEXT_PUBLIC_HOST_IPS", "NEXT_PUBLIC_K8S_VIP_INFO", } ENV_EXAMPLE_PATHS = ( Path("apps/web/.env.example"), ) def _active_assignment(line: str) -> tuple[str, str] | None: stripped = line.strip() if not stripped or stripped.startswith("#") or "=" not in stripped: return None key, value = stripped.split("=", 1) return key.strip(), value.strip() def validate(root: Path) -> None: errors: list[str] = [] for relative_path in ENV_EXAMPLE_PATHS: path = root / relative_path if not path.exists(): continue for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): assignment = _active_assignment(line) if assignment is None: continue key, value = assignment if key in RETIRED_PUBLIC_TOPOLOGY_KEYS: errors.append(f"{relative_path}:{line_number}: 已停用公開前端拓樸 env key:{key}") if PRIVATE_IP_PATTERN.search(value): errors.append(f"{relative_path}:{line_number}: env 範例不得包含內網 IP:{key}") if key.startswith("NEXT_PUBLIC_") and not value: errors.append(f"{relative_path}:{line_number}: NEXT_PUBLIC_* 範例需明確使用公網入口或安全預設值:{key}") if errors: raise SystemExit("BLOCKED public frontend env guard:\n" + "\n".join(f"- {error}" for error in errors)) def main() -> None: parser = argparse.ArgumentParser(description="檢查公開前端 env 範例不可洩漏內網拓樸") parser.add_argument("--root", default=".", help="repository root") args = parser.parse_args() validate(Path(args.root).resolve()) print("OK public frontend env guard") if __name__ == "__main__": main()