@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
|
@@ -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
|