Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Fail-closed secret scanner for public submission artifacts and local logs.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any, NamedTuple | |
| ROOT = Path(__file__).resolve().parents[1] | |
| SCHEMA = "proofframe.secret_scan.v1" | |
| DEFAULT_JSON = ROOT / "docs" / "assets" / "secret-scan-report.json" | |
| DEFAULT_MD = ROOT / "docs" / "assets" / "secret-scan-report.md" | |
| HARD_SKIP_DIRS = { | |
| ".git", | |
| ".mypy_cache", | |
| ".pytest_cache", | |
| ".ruff_cache", | |
| ".venv", | |
| "__pycache__", | |
| "node_modules", | |
| } | |
| LOCAL_SECRET_FILES = { | |
| ".env", | |
| ".env.local", | |
| ".env.final.local", | |
| } | |
| TEXT_EXTENSIONS = { | |
| ".css", | |
| ".env", | |
| ".example", | |
| ".html", | |
| ".js", | |
| ".json", | |
| ".log", | |
| ".md", | |
| ".py", | |
| ".toml", | |
| ".txt", | |
| ".yml", | |
| ".yaml", | |
| } | |
| BINARY_REVIEW_EXTENSIONS = { | |
| ".gif", | |
| ".jpeg", | |
| ".jpg", | |
| ".mov", | |
| ".mp4", | |
| ".pdf", | |
| ".png", | |
| ".webm", | |
| ".webp", | |
| ".zip", | |
| } | |
| class Pattern(NamedTuple): | |
| pattern_id: str | |
| label: str | |
| regex: re.Pattern[str] | |
| PATTERNS = [ | |
| Pattern("aws_access_key", "AWS-style access key", re.compile(r"AKIA[0-9A-Z]{16}")), | |
| Pattern( | |
| "assigned_secret", | |
| "Assigned API key, application key, token, cookie, or secret", | |
| re.compile( | |
| r"(?i)['\"]?(api[_-]?key|application[_-]?key|secret|token|cookie)['\"]?\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{20,}" | |
| ), | |
| ), | |
| Pattern( | |
| "bearer_token", | |
| "Bearer authorization token", | |
| re.compile(r"(?i)authorization:\s*bearer\s+[A-Za-z0-9._\-]{20,}"), | |
| ), | |
| Pattern("gmi_key", "GMI/Genblaze-style key", re.compile(r"(?i)gmi-[A-Za-z0-9_\-]{16,}")), | |
| Pattern( | |
| "signed_url_signature", | |
| "S3/B2 signed URL signature", | |
| re.compile(r"(?i)(x-amz-signature|x-bz-signature|X-Amz-Signature)=[A-Fa-f0-9]{32,}"), | |
| ), | |
| Pattern( | |
| "aws_query_credential", | |
| "AWS/B2 S3 signed URL credential", | |
| re.compile(r"(?i)(AWSAccessKeyId|X-Amz-Credential)=[^&\\s]{12,}"), | |
| ), | |
| ] | |
| def utc_now() -> str: | |
| return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") | |
| def display_path(path: Path, root: Path) -> str: | |
| try: | |
| return str(path.resolve().relative_to(root.resolve())) | |
| except ValueError: | |
| return str(path) | |
| def is_hard_skipped(path: Path, root: Path) -> bool: | |
| try: | |
| parts = path.relative_to(root).parts | |
| except ValueError: | |
| return False | |
| return any(part in HARD_SKIP_DIRS for part in parts) | |
| def is_local_secret_file(path: Path) -> bool: | |
| return path.name in LOCAL_SECRET_FILES | |
| def is_text_file(path: Path) -> bool: | |
| return path.suffix.lower() in TEXT_EXTENSIONS or path.name in {".env.example", "Dockerfile"} | |
| def is_binary_review_file(path: Path) -> bool: | |
| return path.suffix.lower() in BINARY_REVIEW_EXTENSIONS | |
| def should_scan(path: Path, root: Path = ROOT) -> bool: | |
| if is_hard_skipped(path, root) or is_local_secret_file(path): | |
| return False | |
| return is_text_file(path) | |
| def line_is_placeholder(line: str) -> bool: | |
| stripped = line.strip() | |
| lowered = stripped.lower() | |
| return ( | |
| not stripped | |
| or "placeholder" in lowered | |
| or "redacted" in lowered | |
| or stripped.endswith("=") | |
| or stripped.endswith('=""') | |
| or stripped.endswith("=''") | |
| ) | |
| def finding(path: Path, root: Path, line_number: int, pattern: Pattern) -> dict[str, Any]: | |
| return { | |
| "path": display_path(path, root), | |
| "line": line_number, | |
| "pattern": pattern.pattern_id, | |
| "label": pattern.label, | |
| "detail": "Potential secret-like value detected. Line content is intentionally not stored.", | |
| } | |
| def scan_path(path: Path, root: Path = ROOT) -> list[dict[str, Any]]: | |
| findings: list[dict[str, Any]] = [] | |
| try: | |
| text = path.read_text(encoding="utf-8") | |
| except UnicodeDecodeError: | |
| return findings | |
| for line_number, line in enumerate(text.splitlines(), start=1): | |
| if line_is_placeholder(line): | |
| continue | |
| for pattern in PATTERNS: | |
| if pattern.regex.search(line): | |
| findings.append(finding(path, root, line_number, pattern)) | |
| break | |
| return findings | |
| def iter_files(root: Path) -> list[Path]: | |
| return sorted(path for path in root.rglob("*") if path.is_file()) | |
| def collect_scan(root: Path = ROOT) -> dict[str, Any]: | |
| root = root.resolve() | |
| text_files: list[str] = [] | |
| binary_review_files: list[dict[str, Any]] = [] | |
| skipped_local_secret_files: list[str] = [] | |
| skipped_hard_files = 0 | |
| findings: list[dict[str, Any]] = [] | |
| for path in iter_files(root): | |
| if is_hard_skipped(path, root): | |
| skipped_hard_files += 1 | |
| continue | |
| if is_local_secret_file(path): | |
| skipped_local_secret_files.append(display_path(path, root)) | |
| continue | |
| if is_text_file(path): | |
| text_files.append(display_path(path, root)) | |
| findings.extend(scan_path(path, root)) | |
| continue | |
| if is_binary_review_file(path): | |
| binary_review_files.append( | |
| { | |
| "path": display_path(path, root), | |
| "bytes": path.stat().st_size, | |
| "review": "inventoried_binary_media", | |
| } | |
| ) | |
| docs_assets_scanned = [path for path in text_files if path.startswith("docs/assets/")] | |
| var_logs_scanned = [path for path in text_files if path.startswith("var/")] | |
| screenshot_inventory = [ | |
| item for item in binary_review_files if item["path"].startswith("docs/assets/") and item["path"].lower().endswith(".png") | |
| ] | |
| return { | |
| "schema": SCHEMA, | |
| "created_at": utc_now(), | |
| "mode": "clear" if not findings else "findings_detected", | |
| "ok": not findings, | |
| "root": ".", | |
| "findings": findings, | |
| "counts": { | |
| "scanned_text_files": len(text_files), | |
| "inventoried_binary_files": len(binary_review_files), | |
| "skipped_local_secret_files": len(skipped_local_secret_files), | |
| "skipped_hard_files": skipped_hard_files, | |
| "findings": len(findings), | |
| }, | |
| "coverage": { | |
| "public_text_files_scanned": True, | |
| "docs_assets_text_files_scanned": bool(docs_assets_scanned), | |
| "var_log_text_files_scanned": bool(var_logs_scanned), | |
| "screenshot_binary_inventory": bool(screenshot_inventory), | |
| "local_secret_files_excluded_without_reading": skipped_local_secret_files, | |
| "notes": [ | |
| "Text files are scanned for key-like assignments, bearer tokens, signed URL parameters, and GMI-style keys.", | |
| "Binary screenshots/media are inventoried by path and size; review visible content before marking final T041A done.", | |
| "Local credential files such as .env.final.local are intentionally excluded without reading values.", | |
| ], | |
| }, | |
| "scanned_text_files": text_files, | |
| "binary_review_files": binary_review_files, | |
| } | |
| def run_scan(root: Path = ROOT) -> list[str]: | |
| report = collect_scan(root) | |
| return [ | |
| f"{item['path']}:{item['line']}: possible secret ({item['pattern']})" | |
| for item in report["findings"] | |
| ] | |
| def render_markdown(report: dict[str, Any]) -> str: | |
| counts = report["counts"] | |
| coverage = report["coverage"] | |
| lines = [ | |
| "# ProofFrame Secret Scan Report", | |
| "", | |
| f"Mode: `{report['mode']}`", | |
| f"OK: `{str(report['ok']).lower()}`", | |
| f"Created: `{report['created_at']}`", | |
| "", | |
| "## Counts", | |
| "", | |
| f"- Scanned text files: `{counts['scanned_text_files']}`", | |
| f"- Inventoried binary files: `{counts['inventoried_binary_files']}`", | |
| f"- Skipped local secret files: `{counts['skipped_local_secret_files']}`", | |
| f"- Findings: `{counts['findings']}`", | |
| "", | |
| "## Coverage", | |
| "", | |
| f"- Docs/assets text scanned: `{str(coverage['docs_assets_text_files_scanned']).lower()}`", | |
| f"- Var/log text scanned: `{str(coverage['var_log_text_files_scanned']).lower()}`", | |
| f"- Screenshot/media inventory: `{str(coverage['screenshot_binary_inventory']).lower()}`", | |
| f"- Local secret files excluded without reading: `{', '.join(coverage['local_secret_files_excluded_without_reading']) or 'none'}`", | |
| "", | |
| ] | |
| for note in coverage["notes"]: | |
| lines.append(f"- {note}") | |
| lines.extend(["", "## Findings", ""]) | |
| if report["findings"]: | |
| for item in report["findings"]: | |
| lines.append( | |
| f"- `{item['path']}:{item['line']}` {item['label']} ({item['pattern']})" | |
| ) | |
| else: | |
| lines.append("- None") | |
| lines.extend(["", "No credential values, matched line text, browser cookies, or signed URLs are printed in this report."]) | |
| return "\n".join(lines) + "\n" | |
| def write_outputs(report: dict[str, Any], json_path: Path, markdown_path: Path) -> None: | |
| json_path.parent.mkdir(parents=True, exist_ok=True) | |
| markdown_path.parent.mkdir(parents=True, exist_ok=True) | |
| json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") | |
| markdown_path.write_text(render_markdown(report), encoding="utf-8") | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description="Scan public ProofFrame files for obvious secrets.") | |
| parser.add_argument("--quiet", action="store_true") | |
| parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON) | |
| parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD) | |
| parser.add_argument( | |
| "--no-report", | |
| action="store_true", | |
| help="Only print legacy scan output; do not write JSON/Markdown reports.", | |
| ) | |
| return parser | |
| def main() -> None: | |
| args = build_parser().parse_args() | |
| report = collect_scan(ROOT) | |
| if not args.no_report: | |
| write_outputs(report, args.json_out, args.markdown_out) | |
| if report["findings"]: | |
| print( | |
| "\n".join( | |
| f"{item['path']}:{item['line']}: possible secret ({item['pattern']})" | |
| for item in report["findings"] | |
| ) | |
| ) | |
| raise SystemExit(1) | |
| if not args.quiet: | |
| if args.no_report: | |
| print("No obvious secrets found.") | |
| else: | |
| print( | |
| json.dumps( | |
| { | |
| "ok": report["ok"], | |
| "mode": report["mode"], | |
| "json": str(args.json_out), | |
| "markdown": str(args.markdown_out), | |
| "scanned_text_files": report["counts"]["scanned_text_files"], | |
| "inventoried_binary_files": report["counts"]["inventoried_binary_files"], | |
| }, | |
| indent=2, | |
| ) | |
| ) | |
| if __name__ == "__main__": | |
| main() | |