#!/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())