Implement reproducible S1 handoff contracts
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
This commit is contained in:
parent
c8cb1c8edf
commit
b93af8cc78
44 changed files with 2035 additions and 342 deletions
87
scripts/check_secret_paths.py
Normal file
87
scripts/check_secret_paths.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue