-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity_scan.py
More file actions
100 lines (93 loc) · 2.85 KB
/
Copy pathsecurity_scan.py
File metadata and controls
100 lines (93 loc) · 2.85 KB
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
"""Scan current text files for literal credentials without printing secret contents."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
TEXT_SUFFIXES = {
".py",
".md",
".toml",
".yaml",
".yml",
".json",
".ipynb",
".txt",
".csv",
".example",
}
EXCLUDED_PARTS = {
".git",
".pytest_cache",
".ruff_cache",
".ultralytics",
"__pycache__",
"models",
"node_modules",
".playwright-cli",
}
PATTERNS = (
re.compile(
r"""(?ix)
(?:api[_-]?key|access[_-]?token|client[_-]?secret)
\s*[:=]\s*["'][^"'\\\s]{8,}["']
"""
),
re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
re.compile(r'(?i)"private_key"\s*:\s*"[^"]{20,}"'),
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument(
"--output", type=Path, default=Path("reports/security_scan.json")
)
args = parser.parse_args()
root = args.root.resolve()
private_env = root / ".env"
gitignore = root / ".gitignore"
private_env_ignored = False
if gitignore.is_file():
ignored_lines = {
line.strip()
for line in gitignore.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
private_env_ignored = ".env" in ignored_lines
findings: list[str] = []
for path in root.rglob("*"):
if not path.is_file() or any(part in EXCLUDED_PARTS for part in path.parts):
continue
if path.resolve() == private_env:
continue
if path.suffix.lower() not in TEXT_SUFFIXES and path.name not in {
".gitignore",
".env",
}:
continue
try:
content = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
if any(pattern.search(content) for pattern in PATTERNS):
findings.append(str(path.relative_to(root)))
report = {
"status": "PASSED" if not findings else "FAILED",
"credential_like_files": sorted(findings),
"secrets_printed": False,
"private_env_present": private_env.is_file(),
"private_env_ignored": private_env_ignored,
}
if private_env.is_file() and not private_env_ignored:
report["status"] = "FAILED"
report["credential_like_files"].append(".env (not ignored)")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(
"No credentials found"
if not findings
else f"Credential-like content found in {len(findings)} file(s)"
)
return 0 if not findings else 1
if __name__ == "__main__":
raise SystemExit(main())