File size: 3,793 Bytes
79c4c46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""Fail if credential-shaped strings or .env files are present in the repository.



Scans tracked files plus untracked, non-ignored files inside ClearCast's own

directories. Reports file, line, and rule only, never the matched value.

Usage: python scripts/check_secrets.py

"""

from __future__ import annotations

import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PROJECT_PREFIXES = (
    ".github/",
    "agent/",
    "api/",
    "contracts/",
    "deploy/",
    "docs/",
    "evaluation/",
    "frontend/",
    "gateway/",
    "mcp_server/",
    "scripts/",
    "tests/",
    "app.py",
    "Dockerfile",
    "docker-compose.yml",
    "README.md",
    ".env",
    "requirements",
    "pyproject.toml",
    "test_mcp.py",
    "safe_env_check.py",
    "progress.md",
    ".gitignore",
    ".dockerignore",
)
RULES = {
    "openai_key": re.compile(r"sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}"),
    "huggingface_token": re.compile(r"hf_[A-Za-z0-9]{30,}"),
    "langsmith_key": re.compile(r"lsv2_(?:pt|sk)_[A-Za-z0-9]{20,}"),
    "github_token": re.compile(r"(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{40,})"),
    "aws_access_key": re.compile(r"AKIA[0-9A-Z]{16}"),
    "private_key": re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
}
# OpenWeatherMap keys are 32 lowercase alphanumerics; flag them on lines that mention the provider.
CONTEXT_RULES = {
    "openweathermap_key": (re.compile(r"openweather|\bowm|appid", re.I), re.compile(r"\b[0-9a-z]{32}\b")),
}
# Obviously synthetic values used in tests and fixtures.
ALLOW = re.compile(r"fake|test|fixture|offline|example|dummy|redacted|NEVER|SECRET|not-a-real|placeholder", re.I)
SKIP_SUFFIXES = (".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf", ".lock")


def candidate_files() -> list[str]:
    def git(*args: str) -> list[str]:
        out = subprocess.run(["git", *args], cwd=ROOT, capture_output=True, text=True, check=True).stdout
        return [line for line in out.splitlines() if line]

    tracked = set(git("ls-files", "--cached"))
    untracked = {
        path for path in git("ls-files", "--others", "--exclude-standard") if path.startswith(PROJECT_PREFIXES)
    }
    return sorted(tracked | untracked)


def main() -> int:
    problems: list[str] = []
    for path in candidate_files():
        name = Path(path).name
        if (name == ".env" or name.startswith(".env.")) and name != ".env.example":
            problems.append(f"{path}: environment file must not be committed")
            continue
        if path.endswith(SKIP_SUFFIXES) or path.endswith("package-lock.json"):
            continue
        file = ROOT / path
        if not file.is_file():
            continue
        try:
            lines = file.read_text(encoding="utf-8").splitlines()
        except (UnicodeDecodeError, OSError):
            continue
        for number, line in enumerate(lines, 1):
            for rule, pattern in RULES.items():
                for match in pattern.finditer(line):
                    if not ALLOW.search(match.group(0)):
                        problems.append(f"{path}:{number}: possible {rule}")
            for rule, (context, pattern) in CONTEXT_RULES.items():
                if context.search(line) and any(not ALLOW.search(m.group(0)) for m in pattern.finditer(line)):
                    problems.append(f"{path}:{number}: possible {rule}")
    if problems:
        print("Secret scan failed:")
        print("\n".join(f"  {problem}" for problem in problems))
        return 1
    print("Secret scan passed: no credential-shaped strings found.")
    return 0


if __name__ == "__main__":
    sys.exit(main())