Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
87 lines
2.5 KiB
Python
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())
|