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