47 lines
2.0 KiB
Python
47 lines
2.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Check staged Git blobs, not working-tree copies. Does not alter the index."""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
def git(*args):
|
|
return subprocess.check_output(["git", *args])
|
|
|
|
def main():
|
|
errors, total, count = [], 0, 0
|
|
for row in git("ls-files", "--stage", "-z").split(b"\0"):
|
|
if not row:
|
|
continue
|
|
meta, rawpath = row.split(b"\t", 1)
|
|
mode, oid, stage = meta.decode().split()
|
|
path = rawpath.decode("utf-8")
|
|
count += 1
|
|
if mode == "160000" or path.startswith(("datasets/", "experiments/")) or any(p in path.split("/") for p in (".venv", ".git", "__pycache__")):
|
|
errors.append({"path":path, "rule":"forbidden data/environment/submodule"})
|
|
continue
|
|
size = int(git("cat-file", "-s", oid))
|
|
total += size
|
|
if size > 25 * 1024 * 1024:
|
|
errors.append({"path":path, "rule":"blob exceeds 25 MiB policy"})
|
|
continue
|
|
if re.search(r"(^|/)(?:\.env(?:\..*)?|id_rsa|id_ed25519)$|\.(?:pem|key|p12)$", path, re.I):
|
|
errors.append({"path":path, "rule":"credential filename"})
|
|
if path.endswith(".gz"):
|
|
continue # generated hash-only inventory, validated separately
|
|
data = git("cat-file", "blob", oid).decode("utf-8", errors="replace")
|
|
rules = {
|
|
"private key": r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----",
|
|
"access token": r"(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|AKIA[A-Z0-9]{16})",
|
|
"credential URL": r"https?://[^\s/]+:[^\s/@]+@",
|
|
"literal password": r"(?i)(?:password|passwd|api_key|secret_key)\s*[:=]\s*['\"][^'\"\n]{6,}['\"]",
|
|
}
|
|
for rule, pattern in rules.items():
|
|
if re.search(pattern, data):
|
|
errors.append({"path":path, "rule":rule})
|
|
print(json.dumps({"pass":not errors, "files":count, "logical_bytes":total, "issues":errors}, indent=2))
|
|
return int(bool(errors))
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|