140 lines
4.0 KiB
Python
140 lines
4.0 KiB
Python
|
|
#!/usr/bin/env python
|
||
|
|
"""Scrub secrets from the codebase for GitHub-safe public reference.
|
||
|
|
|
||
|
|
Run this on the public/github-safe branch before committing.
|
||
|
|
It redacts hardcoded API keys, tokens, and replaces .env files.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
import shutil
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
|
||
|
|
# Patterns to redact (value -> REDACTED)
|
||
|
|
SECRET_PATTERNS: list[tuple[str, str]] = [
|
||
|
|
# Hardcoded tokens in settlement_sources.py
|
||
|
|
(
|
||
|
|
r'IMGW_METEO_API_TOKEN\s*=\s*"[^"]*"',
|
||
|
|
'IMGW_METEO_API_TOKEN = os.environ.get("IMGW_METEO_API_TOKEN", "")',
|
||
|
|
),
|
||
|
|
(
|
||
|
|
r'NOAA_WRH_MESO_TOKEN\s*=\s*"[^"]*"',
|
||
|
|
'NOAA_WRH_MESO_TOKEN = os.environ.get("NOAA_WRH_MESO_TOKEN", "")',
|
||
|
|
),
|
||
|
|
# mesoToken in tmp files
|
||
|
|
(
|
||
|
|
r"var\s+mesoToken\s*=\s*'[^']*'",
|
||
|
|
"var mesoToken = 'REDACTED'",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
r"mesoToken\s*=\s*'[^']*'",
|
||
|
|
"mesoToken = 'REDACTED'",
|
||
|
|
),
|
||
|
|
# Any bare synopticdata URLs with tokens
|
||
|
|
(
|
||
|
|
r"https://api\.synopticdata\.com/v2/stations/timeseries\?[^\"'\s]*token=[^\"'&\s]+",
|
||
|
|
"https://api.synopticdata.com/v2/stations/timeseries?token=REDACTED",
|
||
|
|
),
|
||
|
|
]
|
||
|
|
|
||
|
|
# Files to delete entirely (temp files with secrets)
|
||
|
|
DELETE_FILES: list[str] = [
|
||
|
|
"tmp_apikey.js",
|
||
|
|
"tmp_obs.js",
|
||
|
|
]
|
||
|
|
|
||
|
|
# Files to replace with .example counterparts
|
||
|
|
REPLACE_WITH_EXAMPLE: dict[str, str] = {
|
||
|
|
".env": ".env.example",
|
||
|
|
".env.local": ".env.example",
|
||
|
|
"frontend/.env.local": "frontend/.env.example",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def scrub_file(path: Path) -> bool:
|
||
|
|
"""Apply secret patterns to a single file. Returns True if changed."""
|
||
|
|
try:
|
||
|
|
content = path.read_text(encoding="utf-8")
|
||
|
|
except (UnicodeDecodeError, PermissionError):
|
||
|
|
return False
|
||
|
|
|
||
|
|
original = content
|
||
|
|
for pattern, replacement in SECRET_PATTERNS:
|
||
|
|
content = re.sub(pattern, replacement, content)
|
||
|
|
|
||
|
|
if content != original:
|
||
|
|
path.write_text(content, encoding="utf-8")
|
||
|
|
print(f" SCRUBBED: {path.relative_to(REPO_ROOT)}")
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
print("=== Scrubbing secrets for GitHub-safe reference ===\n")
|
||
|
|
|
||
|
|
changes = 0
|
||
|
|
|
||
|
|
# 1. Delete known temp files with secrets
|
||
|
|
for fname in DELETE_FILES:
|
||
|
|
fpath = REPO_ROOT / fname
|
||
|
|
if fpath.exists():
|
||
|
|
fpath.unlink()
|
||
|
|
print(f" DELETED: {fname}")
|
||
|
|
changes += 1
|
||
|
|
|
||
|
|
# 2. Replace .env files with .example
|
||
|
|
for target, example in REPLACE_WITH_EXAMPLE.items():
|
||
|
|
target_path = REPO_ROOT / target
|
||
|
|
example_path = REPO_ROOT / example
|
||
|
|
if target_path.exists() and example_path.exists():
|
||
|
|
shutil.copy2(example_path, target_path)
|
||
|
|
print(f" REPLACED: {target} -> {example}")
|
||
|
|
changes += 1
|
||
|
|
elif target_path.exists() and not example_path.exists():
|
||
|
|
target_path.unlink()
|
||
|
|
print(f" DELETED: {target} (no .example to replace with)")
|
||
|
|
changes += 1
|
||
|
|
|
||
|
|
# 3. Scrub hardcoded secrets in source files
|
||
|
|
src_dirs = [
|
||
|
|
REPO_ROOT / "src",
|
||
|
|
REPO_ROOT / "web",
|
||
|
|
REPO_ROOT / "frontend",
|
||
|
|
REPO_ROOT / "scripts",
|
||
|
|
]
|
||
|
|
for src_dir in src_dirs:
|
||
|
|
if not src_dir.is_dir():
|
||
|
|
continue
|
||
|
|
for fpath in src_dir.rglob("*.py"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
for fpath in src_dir.rglob("*.js"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
for fpath in src_dir.rglob("*.ts"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
for fpath in src_dir.rglob("*.tsx"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
|
||
|
|
# 4. Also check root-level configs
|
||
|
|
for fpath in REPO_ROOT.glob("*.yaml"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
for fpath in REPO_ROOT.glob("*.yml"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
for fpath in REPO_ROOT.glob("*.json"):
|
||
|
|
if scrub_file(fpath):
|
||
|
|
changes += 1
|
||
|
|
|
||
|
|
print(f"\n=== Done: {changes} changes made ===")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|