railiance-infra/scripts/check_secret_paths.py
codex b93af8cc78
Some checks failed
CI Smoke / source-contract (push) Failing after 2s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Implement reproducible S1 handoff contracts
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
2026-08-23 12:02:23 +02:00

87 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Fail when a declared secret-bearing Git path is not SOPS/age encrypted."""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def is_protected_path(path: str) -> bool:
normalized = path.strip("/")
name = Path(normalized).name.lower()
return normalized.startswith("secrets/") or (
normalized.startswith("inventory/")
and name.startswith("secrets")
and name.endswith((".yaml", ".yml", ".json"))
)
def is_encrypted_content(path: str, content: str) -> bool:
if path.endswith((".age", ".gpg")):
return bool(content)
return any(
line.strip() == "sops:" or line.lstrip().startswith('"sops"')
for line in content.splitlines()
)
def _git(*args: str) -> str:
return subprocess.check_output(["git", *args], cwd=ROOT, text=True)
def staged_files() -> list[str]:
return [
path
for path in _git("diff", "--cached", "--name-only", "--diff-filter=ACMR").splitlines()
if is_protected_path(path)
]
def tracked_files() -> list[str]:
return [
path
for path in _git("ls-files").splitlines()
if is_protected_path(path) and (ROOT / path).is_file()
]
def validate_paths(paths: list[str], *, staged: bool) -> list[str]:
failures = []
for relative in sorted(set(paths)):
try:
content = (
_git("show", f":{relative}")
if staged
else (ROOT / relative).read_text(encoding="utf-8")
)
except (OSError, subprocess.CalledProcessError):
failures.append(f"{relative}: cannot read protected content")
continue
if not is_encrypted_content(relative, content):
failures.append(f"{relative}: plaintext or missing SOPS metadata")
return failures
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--staged", action="store_true")
mode.add_argument("--tracked", action="store_true")
args = parser.parse_args()
paths = staged_files() if args.staged else tracked_files()
failures = validate_paths(paths, staged=args.staged)
if failures:
print("Unencrypted secret-bearing paths:\n- " + "\n- ".join(failures), file=sys.stderr)
return 1
print(f"secret path check passed ({len(paths)} protected file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())