refuse.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Secret-pattern refuse helpers (§QR.6.3).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import re |
| 6 | from pathlib import Path |
| 7 | |
| 8 | from tools.desktop_release.constants import PRIVATE_KEY_BASENAME_MARKERS, PUBLIC_KEY_ALLOWED_SUFFIXES |
| 9 | |
| 10 | # Private-key PEM headers (workflow / tree lint). |
| 11 | _PEM_PRIVATE_RE = re.compile( |
| 12 | r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----", |
| 13 | ) |
| 14 | # Hard-coded password assignments in YAML (crude but fail-closed for literals). |
| 15 | _HARDCODED_PASSWORD_RE = re.compile( |
| 16 | r"(?i)(password|passwd|secret)\s*[:=]\s*['\"][^'\"]{4,}['\"]", |
| 17 | ) |
| 18 | # PFX / PKCS12 magic often appears as base64 of 0x30 0x82… — flag obvious literals. |
| 19 | _PFX_MAGIC_RE = re.compile(r"(?i)MII[A-Za-z0-9+/]{40,}={0,2}") |
| 20 | |
| 21 | |
| 22 | class RefuseError(ValueError): |
| 23 | """Raised when secret-like material is refused.""" |
| 24 | |
| 25 | |
| 26 | def scan_text_for_secret_patterns(text: str) -> list[str]: |
| 27 | """Return list of matched secret-pattern labels (empty = clean).""" |
| 28 | hits: list[str] = [] |
| 29 | if _PEM_PRIVATE_RE.search(text): |
| 30 | hits.append("pem_private_key") |
| 31 | if _HARDCODED_PASSWORD_RE.search(text): |
| 32 | hits.append("hardcoded_password") |
| 33 | # Avoid flagging `${{ secrets.NAME }}` — only large base64-looking blobs. |
| 34 | for match in _PFX_MAGIC_RE.finditer(text): |
| 35 | snippet = match.group(0) |
| 36 | if "secrets." in text[max(0, match.start() - 40) : match.start()]: |
| 37 | continue |
| 38 | if len(snippet) >= 64: |
| 39 | hits.append("pfx_like_blob") |
| 40 | break |
| 41 | return hits |
| 42 | |
| 43 | |
| 44 | def refuse_secret_write_to_repo(path: Path, content: bytes | str) -> None: |
| 45 | """Refuse writing secret-like content into a repo path. |
| 46 | |
| 47 | Used by tests and helpers — not a runtime daemon. Raises :class:`RefuseError` |
| 48 | when content matches private-key / password / PFX patterns. |
| 49 | """ |
| 50 | text = content.decode("utf-8", errors="replace") if isinstance(content, bytes) else content |
| 51 | hits = scan_text_for_secret_patterns(text) |
| 52 | suffix = path.suffix.lower() |
| 53 | name = path.name.lower() |
| 54 | if suffix in {".p12", ".pfx"} or name.endswith(".p12") or name.endswith(".pfx"): |
| 55 | hits.append("cert_file_extension") |
| 56 | if hits: |
| 57 | raise RefuseError(f"refuse writing secrets to {path}: {', '.join(hits)}") |
| 58 | |
| 59 | |
| 60 | def refuse_private_key_under_desktop_keys(path: Path) -> None: |
| 61 | """Refuse private-key style filenames under ``desktop/keys/``. |
| 62 | |
| 63 | Public material (``.pub``, ``.asc``, ``.minisign.pub``, ``README.md``) is allowed. |
| 64 | """ |
| 65 | parts = Path(path).as_posix().replace("\\", "/").split("/") |
| 66 | try: |
| 67 | desktop_idx = parts.index("desktop") |
| 68 | keys_idx = parts.index("keys", desktop_idx + 1) |
| 69 | except ValueError: |
| 70 | return |
| 71 | if keys_idx != desktop_idx + 1: |
| 72 | return |
| 73 | if keys_idx + 1 >= len(parts): |
| 74 | return |
| 75 | basename = parts[-1] |
| 76 | if basename in {"README.md", ".gitkeep"}: |
| 77 | return |
| 78 | lower = basename.lower() |
| 79 | if any(lower.endswith(suf) for suf in PUBLIC_KEY_ALLOWED_SUFFIXES): |
| 80 | return |
| 81 | if any(marker in lower for marker in PRIVATE_KEY_BASENAME_MARKERS): |
| 82 | raise RefuseError(f"private key filename refused under desktop/keys/: {basename}") |
| 83 | if lower.endswith((".pem", ".p12", ".pfx", ".key")): |
| 84 | raise RefuseError(f"private key filename refused under desktop/keys/: {basename}") |