Files
2026-10-07 21:00:21 +02:00

82 lines
2.9 KiB
Python

"""Check the staged file set before a private repository is pushed.
Print only rule names and paths, never matched document text or secret values.
"""
from __future__ import annotations
from pathlib import Path
import re
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
ROOT_FILES = {".gitignore", "README.md", "pyproject.toml", "config.example.env"}
SUBDIR_SUFFIXES = {
"ocr_compare": {".py", ".js", ".css", ".html"},
"tests": {".py"},
"scripts": {".py"},
}
SENSITIVE = {
"local-user-path": re.compile(r"/(?:Users|home)/[^/\s]+"),
"non-loopback-ipv4": re.compile(r"\b(?!127\.0\.0\.1\b)(?:\d{1,3}\.){3}\d{1,3}\b"),
"named-organization": re.compile(r"\b(?:fair" + "gentur|A" + "TU|Kawa" + "saki)\b", re.I),
"personal-name": re.compile(r"\bPa" + "trick\b", re.I),
"email-address": re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.(?!invalid\b)[A-Z]{2,}\b", re.I),
"private-key": re.compile(r"-----BEGIN (?:OPENSSH|RSA|EC) PRIVATE KEY-----"),
}
def allowed(name: str) -> bool:
if name in ROOT_FILES:
return True
parts = name.split("/")
return (len(parts) == 2 and parts[0] in SUBDIR_SUFFIXES and
Path(parts[1]).suffix in SUBDIR_SUFFIXES[parts[0]])
def main() -> int:
listing = subprocess.run(["git", "ls-files", "--stage", "-z"], cwd=ROOT,
capture_output=True, check=True).stdout
entries = [entry for entry in listing.split(b"\0") if entry]
if not entries:
print("No staged or tracked files to audit")
return 1
findings = []
for entry in entries:
meta, raw_name = entry.split(b"\t", 1)
mode, object_id, stage = meta.decode("ascii").split()
name = raw_name.decode("utf-8", "surrogateescape")
if not allowed(name):
findings.append(("unexpected-file", name))
continue
if mode not in {"100644", "100755"} or stage != "0":
findings.append(("non-regular-file", name))
continue
blob = subprocess.run(["git", "cat-file", "blob", object_id], cwd=ROOT,
capture_output=True, check=True).stdout
if len(blob) > 1_000_000:
findings.append(("oversized-file", name))
continue
try:
source = blob.decode("utf-8")
except UnicodeDecodeError:
findings.append(("binary-file", name))
continue
if name == "scripts/release_audit.py":
continue # The rule definitions themselves contain example patterns.
for label, pattern in SENSITIVE.items():
if pattern.search(source):
findings.append((label, name))
for label, name in findings:
print(f"{label}: {name}")
if findings:
return 1
print(f"Release audit passed: {len(entries)} text files, no private-path or document artifacts")
return 0
if __name__ == "__main__":
sys.exit(main())