@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,415 @@
1
+ #!/usr/bin/env python3
2
+ """TLS configuration analyzer for penetration-tester v3.
3
+
4
+ Companion to skill `analyzing-tls-config`. Per AT-ADEC at the plugin root
5
+ 000-docs/001-AT-ADEC-skill-taxonomy.md, this script imports from the shared
6
+ lib/ module so per-skill plumbing stays narrow.
7
+
8
+ Checks performed (with thresholds):
9
+ 1. Negotiated protocol version — flag TLSv1.0 / TLSv1.1 (HIGH)
10
+ 2. Negotiated cipher suite — flag null / EXPORT / aNULL (CRITICAL),
11
+ flag RC4 / 3DES (HIGH), absence of ECDHE/DHE (MEDIUM forward-secrecy)
12
+ 3. Certificate expiry — <7 days (CRITICAL), <30 days (HIGH)
13
+ 4. Certificate hostname match (SAN/CN) — RFC 6125 (HIGH on mismatch)
14
+ 5. Chain validates to system CA store — self-signed/untrusted (MEDIUM)
15
+ 6. Public-key bit length — RSA < 2048 / ECDSA < 256 (HIGH)
16
+
17
+ References:
18
+ NIST SP 800-52r2 §3 (protocol versions, cipher suites, key sizes)
19
+ Mozilla TLS Configuration Generator (https://ssl-config.mozilla.org/)
20
+ PCI DSS v4.0 Req 4.2.1.1 (strong cryptography)
21
+ RFC 6125 (hostname verification)
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import datetime
28
+ import socket
29
+ import ssl
30
+ import sys
31
+ import urllib.parse
32
+ from pathlib import Path
33
+
34
+ # Make the plugin's lib/ importable when invoked via CLAUDE_PLUGIN_ROOT path.
35
+ _PLUGIN_ROOT = Path(__file__).resolve().parents[3]
36
+ if str(_PLUGIN_ROOT) not in sys.path:
37
+ sys.path.insert(0, str(_PLUGIN_ROOT))
38
+
39
+ from lib.authz_check import require_authorization # noqa: E402
40
+ from lib.finding import Finding, Severity # noqa: E402
41
+ from lib.report import emit, exit_code # noqa: E402
42
+
43
+
44
+ SKILL_ID = "analyzing-tls-config"
45
+
46
+
47
+ # Weak-cipher fragments (substring match against negotiated cipher name)
48
+ NULL_CIPHER_TOKENS = ("NULL", "aNULL", "eNULL", "EXPORT")
49
+ WEAK_CIPHER_TOKENS = ("RC4", "3DES", "DES-CBC", "MD5", "IDEA", "SEED")
50
+ FORWARD_SECRECY_TOKENS = ("ECDHE", "DHE")
51
+
52
+ # Protocol-version thresholds
53
+ OBSOLETE_PROTOCOLS = {"SSLv2", "SSLv3", "TLSv1", "TLSv1.0", "TLSv1.1"}
54
+
55
+ # Key-size thresholds (NIST SP 800-52r2 §3.4)
56
+ MIN_RSA_BITS = 2048
57
+ MIN_EC_BITS = 256
58
+
59
+ # Certificate expiry thresholds
60
+ EXPIRY_CRITICAL_DAYS = 7
61
+ EXPIRY_HIGH_DAYS = 30
62
+
63
+
64
+ def _parse_target(url: str) -> tuple[str, int]:
65
+ """Return (host, port) for a target URL or bare host[:port]."""
66
+ if "://" not in url:
67
+ url = "https://" + url
68
+ parsed = urllib.parse.urlparse(url)
69
+ host = parsed.hostname
70
+ port = parsed.port or 443
71
+ if not host:
72
+ raise ValueError(f"could not parse host from {url!r}")
73
+ return host, port
74
+
75
+
76
+ def _grab_cert_and_cipher(host: str, port: int, timeout: float) -> tuple[dict, tuple, str, ssl.SSLContext]:
77
+ """Open a TLS connection and capture cert + cipher + protocol version.
78
+
79
+ Uses default SSL context (system CA bundle, hostname verification on).
80
+ For chain-untrusted targets a second pass with verification disabled is
81
+ needed for cert-detail capture — handled in _grab_untrusted_cert below.
82
+ """
83
+ ctx = ssl.create_default_context()
84
+ with socket.create_connection((host, port), timeout=timeout) as raw_sock:
85
+ with ctx.wrap_socket(raw_sock, server_hostname=host) as ssock:
86
+ cert = ssock.getpeercert()
87
+ cipher = ssock.cipher() # (name, version, bits)
88
+ version = ssock.version()
89
+ return cert, cipher, version, ctx
90
+
91
+
92
+ def _grab_untrusted_cert(host: str, port: int, timeout: float) -> tuple[dict | None, tuple | None, str | None]:
93
+ """Capture cert details when the trust check would otherwise refuse.
94
+
95
+ Used to report on self-signed / expired / untrusted-chain targets WITHOUT
96
+ masking the trust failure — the caller emits a separate finding for the
97
+ chain failure, then this captures details for follow-on cert findings.
98
+ """
99
+ ctx = ssl._create_unverified_context()
100
+ try:
101
+ with socket.create_connection((host, port), timeout=timeout) as raw_sock:
102
+ with ctx.wrap_socket(raw_sock, server_hostname=host) as ssock:
103
+ # ssock.getpeercert(binary_form=True) → raw DER; we want parsed
104
+ cert = ssock.getpeercert()
105
+ cipher = ssock.cipher()
106
+ version = ssock.version()
107
+ return cert, cipher, version
108
+ except (ssl.SSLError, socket.error):
109
+ return None, None, None
110
+
111
+
112
+ def _cert_expiry_dt(cert: dict) -> datetime.datetime | None:
113
+ """Parse 'notAfter' into UTC datetime. Returns None if absent or malformed."""
114
+ not_after = cert.get("notAfter")
115
+ if not not_after:
116
+ return None
117
+ # Format: 'May 29 15:30:00 2026 GMT'
118
+ try:
119
+ return datetime.datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=datetime.timezone.utc)
120
+ except ValueError:
121
+ return None
122
+
123
+
124
+ def _check_protocol(version: str, target: str) -> list[Finding]:
125
+ if version in OBSOLETE_PROTOCOLS:
126
+ return [
127
+ Finding(
128
+ skill_id=SKILL_ID,
129
+ title=f"Server negotiates obsolete {version}",
130
+ severity=Severity.HIGH,
131
+ target=target,
132
+ detail=(
133
+ f"The TLS handshake completed using {version}, which is "
134
+ "deprecated by NIST SP 800-52r2 §3.1 and PCI DSS v4.0 "
135
+ "Req 4.2.1.1. Modern clients can still negotiate this "
136
+ "version, leaving the connection vulnerable to known "
137
+ "downgrade attacks (e.g., POODLE on SSLv3, BEAST on "
138
+ "TLSv1.0)."
139
+ ),
140
+ remediation=(
141
+ "Configure the server to require TLSv1.2 minimum, prefer "
142
+ "TLSv1.3. nginx: `ssl_protocols TLSv1.2 TLSv1.3;`. "
143
+ "Caddy: TLSv1.2 is the default minimum since v2."
144
+ ),
145
+ cwe_id="CWE-326",
146
+ owasp_category="A02:2021",
147
+ affected_control="NIST 800-52r2 §3.1; PCI DSS v4.0 Req 4.2.1.1",
148
+ references=(
149
+ "https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r2.pdf",
150
+ "https://www.pcisecuritystandards.org/document_library/",
151
+ ),
152
+ )
153
+ ]
154
+ return []
155
+
156
+
157
+ def _check_cipher(cipher: tuple, target: str) -> list[Finding]:
158
+ name = cipher[0] if cipher else ""
159
+ findings: list[Finding] = []
160
+
161
+ for token in NULL_CIPHER_TOKENS:
162
+ if token in name:
163
+ findings.append(
164
+ Finding(
165
+ skill_id=SKILL_ID,
166
+ title=f"Server negotiates null/anonymous cipher: {name}",
167
+ severity=Severity.CRITICAL,
168
+ target=target,
169
+ detail=(
170
+ f"The negotiated cipher suite {name} provides no "
171
+ "confidentiality (NULL) or no authentication "
172
+ "(aNULL/EXPORT). Any in-path attacker reads cleartext."
173
+ ),
174
+ remediation=(
175
+ "Remove !NULL:!aNULL:!EXPORT from cipher list. "
176
+ "nginx: `ssl_ciphers HIGH:!aNULL:!MD5:!EXPORT;`. "
177
+ "Prefer Mozilla's intermediate-config generator output."
178
+ ),
179
+ cwe_id="CWE-327",
180
+ affected_control="NIST 800-52r2 §3.3.1",
181
+ references=("https://ssl-config.mozilla.org/",),
182
+ )
183
+ )
184
+ break
185
+
186
+ for token in WEAK_CIPHER_TOKENS:
187
+ if token in name:
188
+ findings.append(
189
+ Finding(
190
+ skill_id=SKILL_ID,
191
+ title=f"Server negotiates weak cipher: {name}",
192
+ severity=Severity.HIGH,
193
+ target=target,
194
+ detail=(
195
+ f"The negotiated cipher suite {name} contains a "
196
+ "deprecated algorithm. RC4 has documented bias attacks "
197
+ "(RFC 7465); 3DES is vulnerable to Sweet32 (CVE-2016-2183); "
198
+ "MD5/IDEA/SEED are no longer NIST-approved."
199
+ ),
200
+ remediation=(
201
+ "Remove !RC4:!3DES:!MD5 from cipher list and require "
202
+ "AEAD suites (AES-GCM, CHACHA20-POLY1305). Use "
203
+ "Mozilla's intermediate config for backward "
204
+ "compatibility, modern config for greenfield."
205
+ ),
206
+ cwe_id="CWE-327",
207
+ affected_control="NIST 800-52r2 §3.3.1; PCI DSS v4.0 Req 4.2.1.1",
208
+ references=(
209
+ "https://datatracker.ietf.org/doc/html/rfc7465",
210
+ "https://sweet32.info/",
211
+ ),
212
+ )
213
+ )
214
+ break
215
+
216
+ if name and not any(t in name for t in FORWARD_SECRECY_TOKENS):
217
+ findings.append(
218
+ Finding(
219
+ skill_id=SKILL_ID,
220
+ title=f"Negotiated cipher lacks forward secrecy: {name}",
221
+ severity=Severity.MEDIUM,
222
+ target=target,
223
+ detail=(
224
+ "The cipher suite does not use ECDHE or DHE. If the "
225
+ "server's private key is later compromised, all past "
226
+ "session traffic captured by an adversary can be decrypted."
227
+ ),
228
+ remediation=(
229
+ "Configure the server to prefer ECDHE-based cipher suites. "
230
+ "Mozilla's intermediate config achieves this on every "
231
+ "supported server type."
232
+ ),
233
+ cwe_id="CWE-326",
234
+ affected_control="NIST 800-52r2 §3.3.1",
235
+ references=("https://ssl-config.mozilla.org/",),
236
+ )
237
+ )
238
+
239
+ return findings
240
+
241
+
242
+ def _check_expiry(cert: dict, target: str) -> list[Finding]:
243
+ expiry = _cert_expiry_dt(cert)
244
+ if expiry is None:
245
+ return []
246
+ now = datetime.datetime.now(datetime.timezone.utc)
247
+ delta = expiry - now
248
+ days = delta.days
249
+
250
+ if days <= EXPIRY_CRITICAL_DAYS:
251
+ return [
252
+ Finding(
253
+ skill_id=SKILL_ID,
254
+ title=f"Certificate expires in {days} days",
255
+ severity=Severity.CRITICAL,
256
+ target=target,
257
+ detail=(
258
+ f"The presented certificate notAfter is {expiry.isoformat()}, "
259
+ f"only {days} day(s) away. Without renewal, the next handshake "
260
+ "will fail and the service will be unreachable to any "
261
+ "trust-validating client."
262
+ ),
263
+ remediation=(
264
+ "Renew the certificate immediately. If using Let's Encrypt, "
265
+ "force `certbot renew --force-renewal`. Verify the new cert "
266
+ "is in the server's configured key path and that the server "
267
+ "has reloaded (systemctl reload nginx / caddy reload)."
268
+ ),
269
+ affected_control="NIST 800-52r2 §4.1",
270
+ )
271
+ ]
272
+ if days <= EXPIRY_HIGH_DAYS:
273
+ return [
274
+ Finding(
275
+ skill_id=SKILL_ID,
276
+ title=f"Certificate expires in {days} days",
277
+ severity=Severity.HIGH,
278
+ target=target,
279
+ detail=(
280
+ f"The presented certificate notAfter is {expiry.isoformat()}. "
281
+ f"Renewal is recommended ≥30 days before expiry to allow "
282
+ "deployment and OCSP-stapling cache warmup."
283
+ ),
284
+ remediation=(
285
+ "Schedule renewal. Verify your renewal automation "
286
+ "(certbot.timer, caddy auto-cert) is running and has "
287
+ "current credentials for the DNS/HTTP challenge."
288
+ ),
289
+ affected_control="NIST 800-52r2 §4.1",
290
+ )
291
+ ]
292
+ return []
293
+
294
+
295
+ def _check_hostname(cert: dict, host: str, target: str) -> list[Finding]:
296
+ # The default-context handshake already validated hostname; if we got here
297
+ # via the trusted path, hostname matches by definition. This check exists
298
+ # for the untrusted-cert path.
299
+ sans = []
300
+ for entry in cert.get("subjectAltName", ()):
301
+ if entry[0].lower() == "dns":
302
+ sans.append(entry[1].lower())
303
+ subject = dict(x[0] for x in cert.get("subject", ()))
304
+ cn = (subject.get("commonName") or "").lower()
305
+
306
+ host_l = host.lower()
307
+ if host_l in sans:
308
+ return []
309
+ if any(host_l.endswith(s.lstrip("*")) for s in sans if s.startswith("*.")):
310
+ return []
311
+ if cn and host_l == cn:
312
+ return []
313
+ if cn.startswith("*.") and host_l.endswith(cn[1:]):
314
+ return []
315
+
316
+ return [
317
+ Finding(
318
+ skill_id=SKILL_ID,
319
+ title=f"Certificate hostname does not match target ({host})",
320
+ severity=Severity.HIGH,
321
+ target=target,
322
+ detail=(
323
+ f"The cert SAN/CN values ({sans or [cn]!r}) do not include "
324
+ f"the target host {host!r}. RFC 6125 requires the client to "
325
+ "reject this; some legacy clients may still accept it, opening "
326
+ "an MITM window."
327
+ ),
328
+ remediation=(
329
+ "Reissue the certificate with the correct SAN list including "
330
+ "every hostname the service responds on. Modern CAs (Let's "
331
+ "Encrypt, ZeroSSL) accept SAN lists at issuance."
332
+ ),
333
+ cwe_id="CWE-297",
334
+ affected_control="RFC 6125 §6",
335
+ references=("https://datatracker.ietf.org/doc/html/rfc6125",),
336
+ )
337
+ ]
338
+
339
+
340
+ def main(argv: list[str] | None = None) -> int:
341
+ parser = argparse.ArgumentParser(description="TLS configuration analyzer")
342
+ parser.add_argument("url", help="Target URL (https://...) or host[:port]")
343
+ parser.add_argument(
344
+ "--authorized", action="store_true", help="Attest authorization for non-local targets (required)"
345
+ )
346
+ parser.add_argument("--port", type=int, default=None, help="Target port (overrides URL port; default 443)")
347
+ parser.add_argument("--output", default=None, help="Output file (default: stdout)")
348
+ parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
349
+ parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
350
+ parser.add_argument("--timeout", type=float, default=10.0)
351
+ args = parser.parse_args(argv)
352
+
353
+ host, default_port = _parse_target(args.url)
354
+ port = args.port or default_port
355
+ target_display = f"{host}:{port}"
356
+
357
+ # Authorization gate (lib/authz_check applies the local-target carve-out)
358
+ require_authorization(args.url, args.authorized)
359
+
360
+ findings: list[Finding] = []
361
+
362
+ # Phase 1: try trusted handshake
363
+ trusted_cert: dict | None = None
364
+ cipher: tuple | None = None
365
+ version: str | None = None
366
+ try:
367
+ trusted_cert, cipher, version, _ = _grab_cert_and_cipher(host, port, args.timeout)
368
+ except ssl.SSLCertVerificationError as exc:
369
+ # Chain trust failed — emit finding + fall back to untrusted handshake
370
+ findings.append(
371
+ Finding(
372
+ skill_id=SKILL_ID,
373
+ title="Certificate chain does not validate to system trust store",
374
+ severity=Severity.MEDIUM,
375
+ target=target_display,
376
+ detail=(
377
+ f"openssl/ssl rejected the chain: {exc!s}. Common causes: "
378
+ "self-signed certificate, intermediate certificate missing "
379
+ "from the server response, or expired/revoked root in the "
380
+ "server's chain."
381
+ ),
382
+ remediation=(
383
+ "Verify the server returns the full chain (cert + all "
384
+ "intermediates, NOT the root). Run `openssl s_client -connect "
385
+ f"{target_display} -showcerts` and confirm each intermediate is "
386
+ "present. Reissue with a trusted CA if currently self-signed."
387
+ ),
388
+ cwe_id="CWE-295",
389
+ affected_control="Mozilla TLS Guidelines",
390
+ )
391
+ )
392
+ cert_fallback, cipher, version = _grab_untrusted_cert(host, port, args.timeout)
393
+ trusted_cert = cert_fallback
394
+ except (socket.error, ssl.SSLError) as exc:
395
+ sys.stderr.write(f"ERROR: failed to connect to {target_display}: {exc}\n")
396
+ return 2
397
+
398
+ if version:
399
+ findings.extend(_check_protocol(version, target_display))
400
+ if cipher:
401
+ findings.extend(_check_cipher(cipher, target_display))
402
+ if trusted_cert:
403
+ findings.extend(_check_expiry(trusted_cert, target_display))
404
+ findings.extend(_check_hostname(trusted_cert, host, target_display))
405
+
406
+ # Severity floor
407
+ floor = Severity(args.min_severity)
408
+ findings = [f for f in findings if f.severity.numeric >= floor.numeric]
409
+
410
+ emit(findings, args.output, args.format, target_display)
411
+ return exit_code(findings)
412
+
413
+
414
+ if __name__ == "__main__":
415
+ sys.exit(main())
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: auditing-cors-policy
3
+ description: |
4
+ Audit a target's CORS posture — Access-Control-Allow-Origin handling,
5
+ reflected-origin bypass, credentials+wildcard mismatch, preflight
6
+ OPTIONS behavior, Vary header correctness.
7
+ Use when: a third-party integration is failing CORS preflight and
8
+ someone proposes "just set Allow-Origin to *" as the fix, OR your
9
+ bug-bounty inbox has a credential-reuse exploit chain.
10
+ Threshold: any reflection of arbitrary Origin into Allow-Origin,
11
+ Allow-Credentials:true with wildcard origin (browser-rejected combo
12
+ but server config wrong), missing Vary:Origin on per-origin responses,
13
+ preflight cached over 86400s, OR Allow-Origin trust of attacker-
14
+ controlled subdomain pattern.
15
+ Trigger with: "audit cors", "check cors policy", "cors bypass",
16
+ "preflight check".
17
+ allowed-tools:
18
+ - Read
19
+ - Bash(python3:*)
20
+ - Bash(curl:*)
21
+ disallowed-tools:
22
+ - Bash(rm:*)
23
+ - Edit(/etc/*)
24
+ version: 3.0.0-dev
25
+ author: Jeremy Longshore <jeremy@intentsolutions.io>
26
+ license: MIT
27
+ compatibility: Designed for Claude Code
28
+ tags:
29
+ - security
30
+ - cors
31
+ - web
32
+ - pentest
33
+ ---
34
+
35
+ # Auditing CORS Policy
36
+
37
+ ## Overview
38
+
39
+ CORS misconfiguration is one of the most common middle-severity findings
40
+ in web bug bounties. The browser-enforced rules are subtle, the failure
41
+ modes are silent (the wrong cors response just works until an attacker
42
+ weaponizes it), and the "fix" engineers reach for —
43
+ `Access-Control-Allow-Origin: *` — opens the very class of attacks CORS
44
+ was meant to prevent when paired with credentials.
45
+
46
+ This skill probes each common CORS misconfiguration with synthetic
47
+ Origin headers and grades the response.
48
+
49
+ ## When the skill produces findings
50
+
51
+ | Finding | Severity | Threshold | Affected control |
52
+ |---|---|---|---|
53
+ | Origin reflected without validation | **HIGH** | Synthetic Origin `https://attacker.example` echoed in Allow-Origin | OWASP A05:2021 |
54
+ | Allow-Credentials:true with wildcard Allow-Origin | **CRITICAL** | Browser rejects but server is asserting the worst combo | OWASP A05:2021 |
55
+ | Allow-Credentials:true with reflected Origin | **CRITICAL** | Attacker site can read authenticated responses cross-origin | OWASP A05:2021, CWE-942 |
56
+ | Subdomain wildcard pattern bypass | **HIGH** | `Allow-Origin: *.example.com` matches `attacker.example.com.evil.com` | CWE-942 |
57
+ | Missing Vary:Origin on per-origin response | **MEDIUM** | CDN caches one origin's response for all origins | RFC 7234 |
58
+ | Preflight cache > 86400s | **LOW** | Access-Control-Max-Age over 24h limits revocation agility | MDN best practice |
59
+ | Null Origin trusted | **HIGH** | `Allow-Origin: null` accepted (sandboxed iframes, data: URLs) | CWE-942 |
60
+ | All HTTP methods permitted | **MEDIUM** | `Allow-Methods: *` enables CSRF-via-CORS for state-change | OWASP A05:2021 |
61
+
62
+ ## Prerequisites
63
+
64
+ - Python 3.9+ (`requests` library)
65
+ - Authorization for non-local targets (`../analyzing-tls-config/references/AUTHORIZATION.md`)
66
+
67
+ ## Instructions
68
+
69
+ ### Step 1 — Confirm Authorization
70
+
71
+ ```text
72
+ "Do you have authorization to perform CORS testing on this target?
73
+ I need confirmation before proceeding."
74
+ ```
75
+
76
+ ### Step 2 — Run the scanner
77
+
78
+ ```bash
79
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/auditing-cors-policy/scripts/audit_cors.py \
80
+ https://api.example.com/endpoint \
81
+ --authorized
82
+ ```
83
+
84
+ Options:
85
+
86
+ ```
87
+ Usage: audit_cors.py URL [OPTIONS]
88
+
89
+ Options:
90
+ --authorized Attest authorization for non-local targets (required)
91
+ --output FILE Write findings to FILE
92
+ --format FMT json | jsonl | markdown (default: markdown)
93
+ --min-severity SEV (default: info)
94
+ --timeout SECS Per-probe timeout (default: 10)
95
+ --method METHOD HTTP method for the main probe (default: GET)
96
+ ```
97
+
98
+ The scanner sends multiple probes per target:
99
+
100
+ 1. Baseline request with no Origin header
101
+ 2. Probe with safe Origin (https://allowed-origin.example.com)
102
+ 3. Probe with attacker Origin (https://attacker.example)
103
+ 4. Probe with subdomain-bypass Origin
104
+ 5. Probe with Origin:null
105
+ 6. OPTIONS preflight with Access-Control-Request-Headers / Method
106
+
107
+ For each, it records the response's CORS headers and grades against
108
+ the threshold table above.
109
+
110
+ ### Step 3 — Interpret findings
111
+
112
+ CRITICAL = credential-stealing chain available; ship same-day fix.
113
+ HIGH = arbitrary cross-origin read of public-but-sensitive content;
114
+ ship within sprint.
115
+ MEDIUM/LOW = posture hardening; backlog.
116
+
117
+ ### Step 4 — Cross-skill chaining
118
+
119
+ If CORS findings land alongside auth findings (skill #20 `confirming-
120
+ pentest-authorization` would have caught the auth side at engagement
121
+ scope), suggest `authentication-validator` plugin for full session-
122
+ handling audit.
123
+
124
+ ## Examples
125
+
126
+ ### Example 1 — Reflected-origin bug bounty triage
127
+
128
+ User: "Bug bounty submission claims CORS bypass on /api/profile."
129
+
130
+ ```bash
131
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/auditing-cors-policy/scripts/audit_cors.py \
132
+ https://api.example.com/profile \
133
+ --authorized \
134
+ --format json | jq '.[] | select(.severity == "critical" or .severity == "high")'
135
+ ```
136
+
137
+ If the reflected-origin probe + Allow-Credentials:true both fire, the
138
+ submission is valid; pay the bounty and ship the fix from `PLAYBOOK.md`.
139
+
140
+ ### Example 2 — Pre-launch CORS sweep on a new API gateway
141
+
142
+ User: "We're rolling out a new gateway — audit every endpoint's CORS posture."
143
+
144
+ ```bash
145
+ for ENDPOINT in $(cat endpoints.txt); do
146
+ python3 ${CLAUDE_PLUGIN_ROOT}/skills/auditing-cors-policy/scripts/audit_cors.py \
147
+ "$ENDPOINT" --authorized --min-severity high --format jsonl >> cors-audit.jsonl
148
+ done
149
+ jq -s 'group_by(.severity) | map({sev: .[0].severity, count: length})' cors-audit.jsonl
150
+ ```
151
+
152
+ ### Example 3 — CI guard against CORS regression
153
+
154
+ Drop into deploy gate:
155
+
156
+ ```yaml
157
+ - name: CORS posture gate
158
+ run: |
159
+ python3 plugins/security/penetration-tester/skills/auditing-cors-policy/scripts/audit_cors.py \
160
+ https://staging-api.example.com/auth \
161
+ --authorized \
162
+ --min-severity high
163
+ ```
164
+
165
+ Exit 1 fails the deploy if any new high/critical lands.
166
+
167
+ ## Output
168
+
169
+ JSON / JSONL / Markdown per `lib/report.py`. Exit 0 clean, 1 high/
170
+ critical, 2 error.
171
+
172
+ ## Error Handling
173
+
174
+ - **Target returns no CORS headers at all** → INFO finding "CORS not
175
+ configured" with note that this is fine for non-cross-origin endpoints.
176
+ - **Preflight fails with 405** → MEDIUM finding suggesting OPTIONS
177
+ handler missing.
178
+ - **Connection error** → exit 2 with underlying error.
179
+
180
+ ## Resources
181
+
182
+ - `references/THEORY.md` — How CORS works, why each finding matters
183
+ - `references/PLAYBOOK.md` — Allow-list config templates per server
184
+ type / framework (nginx, Express, Spring, FastAPI, Rails)
185
+ - `../analyzing-tls-config/references/AUTHORIZATION.md` — Active-scan
186
+ authorization pattern