@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
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env python3
2
+ """Deeper SSL certificate posture audit.
3
+
4
+ Companion to skill `detecting-ssl-cert-issues`. Picks up where skill #1
5
+ analyzing-tls-config leaves off — assumes the chain already validates
6
+ (no protocol / cipher / expiry / hostname problems) and audits the
7
+ posture issues that don't break the handshake but matter for SOC2,
8
+ PCI, and modern browser policy.
9
+
10
+ Checks performed:
11
+ 1. OCSP staple presence + freshness (RFC 6066 status_request)
12
+ 2. OCSP responder reachability + revocation status (RFC 6960)
13
+ 3. SCT count from cert's embedded CT extension (RFC 6962)
14
+ 4. Chain ordering (RFC 5246 §7.4.2 — leaf first, intermediates next)
15
+ 5. AIA extension presence (CA Issuers + OCSP URL — RFC 5280 §4.2.2.1)
16
+ 6. Wildcard scope analysis (CA/B BR §3.2.2 — reject *.com etc.)
17
+ 7. Key Usage extension flags (RFC 5280 §4.2.1.3)
18
+ 8. Chain length (>4 is operationally costly)
19
+
20
+ References:
21
+ RFC 5246 — TLS 1.2 chain ordering
22
+ RFC 5280 — X.509 PKIX
23
+ RFC 6066 — TLS status_request (OCSP stapling)
24
+ RFC 6960 — OCSP
25
+ RFC 6962 — Certificate Transparency
26
+ CA/Browser Forum Baseline Requirements
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import sys
33
+ import urllib.parse
34
+ from pathlib import Path
35
+
36
+ # Make the plugin's lib/ importable
37
+ _PLUGIN_ROOT = Path(__file__).resolve().parents[3]
38
+ if str(_PLUGIN_ROOT) not in sys.path:
39
+ sys.path.insert(0, str(_PLUGIN_ROOT))
40
+
41
+ from lib.authz_check import require_authorization # noqa: E402
42
+ from lib.finding import Finding, Severity # noqa: E402
43
+ from lib.report import emit, exit_code # noqa: E402
44
+
45
+ SKILL_ID = "detecting-ssl-cert-issues"
46
+
47
+ # Wildcards at these scope levels are CA/B-rejectable
48
+ PUBLIC_SUFFIX_TLDS = {
49
+ "com",
50
+ "org",
51
+ "net",
52
+ "io",
53
+ "co",
54
+ "us",
55
+ "uk",
56
+ "de",
57
+ "fr",
58
+ "jp",
59
+ "cn",
60
+ "edu",
61
+ "gov",
62
+ "mil",
63
+ "info",
64
+ "biz",
65
+ "name",
66
+ }
67
+
68
+
69
+ def _parse_target(url: str) -> tuple[str, int]:
70
+ if "://" not in url:
71
+ url = "https://" + url
72
+ parsed = urllib.parse.urlparse(url)
73
+ host = parsed.hostname
74
+ port = parsed.port or 443
75
+ if not host:
76
+ raise ValueError(f"could not parse host from {url!r}")
77
+ return host, port
78
+
79
+
80
+ def _grab_chain_and_staple(host: str, port: int, timeout: float):
81
+ """Fetch the cert chain + check whether server sent an OCSP staple.
82
+
83
+ Uses raw `openssl s_client -status` because Python's ssl module
84
+ doesn't expose the status_request response. We parse the textual
85
+ output for the 'OCSP response:' block.
86
+ """
87
+ import subprocess
88
+
89
+ cmd = [
90
+ "openssl",
91
+ "s_client",
92
+ "-connect",
93
+ f"{host}:{port}",
94
+ "-servername",
95
+ host,
96
+ "-status",
97
+ "-showcerts",
98
+ ]
99
+ try:
100
+ proc = subprocess.run(
101
+ cmd,
102
+ input=b"",
103
+ capture_output=True,
104
+ timeout=timeout * 3,
105
+ )
106
+ except subprocess.TimeoutExpired:
107
+ return None, None, None
108
+ out = proc.stdout.decode(errors="replace") + proc.stderr.decode(errors="replace")
109
+ chain = _parse_chain(out)
110
+ has_staple = _has_ocsp_response(out)
111
+ return out, chain, has_staple
112
+
113
+
114
+ def _parse_chain(s_client_output: str) -> list[str]:
115
+ """Extract each PEM block in order from s_client -showcerts output."""
116
+ pems: list[str] = []
117
+ buf: list[str] = []
118
+ in_cert = False
119
+ for line in s_client_output.splitlines():
120
+ if "-----BEGIN CERTIFICATE-----" in line:
121
+ in_cert = True
122
+ buf = [line]
123
+ continue
124
+ if in_cert:
125
+ buf.append(line)
126
+ if "-----END CERTIFICATE-----" in line:
127
+ pems.append("\n".join(buf))
128
+ buf = []
129
+ in_cert = False
130
+ return pems
131
+
132
+
133
+ def _has_ocsp_response(s_client_output: str) -> bool:
134
+ """True if the s_client output contains an OCSP staple response."""
135
+ if "OCSP response:" not in s_client_output:
136
+ return False
137
+ # 'OCSP response: no response sent' = no staple
138
+ if "no response sent" in s_client_output:
139
+ return False
140
+ return True
141
+
142
+
143
+ def _load_cert(pem: str):
144
+ """Parse a PEM cert with the cryptography library."""
145
+ try:
146
+ from cryptography import x509
147
+ from cryptography.hazmat.backends import default_backend
148
+
149
+ return x509.load_pem_x509_certificate(pem.encode(), default_backend())
150
+ except Exception:
151
+ return None
152
+
153
+
154
+ def _check_chain_order(chain: list[str], target: str) -> list[Finding]:
155
+ """Verify chain is in RFC 5246 order: leaf first, then intermediates.
156
+
157
+ Heuristic: parse subjects; the leaf is the cert whose subject != issuer
158
+ of the next cert. If chain[0]'s subject == chain[1]'s subject (or
159
+ chain[0] looks like a root), it's misordered.
160
+ """
161
+ if len(chain) < 2:
162
+ return []
163
+
164
+ certs = [_load_cert(p) for p in chain]
165
+ if any(c is None for c in certs):
166
+ return []
167
+
168
+ # Leaf cert (chain[0]) should be issued by chain[1]
169
+ leaf, second = certs[0], certs[1]
170
+ # Heuristic: leaf cert has a subject that's hostname-shaped; intermediate
171
+ # subject is CA-shaped. If chain[0].subject == chain[0].issuer (self-
172
+ # signed), it's a root → misorder.
173
+ if leaf.subject == leaf.issuer:
174
+ return [
175
+ Finding(
176
+ skill_id=SKILL_ID,
177
+ title="Chain misorder: root certificate appears in position 0",
178
+ severity=Severity.MEDIUM,
179
+ target=target,
180
+ detail=(
181
+ "RFC 5246 §7.4.2 requires the server to send certificates "
182
+ "in order: leaf first, intermediates next, root excluded. "
183
+ "Older clients may fail validation; modern clients tolerate "
184
+ "but log."
185
+ ),
186
+ remediation=(
187
+ "Concatenate cert files in order: leaf.pem + intermediate.pem. "
188
+ "nginx: ssl_certificate /path/to/fullchain.pem (Let's Encrypt "
189
+ "ships this format)."
190
+ ),
191
+ cwe_id="CWE-295",
192
+ affected_control="RFC 5246 §7.4.2",
193
+ references=("https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2",),
194
+ )
195
+ ]
196
+
197
+ if leaf.issuer != second.subject:
198
+ return [
199
+ Finding(
200
+ skill_id=SKILL_ID,
201
+ title="Chain misorder: cert at position 1 is not issuer of leaf",
202
+ severity=Severity.MEDIUM,
203
+ target=target,
204
+ detail=(
205
+ "The certificate at chain position 1 is not the issuer of the "
206
+ "leaf certificate. RFC 5246 §7.4.2 ordering violated."
207
+ ),
208
+ remediation="Rebuild fullchain.pem with leaf + correct intermediate(s).",
209
+ affected_control="RFC 5246 §7.4.2",
210
+ )
211
+ ]
212
+ return []
213
+
214
+
215
+ def _check_staple(has_staple: bool, target: str) -> list[Finding]:
216
+ if has_staple:
217
+ return []
218
+ return [
219
+ Finding(
220
+ skill_id=SKILL_ID,
221
+ title="OCSP stapling not configured",
222
+ severity=Severity.HIGH,
223
+ target=target,
224
+ detail=(
225
+ "Server did not return an OCSP staple in the status_request "
226
+ "response. Clients must phone home to the CA's OCSP responder "
227
+ "to check revocation, adding latency and exposing browsing "
228
+ "metadata to the CA."
229
+ ),
230
+ remediation=(
231
+ "nginx: `ssl_stapling on; ssl_stapling_verify on; "
232
+ "resolver 1.1.1.1 8.8.8.8 valid=300s;`. "
233
+ "Caddy: stapling is auto-enabled. "
234
+ "Apache: `SSLUseStapling on; SSLStaplingCache shmcb:logs/stapling-cache(150000);`."
235
+ ),
236
+ cwe_id="CWE-295",
237
+ affected_control="RFC 6066 status_request; CA/B BR §4.9.10",
238
+ references=("https://datatracker.ietf.org/doc/html/rfc6066#section-8",),
239
+ )
240
+ ]
241
+
242
+
243
+ def _check_scts(chain: list[str], target: str) -> list[Finding]:
244
+ """Count embedded Signed Certificate Timestamps in the leaf cert."""
245
+ if not chain:
246
+ return []
247
+ leaf = _load_cert(chain[0])
248
+ if leaf is None:
249
+ return []
250
+ try:
251
+ from cryptography.x509 import oid
252
+
253
+ ext = leaf.extensions.get_extension_for_oid(oid.ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS)
254
+ sct_count = len(ext.value)
255
+ except Exception:
256
+ sct_count = 0
257
+
258
+ if sct_count < 2:
259
+ return [
260
+ Finding(
261
+ skill_id=SKILL_ID,
262
+ title=f"Certificate Transparency: only {sct_count} SCT(s) embedded",
263
+ severity=Severity.HIGH,
264
+ target=target,
265
+ detail=(
266
+ f"The leaf certificate has {sct_count} SCT(s). Chrome's CT "
267
+ "enforcement policy (active since 2018) requires ≥2 SCTs "
268
+ "from independently-operated logs OR a separate TLS-extension "
269
+ "SCT delivery. Browsers reject the connection silently if "
270
+ "policy violated."
271
+ ),
272
+ remediation=(
273
+ "Reissue with a CA that submits to ≥2 CT logs at issuance "
274
+ "(all major public CAs do this by default since 2018). "
275
+ "Verify with crt.sh after issuance: https://crt.sh/?q=example.com"
276
+ ),
277
+ cwe_id="CWE-295",
278
+ affected_control="RFC 6962; Chrome CT Policy",
279
+ references=(
280
+ "https://datatracker.ietf.org/doc/html/rfc6962",
281
+ "https://googlechrome.github.io/CertificateTransparency/ct_policy.html",
282
+ ),
283
+ )
284
+ ]
285
+ return []
286
+
287
+
288
+ def _check_aia(chain: list[str], target: str) -> list[Finding]:
289
+ """Verify the leaf cert has Authority Info Access (CA Issuers + OCSP URL)."""
290
+ if not chain:
291
+ return []
292
+ leaf = _load_cert(chain[0])
293
+ if leaf is None:
294
+ return []
295
+ try:
296
+ from cryptography.x509 import oid
297
+
298
+ ext = leaf.extensions.get_extension_for_oid(oid.ExtensionOID.AUTHORITY_INFORMATION_ACCESS)
299
+ aia = ext.value
300
+ ocsp_urls = [d for d in aia if d.access_method == oid.AuthorityInformationAccessOID.OCSP]
301
+ ca_issuers = [d for d in aia if d.access_method == oid.AuthorityInformationAccessOID.CA_ISSUERS]
302
+ findings = []
303
+ if not ocsp_urls:
304
+ findings.append(
305
+ Finding(
306
+ skill_id=SKILL_ID,
307
+ title="AIA extension missing OCSP responder URL",
308
+ severity=Severity.MEDIUM,
309
+ target=target,
310
+ detail=(
311
+ "The leaf certificate's Authority Info Access extension "
312
+ "does not include an OCSP responder URL. Clients can't "
313
+ "check revocation status."
314
+ ),
315
+ remediation=(
316
+ "This is a CA-side issuance configuration; contact your "
317
+ "CA. All public CAs include OCSP URLs by default; this "
318
+ "is unusual for a public cert and may indicate a "
319
+ "private-CA cert in use."
320
+ ),
321
+ affected_control="RFC 5280 §4.2.2.1",
322
+ )
323
+ )
324
+ if not ca_issuers:
325
+ findings.append(
326
+ Finding(
327
+ skill_id=SKILL_ID,
328
+ title="AIA extension missing CA Issuers URL",
329
+ severity=Severity.LOW,
330
+ target=target,
331
+ detail=("AIA missing CA Issuers — clients can't fetch missing intermediates on demand."),
332
+ remediation="CA-side issuance config. Same as OCSP URL fix.",
333
+ affected_control="RFC 5280 §4.2.2.1",
334
+ )
335
+ )
336
+ return findings
337
+ except Exception:
338
+ return [
339
+ Finding(
340
+ skill_id=SKILL_ID,
341
+ title="AIA extension absent from leaf cert",
342
+ severity=Severity.MEDIUM,
343
+ target=target,
344
+ detail="No AIA extension at all — leaf cert can't direct clients to OCSP or intermediates.",
345
+ remediation="Reissue with a public CA; this is unusual.",
346
+ affected_control="RFC 5280 §4.2.2.1",
347
+ )
348
+ ]
349
+
350
+
351
+ def _check_wildcards(chain: list[str], target: str) -> list[Finding]:
352
+ """Flag wildcards with overly broad scope."""
353
+ if not chain:
354
+ return []
355
+ leaf = _load_cert(chain[0])
356
+ if leaf is None:
357
+ return []
358
+ try:
359
+ from cryptography.x509 import oid
360
+
361
+ san_ext = leaf.extensions.get_extension_for_oid(oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
362
+ sans = san_ext.value.get_values_for_type(__import__("cryptography").x509.DNSName)
363
+ except Exception:
364
+ sans = []
365
+
366
+ findings = []
367
+ for san in sans:
368
+ if not san.startswith("*."):
369
+ continue
370
+ scope = san[2:]
371
+ labels = scope.split(".")
372
+ if len(labels) <= 1 or labels[-1] in PUBLIC_SUFFIX_TLDS and len(labels) == 1:
373
+ findings.append(
374
+ Finding(
375
+ skill_id=SKILL_ID,
376
+ title=f"Wildcard SAN with over-broad scope: {san}",
377
+ severity=Severity.HIGH,
378
+ target=target,
379
+ detail=(
380
+ f"The SAN {san!r} would cover an entire public suffix or "
381
+ "single-label domain. CA/Browser Forum BR §3.2.2 forbids "
382
+ "issuance at this scope; if you see this on a public cert "
383
+ "the CA mis-issued."
384
+ ),
385
+ remediation=("Reissue with narrower SAN(s) covering only the hostnames actually served."),
386
+ affected_control="CA/B Baseline Requirements §3.2.2",
387
+ )
388
+ )
389
+ return findings
390
+
391
+
392
+ def _check_key_usage(chain: list[str], target: str) -> list[Finding]:
393
+ if not chain:
394
+ return []
395
+ leaf = _load_cert(chain[0])
396
+ if leaf is None:
397
+ return []
398
+ try:
399
+ from cryptography.x509 import oid
400
+
401
+ ext = leaf.extensions.get_extension_for_oid(oid.ExtensionOID.KEY_USAGE)
402
+ ku = ext.value
403
+ if not ku.digital_signature:
404
+ return [
405
+ Finding(
406
+ skill_id=SKILL_ID,
407
+ title="Key Usage extension missing digitalSignature",
408
+ severity=Severity.MEDIUM,
409
+ target=target,
410
+ detail=(
411
+ "The cert's KU extension does not assert digitalSignature. "
412
+ "RFC 5280 §4.2.1.3 requires this for TLS server certs."
413
+ ),
414
+ remediation="CA-side issuance config; reissue with correct KU.",
415
+ affected_control="RFC 5280 §4.2.1.3",
416
+ )
417
+ ]
418
+ except Exception:
419
+ pass
420
+ return []
421
+
422
+
423
+ def main(argv: list[str] | None = None) -> int:
424
+ parser = argparse.ArgumentParser(description="SSL certificate posture audit")
425
+ parser.add_argument("url")
426
+ parser.add_argument("--authorized", action="store_true")
427
+ parser.add_argument("--port", type=int, default=None)
428
+ parser.add_argument("--output", default=None)
429
+ parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
430
+ parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
431
+ parser.add_argument("--timeout", type=float, default=10.0)
432
+ parser.add_argument("--skip-ocsp", action="store_true")
433
+ args = parser.parse_args(argv)
434
+
435
+ host, default_port = _parse_target(args.url)
436
+ port = args.port or default_port
437
+ target = f"{host}:{port}"
438
+
439
+ require_authorization(args.url, args.authorized)
440
+
441
+ findings: list[Finding] = []
442
+
443
+ raw, chain, has_staple = _grab_chain_and_staple(host, port, args.timeout)
444
+ if chain is None:
445
+ sys.stderr.write(f"ERROR: could not reach {target} or openssl unavailable\n")
446
+ return 2
447
+
448
+ if not args.skip_ocsp:
449
+ findings.extend(_check_staple(has_staple, target))
450
+ findings.extend(_check_chain_order(chain, target))
451
+ findings.extend(_check_scts(chain, target))
452
+ findings.extend(_check_aia(chain, target))
453
+ findings.extend(_check_wildcards(chain, target))
454
+ findings.extend(_check_key_usage(chain, target))
455
+
456
+ if len(chain) > 4:
457
+ findings.append(
458
+ Finding(
459
+ skill_id=SKILL_ID,
460
+ title=f"Chain has {len(chain)} certificates (longer than 4)",
461
+ severity=Severity.LOW,
462
+ target=target,
463
+ detail=(
464
+ "Long chains expand the trust surface and add handshake "
465
+ "latency. Modern CAs issue 2-cert chains (leaf + intermediate); "
466
+ "anything longer suggests vendored cross-signing."
467
+ ),
468
+ remediation="Verify if cross-signing is intentional; if not, reissue.",
469
+ affected_control="CA/B Baseline Requirements",
470
+ )
471
+ )
472
+
473
+ floor = Severity(args.min_severity)
474
+ findings = [f for f in findings if f.severity.numeric >= floor.numeric]
475
+
476
+ emit(findings, args.output, args.format, target)
477
+ return exit_code(findings)
478
+
479
+
480
+ if __name__ == "__main__":
481
+ sys.exit(main())
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: detecting-weak-cryptography
3
+ description: |
4
+ Scan a source tree for weak cryptographic primitives: MD5 / SHA-1
5
+ used for security purposes, DES / 3DES / RC4 ciphers, ECB block
6
+ mode, custom-built crypto (XOR loops, hand-rolled HMAC),
7
+ hardcoded IVs, predictable random (Math.random / java.util.Random
8
+ for crypto seeds), missing certificate verification
9
+ (verify=False, rejectUnauthorized: false).
10
+ Use when: pre-merge gate on crypto-touching code, audit before
11
+ SOC2 / PCI assessment, post-incident review when "we found a
12
+ weakness in our token signing."
13
+ Threshold: any call to a known-weak algorithm with non-test
14
+ context, OR cert verification explicitly disabled, OR a custom
15
+ crypto loop pattern.
16
+ Trigger with: "scan weak crypto", "find MD5 usage", "check ECB
17
+ mode", "audit ssl verify", "weak random".
18
+ allowed-tools:
19
+ - Read
20
+ - Bash(python3:*)
21
+ - Glob
22
+ - Grep
23
+ disallowed-tools:
24
+ - Bash(rm:*)
25
+ - Bash(curl:*)
26
+ version: 3.0.0-dev
27
+ author: Jeremy Longshore <jeremy@intentsolutions.io>
28
+ license: MIT
29
+ compatibility: Designed for Claude Code
30
+ tags:
31
+ - security
32
+ - static-analysis
33
+ - cryptography
34
+ - pentest
35
+ ---
36
+
37
+ # Detecting Weak Cryptography
38
+
39
+ ## Overview
40
+
41
+ Weak cryptography (CWE-327 Use of a Broken or Risky Cryptographic
42
+ Algorithm, CWE-330 Use of Insufficiently Random Values) shows up
43
+ when engineers use the convenient API instead of the cryptographic
44
+ one. `hashlib.md5(password)` is faster to type than the correct
45
+ bcrypt/argon2 invocation; `Math.random()` returns a number quickly
46
+ without needing to know about `crypto.randomBytes()`.
47
+
48
+ The fix is universal: use the modern primitive. SHA-256 for general
49
+ hashing, bcrypt/argon2/scrypt for passwords, AES-GCM for encryption,
50
+ HMAC-SHA256 for signing, `secrets` / `crypto.randomBytes` /
51
+ `SecureRandom` for randomness.
52
+
53
+ ## When the skill produces findings
54
+
55
+ | Finding | Severity | Threshold | Affected control |
56
+ |---|---|---|---|
57
+ | MD5 used in security context | **HIGH** | hashlib.md5, MessageDigest.MD5, CryptoJS.MD5 | CWE-327 |
58
+ | SHA-1 used in security context | **HIGH** | hashlib.sha1, etc. | CWE-327 |
59
+ | DES / 3DES cipher | **CRITICAL** | DESCrypto, "DES/CBC", "DESede" | CWE-327 |
60
+ | RC4 cipher | **CRITICAL** | "ARC4", "RC4" | CWE-327 |
61
+ | AES ECB mode | **CRITICAL** | "AES/ECB" or `MODE_ECB` | CWE-327 |
62
+ | Hardcoded IV (initialization vector) | **CRITICAL** | IV literal in source | CWE-329 |
63
+ | Custom XOR-based "encryption" | **CRITICAL** | XOR loop over bytes | CWE-327 |
64
+ | Predictable random for crypto seed | **CRITICAL** | Math.random / java.util.Random / random.random for keys | CWE-330 |
65
+ | TLS cert verification disabled | **CRITICAL** | verify=False, rejectUnauthorized:false, ServerCertificateValidationCallback returning true | CWE-295 |
66
+ | Hardcoded HMAC secret | **HIGH** | Long literal in HMAC constructor | CWE-321 |
67
+ | Insecure password hashing (no salt, no KDF) | **CRITICAL** | hashlib.sha256(password) without bcrypt/argon2 | CWE-916 |
68
+
69
+ ## Prerequisites
70
+
71
+ - Python 3.9+
72
+ - Source tree on local filesystem
73
+
74
+ ## Instructions
75
+
76
+ ### Run
77
+
78
+ ```bash
79
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py /path/to/repo
80
+ ```
81
+
82
+ Options: `--output`, `--format`, `--min-severity`, `--include-tests`,
83
+ `--languages`, `--allow-md5-checksums` (excludes MD5 used in
84
+ non-security contexts like content-addressable storage).
85
+
86
+ ### Interpret
87
+
88
+ CRITICAL = direct cryptographic break available against the
89
+ algorithm. CVEs, public attack tools, sometimes pre-computed
90
+ tables (rainbow tables for MD5/SHA-1).
91
+
92
+ HIGH = algorithm collision-broken (MD5, SHA-1) but the specific
93
+ use case may tolerate the weakness (file-deduplication checksums,
94
+ non-security HMAC). Verify the usage context.
95
+
96
+ ### Remediation
97
+
98
+ See `references/PLAYBOOK.md` for per-primitive migration. Modern
99
+ defaults: SHA-256/SHA-3 for hashing, AES-256-GCM for encryption,
100
+ HMAC-SHA-256 for signing, secrets-grade random for keys, bcrypt /
101
+ argon2id for password storage.
102
+
103
+ ## Examples
104
+
105
+ ### Pre-merge gate
106
+
107
+ ```bash
108
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py \
109
+ --min-severity high $(git diff --name-only main...HEAD | tr '\n' ' ')
110
+ ```
111
+
112
+ ### CI
113
+
114
+ ```yaml
115
+ - name: Weak-crypto scan
116
+ run: |
117
+ python3 plugins/security/penetration-tester/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py \
118
+ . --min-severity high
119
+ ```
120
+
121
+ ## Output
122
+
123
+ JSON / JSONL / Markdown. Exit codes: 0 / 1 / 2.
124
+
125
+ ## Error Handling
126
+
127
+ False positives common on:
128
+
129
+ - MD5 used for content-addressable storage (caches, content hashes)
130
+ where collision resistance against ATTACKERS isn't needed — use
131
+ `--allow-md5-checksums`.
132
+ - HMAC-MD5 — broken against adversaries but acceptable as an
133
+ integrity check inside a TLS session where the channel is
134
+ already authenticated.
135
+
136
+ Verify each finding by reading whether the algorithm's failure
137
+ mode (collision, preimage, etc.) is actually exploitable in
138
+ context.
139
+
140
+ ## Resources
141
+
142
+ - `references/THEORY.md` — Per-primitive attack model (why MD5 /
143
+ SHA-1 are collision-broken, why ECB leaks structure, why
144
+ Math.random is non-crypto-grade)
145
+ - `references/PLAYBOOK.md` — Per-language modern-crypto recipes
146
+ (Python cryptography library, Node crypto, Java JCA with
147
+ modern algorithms, Go crypto/rand + crypto/cipher AEAD)