@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.
Files changed (112) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/README.md +8 -0
  3. package/commands/pentest.md +5 -0
  4. package/package.json +8 -3
  5. package/skills/analyzing-tls-config/SKILL.md +221 -0
  6. package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
  7. package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
  8. package/skills/analyzing-tls-config/references/THEORY.md +128 -0
  9. package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
  10. package/skills/auditing-cors-policy/SKILL.md +186 -0
  11. package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
  12. package/skills/auditing-cors-policy/references/THEORY.md +142 -0
  13. package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
  14. package/skills/auditing-npm-dependencies/SKILL.md +254 -0
  15. package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
  16. package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
  17. package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
  18. package/skills/auditing-python-dependencies/SKILL.md +251 -0
  19. package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
  20. package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
  21. package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
  22. package/skills/checking-http-security-headers/SKILL.md +176 -0
  23. package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
  24. package/skills/checking-http-security-headers/references/THEORY.md +137 -0
  25. package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
  26. package/skills/checking-license-compliance/SKILL.md +225 -0
  27. package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
  28. package/skills/checking-license-compliance/references/THEORY.md +152 -0
  29. package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
  30. package/skills/composing-vulnerability-report/SKILL.md +212 -0
  31. package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
  32. package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
  33. package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
  34. package/skills/confirming-pentest-authorization/SKILL.md +247 -0
  35. package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
  36. package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
  37. package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
  38. package/skills/defining-pentest-scope/SKILL.md +227 -0
  39. package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
  40. package/skills/defining-pentest-scope/references/THEORY.md +170 -0
  41. package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
  42. package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
  43. package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
  44. package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
  45. package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
  46. package/skills/detecting-debug-endpoints/SKILL.md +207 -0
  47. package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
  48. package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
  49. package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
  50. package/skills/detecting-directory-listing/SKILL.md +206 -0
  51. package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
  52. package/skills/detecting-directory-listing/references/THEORY.md +203 -0
  53. package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
  54. package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
  55. package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
  56. package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
  57. package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
  58. package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
  59. package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
  60. package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
  61. package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
  62. package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
  63. package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
  64. package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
  65. package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
  66. package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
  67. package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
  68. package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
  69. package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
  70. package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
  71. package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
  72. package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
  73. package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
  74. package/skills/detecting-weak-cryptography/SKILL.md +147 -0
  75. package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
  76. package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
  77. package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
  78. package/skills/fingerprinting-server-software/SKILL.md +191 -0
  79. package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
  80. package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
  81. package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
  82. package/skills/generating-executive-summary/SKILL.md +261 -0
  83. package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
  84. package/skills/generating-executive-summary/references/THEORY.md +195 -0
  85. package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
  86. package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
  87. package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
  88. package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
  89. package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
  90. package/skills/performing-penetration-testing/SKILL.md +282 -190
  91. package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
  92. package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
  93. package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
  94. package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
  95. package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
  96. package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
  97. package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
  98. package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
  99. package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
  100. package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
  101. package/skills/recording-pentest-engagement/SKILL.md +253 -0
  102. package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
  103. package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
  104. package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
  105. package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
  106. package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
  107. package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
  108. package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
  109. package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
  110. package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
  111. package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
  112. 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, Optional
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", ".js", ".ts", ".jsx", ".tsx",
48
- ".java", ".rb", ".go", ".php", ".sh",
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", "node_modules", "__pycache__", ".venv", "venv",
53
- ".tox", ".mypy_cache", ".pytest_cache", "dist", "build",
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", "high", "medium",
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", "critical", "high",
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", "high", "medium",
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", "critical", "high",
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", "high", "medium",
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", "high", "high",
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", "high", "medium",
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", "high", "medium",
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", "high", "high",
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", "high", "high",
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", "medium", "medium",
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", "medium", "medium",
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", "high", "high",
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", "high", "high",
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", "high", "medium",
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", "medium", "high",
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", "medium", "medium",
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", "medium", "medium",
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", "medium", "low",
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
- "tool": "bandit",
347
- "file": str(Path(issue.get("filename", "unknown")).resolve()),
348
- "line": issue.get("line_number", 0),
349
- "severity": _normalize_bandit_severity(issue.get("issue_severity", "LOW")),
350
- "confidence": _normalize_bandit_confidence(issue.get("issue_confidence", "LOW")),
351
- "category": issue.get("test_id", "unknown"),
352
- "title": issue.get("test_name", "Unknown issue"),
353
- "detail": issue.get("issue_text", ""),
354
- "remediation": "",
355
- "cwe": (
356
- f"CWE-{issue['issue_cwe']['id']}"
357
- if issue.get("issue_cwe", {}).get("id")
358
- else None
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
- "tool": "regex",
434
- "file": str(filepath.resolve()),
435
- "line": line_num,
436
- "severity": severity,
437
- "confidence": confidence,
438
- "category": category,
439
- "title": title,
440
- "detail": truncated_line,
441
- "remediation": remediation,
442
- "cwe": cwe,
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 = ""', "password = ''",
475
- 'password: ""', "password: ''",
476
- "password = os.environ", "password = os.getenv",
477
- "password = env(", "password = config",
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", "password = null",
480
- "password_hash", "password_field",
481
- "password_input", "password_reset",
482
- "${", "<%=", "{{",
483
- "placeholder", "changeme", "xxx", "example",
484
- "your_password", "your-password",
485
- "password_here", "<password>",
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
- len(finding.get("detail", "")) == len(existing.get("detail", ""))
523
- and SEVERITY_ORDER.get(finding["severity"], 99)
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