@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -30,7 +30,7 @@ import re
|
|
|
30
30
|
import subprocess
|
|
31
31
|
import sys
|
|
32
32
|
from pathlib import Path
|
|
33
|
-
from typing import Any
|
|
33
|
+
from typing import Any
|
|
34
34
|
|
|
35
35
|
# ---------------------------------------------------------------------------
|
|
36
36
|
# Constants
|
|
@@ -44,13 +44,29 @@ SEVERITY_ORDER: dict[str, int] = {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
SCANNABLE_EXTENSIONS: set[str] = {
|
|
47
|
-
".py",
|
|
48
|
-
".
|
|
47
|
+
".py",
|
|
48
|
+
".js",
|
|
49
|
+
".ts",
|
|
50
|
+
".jsx",
|
|
51
|
+
".tsx",
|
|
52
|
+
".java",
|
|
53
|
+
".rb",
|
|
54
|
+
".go",
|
|
55
|
+
".php",
|
|
56
|
+
".sh",
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
SKIP_DIRS: set[str] = {
|
|
52
|
-
".git",
|
|
53
|
-
"
|
|
60
|
+
".git",
|
|
61
|
+
"node_modules",
|
|
62
|
+
"__pycache__",
|
|
63
|
+
".venv",
|
|
64
|
+
"venv",
|
|
65
|
+
".tox",
|
|
66
|
+
".mypy_cache",
|
|
67
|
+
".pytest_cache",
|
|
68
|
+
"dist",
|
|
69
|
+
"build",
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
BANDIT_TIMEOUT_SECONDS: int = 120
|
|
@@ -64,28 +80,36 @@ BANDIT_TIMEOUT_SECONDS: int = 120
|
|
|
64
80
|
_HARDCODED_SECRET_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str, str | None]] = [
|
|
65
81
|
(
|
|
66
82
|
re.compile(r"""api[_\-]?key\s*[=:]\s*["'][A-Za-z0-9]{20,}""", re.IGNORECASE),
|
|
67
|
-
"hardcoded-secret",
|
|
83
|
+
"hardcoded-secret",
|
|
84
|
+
"high",
|
|
85
|
+
"medium",
|
|
68
86
|
"Hardcoded API key detected",
|
|
69
87
|
"Move API keys to environment variables or a secrets manager.",
|
|
70
88
|
"CWE-798",
|
|
71
89
|
),
|
|
72
90
|
(
|
|
73
91
|
re.compile(r"""AKIA[0-9A-Z]{16}"""),
|
|
74
|
-
"hardcoded-secret",
|
|
92
|
+
"hardcoded-secret",
|
|
93
|
+
"critical",
|
|
94
|
+
"high",
|
|
75
95
|
"AWS Access Key ID detected",
|
|
76
96
|
"Rotate the exposed key immediately and use IAM roles or environment variables.",
|
|
77
97
|
"CWE-798",
|
|
78
98
|
),
|
|
79
99
|
(
|
|
80
100
|
re.compile(r"""password\s*[=:]\s*["'](?!["']$)(?!\s*$)(?!<%=)(?!\$\{)(?!\{\{)[^"']+["']""", re.IGNORECASE),
|
|
81
|
-
"hardcoded-secret",
|
|
101
|
+
"hardcoded-secret",
|
|
102
|
+
"high",
|
|
103
|
+
"medium",
|
|
82
104
|
"Hardcoded password detected",
|
|
83
105
|
"Use environment variables or a secrets manager instead of hardcoded passwords.",
|
|
84
106
|
"CWE-798",
|
|
85
107
|
),
|
|
86
108
|
(
|
|
87
109
|
re.compile(r"""-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+)?PRIVATE\s+KEY-----"""),
|
|
88
|
-
"hardcoded-secret",
|
|
110
|
+
"hardcoded-secret",
|
|
111
|
+
"critical",
|
|
112
|
+
"high",
|
|
89
113
|
"Private key embedded in source code",
|
|
90
114
|
"Remove the private key from source and store it in a secure vault.",
|
|
91
115
|
"CWE-321",
|
|
@@ -95,7 +119,9 @@ _HARDCODED_SECRET_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str,
|
|
|
95
119
|
r"""(?:secret|token|bearer)\s*[=:]\s*["'][A-Za-z0-9+/=]{20,}""",
|
|
96
120
|
re.IGNORECASE,
|
|
97
121
|
),
|
|
98
|
-
"hardcoded-secret",
|
|
122
|
+
"hardcoded-secret",
|
|
123
|
+
"high",
|
|
124
|
+
"medium",
|
|
99
125
|
"Hardcoded secret or token detected",
|
|
100
126
|
"Store secrets in environment variables or a dedicated secrets manager.",
|
|
101
127
|
"CWE-798",
|
|
@@ -108,21 +134,27 @@ _SQL_INJECTION_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str, st
|
|
|
108
134
|
r"""(?:execute|cursor|query)\s*\(\s*f["'].*(?:%s|%d|\{)""",
|
|
109
135
|
re.IGNORECASE,
|
|
110
136
|
),
|
|
111
|
-
"sql-injection",
|
|
137
|
+
"sql-injection",
|
|
138
|
+
"high",
|
|
139
|
+
"high",
|
|
112
140
|
"Potential SQL injection via string formatting",
|
|
113
141
|
"Use parameterized queries or prepared statements instead of string formatting.",
|
|
114
142
|
"CWE-89",
|
|
115
143
|
),
|
|
116
144
|
(
|
|
117
145
|
re.compile(r"""["']SELECT\s+.*["']\s*\+\s*""", re.IGNORECASE),
|
|
118
|
-
"sql-injection",
|
|
146
|
+
"sql-injection",
|
|
147
|
+
"high",
|
|
148
|
+
"medium",
|
|
119
149
|
"SQL query built with string concatenation (SELECT)",
|
|
120
150
|
"Use parameterized queries instead of string concatenation.",
|
|
121
151
|
"CWE-89",
|
|
122
152
|
),
|
|
123
153
|
(
|
|
124
154
|
re.compile(r"""["']INSERT\s+.*["']\s*\+\s*""", re.IGNORECASE),
|
|
125
|
-
"sql-injection",
|
|
155
|
+
"sql-injection",
|
|
156
|
+
"high",
|
|
157
|
+
"medium",
|
|
126
158
|
"SQL query built with string concatenation (INSERT)",
|
|
127
159
|
"Use parameterized queries instead of string concatenation.",
|
|
128
160
|
"CWE-89",
|
|
@@ -132,28 +164,36 @@ _SQL_INJECTION_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str, st
|
|
|
132
164
|
_COMMAND_INJECTION_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str, str | None]] = [
|
|
133
165
|
(
|
|
134
166
|
re.compile(r"""os\.system\("""),
|
|
135
|
-
"command-injection",
|
|
167
|
+
"command-injection",
|
|
168
|
+
"high",
|
|
169
|
+
"high",
|
|
136
170
|
"Use of os.system() allows shell command injection",
|
|
137
171
|
"Use subprocess.run() with a list of arguments and shell=False.",
|
|
138
172
|
"CWE-78",
|
|
139
173
|
),
|
|
140
174
|
(
|
|
141
175
|
re.compile(r"""subprocess\.(?:call|run|Popen)\(.*shell\s*=\s*True"""),
|
|
142
|
-
"command-injection",
|
|
176
|
+
"command-injection",
|
|
177
|
+
"high",
|
|
178
|
+
"high",
|
|
143
179
|
"Subprocess call with shell=True enables command injection",
|
|
144
180
|
"Pass commands as a list with shell=False instead of shell=True.",
|
|
145
181
|
"CWE-78",
|
|
146
182
|
),
|
|
147
183
|
(
|
|
148
184
|
re.compile(r"""\beval\("""),
|
|
149
|
-
"command-injection",
|
|
185
|
+
"command-injection",
|
|
186
|
+
"medium",
|
|
187
|
+
"medium",
|
|
150
188
|
"Use of eval() can execute arbitrary code",
|
|
151
189
|
"Avoid eval(). Use ast.literal_eval() for data parsing or refactor logic.",
|
|
152
190
|
"CWE-95",
|
|
153
191
|
),
|
|
154
192
|
(
|
|
155
193
|
re.compile(r"""\bexec\("""),
|
|
156
|
-
"command-injection",
|
|
194
|
+
"command-injection",
|
|
195
|
+
"medium",
|
|
196
|
+
"medium",
|
|
157
197
|
"Use of exec() can execute arbitrary code",
|
|
158
198
|
"Avoid exec(). Refactor to use safer alternatives.",
|
|
159
199
|
"CWE-95",
|
|
@@ -163,21 +203,27 @@ _COMMAND_INJECTION_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str
|
|
|
163
203
|
_DESERIALIZATION_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str, str | None]] = [
|
|
164
204
|
(
|
|
165
205
|
re.compile(r"""pickle\.loads?\("""),
|
|
166
|
-
"insecure-deserialization",
|
|
206
|
+
"insecure-deserialization",
|
|
207
|
+
"high",
|
|
208
|
+
"high",
|
|
167
209
|
"Insecure deserialization with pickle",
|
|
168
210
|
"Avoid pickle for untrusted data. Use JSON or a safe serialization format.",
|
|
169
211
|
"CWE-502",
|
|
170
212
|
),
|
|
171
213
|
(
|
|
172
214
|
re.compile(r"""yaml\.load\((?!.*Loader\s*=\s*(?:Safe|Base)Loader)"""),
|
|
173
|
-
"insecure-deserialization",
|
|
215
|
+
"insecure-deserialization",
|
|
216
|
+
"high",
|
|
217
|
+
"high",
|
|
174
218
|
"Unsafe YAML loading without SafeLoader",
|
|
175
219
|
"Use yaml.safe_load() or pass Loader=SafeLoader to yaml.load().",
|
|
176
220
|
"CWE-502",
|
|
177
221
|
),
|
|
178
222
|
(
|
|
179
223
|
re.compile(r"""marshal\.loads?\("""),
|
|
180
|
-
"insecure-deserialization",
|
|
224
|
+
"insecure-deserialization",
|
|
225
|
+
"high",
|
|
226
|
+
"medium",
|
|
181
227
|
"Insecure deserialization with marshal",
|
|
182
228
|
"Avoid marshal for untrusted data. Use JSON or a safe serialization format.",
|
|
183
229
|
"CWE-502",
|
|
@@ -187,28 +233,36 @@ _DESERIALIZATION_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str,
|
|
|
187
233
|
_CRYPTO_NETWORK_PATTERNS: list[tuple[re.Pattern[str], str, str, str, str, str, str | None]] = [
|
|
188
234
|
(
|
|
189
235
|
re.compile(r"""verify\s*=\s*False"""),
|
|
190
|
-
"insecure-transport",
|
|
236
|
+
"insecure-transport",
|
|
237
|
+
"medium",
|
|
238
|
+
"high",
|
|
191
239
|
"SSL/TLS certificate verification disabled",
|
|
192
240
|
"Enable certificate verification. Set verify=True or provide a CA bundle.",
|
|
193
241
|
"CWE-295",
|
|
194
242
|
),
|
|
195
243
|
(
|
|
196
244
|
re.compile(r"""\bMD5\b|\.md5\(""", re.IGNORECASE),
|
|
197
|
-
"weak-crypto",
|
|
245
|
+
"weak-crypto",
|
|
246
|
+
"medium",
|
|
247
|
+
"medium",
|
|
198
248
|
"Use of weak MD5 hashing algorithm",
|
|
199
249
|
"Use SHA-256 or stronger hashing. For passwords, use bcrypt or Argon2.",
|
|
200
250
|
"CWE-328",
|
|
201
251
|
),
|
|
202
252
|
(
|
|
203
253
|
re.compile(r"""\bSHA1\b|\.sha1\(""", re.IGNORECASE),
|
|
204
|
-
"weak-crypto",
|
|
254
|
+
"weak-crypto",
|
|
255
|
+
"medium",
|
|
256
|
+
"medium",
|
|
205
257
|
"Use of weak SHA-1 hashing algorithm",
|
|
206
258
|
"Use SHA-256 or stronger hashing. For passwords, use bcrypt or Argon2.",
|
|
207
259
|
"CWE-328",
|
|
208
260
|
),
|
|
209
261
|
(
|
|
210
262
|
re.compile(r"""http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])"""),
|
|
211
|
-
"insecure-transport",
|
|
263
|
+
"insecure-transport",
|
|
264
|
+
"medium",
|
|
265
|
+
"low",
|
|
212
266
|
"Insecure HTTP URL (not HTTPS)",
|
|
213
267
|
"Use HTTPS for all external communications.",
|
|
214
268
|
"CWE-319",
|
|
@@ -228,6 +282,7 @@ ALL_REGEX_PATTERNS = (
|
|
|
228
282
|
# Utility helpers
|
|
229
283
|
# ---------------------------------------------------------------------------
|
|
230
284
|
|
|
285
|
+
|
|
231
286
|
def _log(message: str, verbose: bool = True) -> None:
|
|
232
287
|
"""Print a progress message to stderr."""
|
|
233
288
|
if verbose:
|
|
@@ -281,6 +336,7 @@ def _normalize_bandit_confidence(raw: str) -> str:
|
|
|
281
336
|
# Bandit scanning
|
|
282
337
|
# ---------------------------------------------------------------------------
|
|
283
338
|
|
|
339
|
+
|
|
284
340
|
def run_bandit_scan(
|
|
285
341
|
directory: Path,
|
|
286
342
|
exclude_patterns: list[str] | None = None,
|
|
@@ -318,8 +374,7 @@ def run_bandit_scan(
|
|
|
318
374
|
return []
|
|
319
375
|
except subprocess.TimeoutExpired:
|
|
320
376
|
print(
|
|
321
|
-
f"[scanner] Bandit scan timed out after {BANDIT_TIMEOUT_SECONDS}s. "
|
|
322
|
-
"Consider narrowing the scan scope.",
|
|
377
|
+
f"[scanner] Bandit scan timed out after {BANDIT_TIMEOUT_SECONDS}s. Consider narrowing the scan scope.",
|
|
323
378
|
file=sys.stderr,
|
|
324
379
|
)
|
|
325
380
|
return []
|
|
@@ -342,22 +397,20 @@ def run_bandit_scan(
|
|
|
342
397
|
|
|
343
398
|
findings: list[dict[str, Any]] = []
|
|
344
399
|
for issue in data.get("results", []):
|
|
345
|
-
findings.append(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
f"CWE-{issue['issue_cwe']['id']}"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
),
|
|
360
|
-
})
|
|
400
|
+
findings.append(
|
|
401
|
+
{
|
|
402
|
+
"tool": "bandit",
|
|
403
|
+
"file": str(Path(issue.get("filename", "unknown")).resolve()),
|
|
404
|
+
"line": issue.get("line_number", 0),
|
|
405
|
+
"severity": _normalize_bandit_severity(issue.get("issue_severity", "LOW")),
|
|
406
|
+
"confidence": _normalize_bandit_confidence(issue.get("issue_confidence", "LOW")),
|
|
407
|
+
"category": issue.get("test_id", "unknown"),
|
|
408
|
+
"title": issue.get("test_name", "Unknown issue"),
|
|
409
|
+
"detail": issue.get("issue_text", ""),
|
|
410
|
+
"remediation": "",
|
|
411
|
+
"cwe": (f"CWE-{issue['issue_cwe']['id']}" if issue.get("issue_cwe", {}).get("id") else None),
|
|
412
|
+
}
|
|
413
|
+
)
|
|
361
414
|
|
|
362
415
|
_log(f"Bandit found {len(findings)} issue(s).", verbose)
|
|
363
416
|
return findings
|
|
@@ -367,6 +420,7 @@ def run_bandit_scan(
|
|
|
367
420
|
# Regex-based scanning
|
|
368
421
|
# ---------------------------------------------------------------------------
|
|
369
422
|
|
|
423
|
+
|
|
370
424
|
def run_regex_scan(
|
|
371
425
|
directory: Path,
|
|
372
426
|
exclude_patterns: list[str] | None = None,
|
|
@@ -429,18 +483,20 @@ def run_regex_scan(
|
|
|
429
483
|
continue
|
|
430
484
|
|
|
431
485
|
truncated_line = line.strip()[:200]
|
|
432
|
-
findings.append(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
486
|
+
findings.append(
|
|
487
|
+
{
|
|
488
|
+
"tool": "regex",
|
|
489
|
+
"file": str(filepath.resolve()),
|
|
490
|
+
"line": line_num,
|
|
491
|
+
"severity": severity,
|
|
492
|
+
"confidence": confidence,
|
|
493
|
+
"category": category,
|
|
494
|
+
"title": title,
|
|
495
|
+
"detail": truncated_line,
|
|
496
|
+
"remediation": remediation,
|
|
497
|
+
"cwe": cwe,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
444
500
|
|
|
445
501
|
_log(f"Regex scan complete: {files_scanned} file(s) scanned, {len(findings)} issue(s) found.", verbose)
|
|
446
502
|
return findings
|
|
@@ -471,18 +527,32 @@ def _is_password_placeholder(line: str) -> bool:
|
|
|
471
527
|
"""
|
|
472
528
|
lower = line.lower()
|
|
473
529
|
placeholders = [
|
|
474
|
-
'password = ""',
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
"password
|
|
530
|
+
'password = ""',
|
|
531
|
+
"password = ''",
|
|
532
|
+
'password: ""',
|
|
533
|
+
"password: ''",
|
|
534
|
+
"password = os.environ",
|
|
535
|
+
"password = os.getenv",
|
|
536
|
+
"password = env(",
|
|
537
|
+
"password = config",
|
|
478
538
|
"password = settings",
|
|
479
|
-
"password = none",
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
"
|
|
483
|
-
"
|
|
484
|
-
"
|
|
485
|
-
"
|
|
539
|
+
"password = none",
|
|
540
|
+
"password = null",
|
|
541
|
+
"password_hash",
|
|
542
|
+
"password_field",
|
|
543
|
+
"password_input",
|
|
544
|
+
"password_reset",
|
|
545
|
+
"${",
|
|
546
|
+
"<%=",
|
|
547
|
+
"{{",
|
|
548
|
+
"placeholder",
|
|
549
|
+
"changeme",
|
|
550
|
+
"xxx",
|
|
551
|
+
"example",
|
|
552
|
+
"your_password",
|
|
553
|
+
"your-password",
|
|
554
|
+
"password_here",
|
|
555
|
+
"<password>",
|
|
486
556
|
]
|
|
487
557
|
for p in placeholders:
|
|
488
558
|
if p in lower:
|
|
@@ -494,6 +564,7 @@ def _is_password_placeholder(line: str) -> bool:
|
|
|
494
564
|
# Merge and deduplicate findings
|
|
495
565
|
# ---------------------------------------------------------------------------
|
|
496
566
|
|
|
567
|
+
|
|
497
568
|
def merge_findings(
|
|
498
569
|
bandit_results: list[dict[str, Any]],
|
|
499
570
|
regex_results: list[dict[str, Any]],
|
|
@@ -518,11 +589,9 @@ def merge_findings(
|
|
|
518
589
|
if len(finding.get("detail", "")) > len(existing.get("detail", "")):
|
|
519
590
|
seen[key] = finding
|
|
520
591
|
# If equal detail length, prefer higher severity
|
|
521
|
-
elif (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
< SEVERITY_ORDER.get(existing["severity"], 99)
|
|
525
|
-
):
|
|
592
|
+
elif len(finding.get("detail", "")) == len(existing.get("detail", "")) and SEVERITY_ORDER.get(
|
|
593
|
+
finding["severity"], 99
|
|
594
|
+
) < SEVERITY_ORDER.get(existing["severity"], 99):
|
|
526
595
|
seen[key] = finding
|
|
527
596
|
else:
|
|
528
597
|
seen[key] = finding
|
|
@@ -542,6 +611,7 @@ def merge_findings(
|
|
|
542
611
|
# Reporting
|
|
543
612
|
# ---------------------------------------------------------------------------
|
|
544
613
|
|
|
614
|
+
|
|
545
615
|
def generate_report(
|
|
546
616
|
directory: Path,
|
|
547
617
|
findings: list[dict[str, Any]],
|
|
@@ -671,6 +741,7 @@ def _write_json_report(
|
|
|
671
741
|
# CLI entry point
|
|
672
742
|
# ---------------------------------------------------------------------------
|
|
673
743
|
|
|
744
|
+
|
|
674
745
|
def main() -> None:
|
|
675
746
|
"""Parse arguments and run the security scanner."""
|
|
676
747
|
parser = argparse.ArgumentParser(
|
|
@@ -755,14 +826,10 @@ def main() -> None:
|
|
|
755
826
|
all_findings = merge_findings(bandit_results, regex_results)
|
|
756
827
|
|
|
757
828
|
# Apply severity filter
|
|
758
|
-
filtered_findings = [
|
|
759
|
-
f for f in all_findings
|
|
760
|
-
if _severity_at_or_above(f["severity"], severity_threshold)
|
|
761
|
-
]
|
|
829
|
+
filtered_findings = [f for f in all_findings if _severity_at_or_above(f["severity"], severity_threshold)]
|
|
762
830
|
|
|
763
831
|
_log(
|
|
764
|
-
f"Total: {len(all_findings)} finding(s), "
|
|
765
|
-
f"{len(filtered_findings)} at or above '{severity_threshold}' severity.",
|
|
832
|
+
f"Total: {len(all_findings)} finding(s), {len(filtered_findings)} at or above '{severity_threshold}' severity.",
|
|
766
833
|
verbose,
|
|
767
834
|
)
|
|
768
835
|
|
|
@@ -770,9 +837,7 @@ def main() -> None:
|
|
|
770
837
|
generate_report(directory, filtered_findings, args.output)
|
|
771
838
|
|
|
772
839
|
# Exit code based on critical/high findings
|
|
773
|
-
has_critical_or_high = any(
|
|
774
|
-
f["severity"] in ("critical", "high") for f in filtered_findings
|
|
775
|
-
)
|
|
840
|
+
has_critical_or_high = any(f["severity"] in ("critical", "high") for f in filtered_findings)
|
|
776
841
|
sys.exit(1 if has_critical_or_high else 0)
|
|
777
842
|
|
|
778
843
|
|