| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- #!/usr/bin/env python3
- """Fail closed when a release tree contains secret or binary payloads."""
- from __future__ import annotations
- import re
- import sys
- from pathlib import Path
- _FORBIDDEN_SUFFIXES = frozenset(
- {".der", ".jks", ".key", ".keystore", ".p12", ".pfx", ".pkcs12", ".pyc", ".pyo"}
- )
- _CONFIG_SUFFIXES = frozenset({".conf", ".env", ".ini", ".json", ".toml", ".txt", ".yaml", ".yml"})
- _PRIVATE_BLOCK = re.compile(
- "-----BEGIN " + r"(?:ENCRYPTED |RSA |EC |OPENSSH )?PRIVATE KEY-----",
- re.IGNORECASE,
- )
- _API_SECRET = re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b")
- _ASSIGNMENT = re.compile(
- r"(?im)^\s*(?:[A-Za-z0-9_]*_)?"
- r"(?:credential|password|passwd|secret|token|api_key|private_key)"
- r"\s*[:=]\s*['\"]?([^'\"\s#;,]{8,})"
- )
- _PLACEHOLDERS = ("${", "<", "change", "example", "placeholder", "replace-", "tbd")
- def _reject(message: str) -> int:
- print(f"release secret scan rejected: {message}", file=sys.stderr)
- return 1
- def scan(root: Path) -> int:
- if not root.is_absolute() or not root.is_dir() or root.is_symlink():
- return _reject("root must be an existing absolute directory")
- for path in sorted(root.rglob("*")):
- relative = path.relative_to(root)
- if path.is_symlink():
- return _reject(f"symbolic link {relative}")
- if path.is_dir():
- if path.name == "__pycache__":
- return _reject(f"cache directory {relative}")
- continue
- if not path.is_file() or path.suffix.casefold() in _FORBIDDEN_SUFFIXES:
- return _reject(f"forbidden file type {relative}")
- try:
- raw = path.read_bytes()
- if b"\x00" in raw:
- return _reject(f"binary file {relative}")
- text = raw.decode("utf-8")
- except (OSError, UnicodeDecodeError):
- return _reject(f"unreadable or non-UTF-8 file {relative}")
- if _PRIVATE_BLOCK.search(text) or _API_SECRET.search(text):
- return _reject(f"private key or API secret shape in {relative}")
- if path.suffix.casefold() in _CONFIG_SUFFIXES or path.name.startswith(".env"):
- for match in _ASSIGNMENT.finditer(text):
- value = match.group(1).casefold()
- if value in {"false", "none", "null", "true"}:
- continue
- if any(marker in value for marker in _PLACEHOLDERS):
- continue
- return _reject(f"literal credential shape in {relative}")
- return 0
- def main() -> int:
- if len(sys.argv) != 2:
- print("usage: scan_release_secrets.py ABSOLUTE_RELEASE_ROOT", file=sys.stderr)
- return 2
- return scan(Path(sys.argv[1]))
- if __name__ == "__main__":
- raise SystemExit(main())
|