@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,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CORS policy auditor.
|
|
3
|
+
|
|
4
|
+
Companion to skill `auditing-cors-policy`. Probes the target with multiple
|
|
5
|
+
synthetic Origin values and grades the responses against known
|
|
6
|
+
misconfiguration patterns.
|
|
7
|
+
|
|
8
|
+
Checks performed:
|
|
9
|
+
1. Baseline (no Origin) — establishes default headers
|
|
10
|
+
2. Safe Origin — probes legitimate cross-origin behavior
|
|
11
|
+
3. Attacker Origin — checks for blind reflection (CWE-942)
|
|
12
|
+
4. Subdomain-bypass Origin — tests pattern matching weaknesses
|
|
13
|
+
5. Origin:null — tests sandboxed-iframe / data: URL trust
|
|
14
|
+
6. Preflight OPTIONS — checks Access-Control-Max-Age + allowed methods
|
|
15
|
+
7. Allow-Credentials + wildcard combination — the worst-case combo
|
|
16
|
+
8. Vary:Origin header presence — CDN poisoning risk
|
|
17
|
+
|
|
18
|
+
References:
|
|
19
|
+
Fetch Standard (https://fetch.spec.whatwg.org/) — CORS protocol
|
|
20
|
+
MDN CORS documentation — common misconfigurations
|
|
21
|
+
OWASP A05:2021 Security Misconfiguration
|
|
22
|
+
CWE-942 Permissive Cross-domain Policy with Untrusted Domains
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.parse
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
33
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
34
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
35
|
+
|
|
36
|
+
from lib.authz_check import require_authorization # noqa: E402
|
|
37
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
38
|
+
from lib.http_client import make_session, safe_options # noqa: E402
|
|
39
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
40
|
+
|
|
41
|
+
SKILL_ID = "auditing-cors-policy"
|
|
42
|
+
|
|
43
|
+
# Synthetic origins used to probe behavior
|
|
44
|
+
ATTACKER_ORIGIN = "https://attacker.example"
|
|
45
|
+
SUBDOMAIN_BYPASS_ORIGIN = None # computed per-target — see _subdomain_bypass_origin
|
|
46
|
+
NULL_ORIGIN = "null"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _subdomain_bypass_origin(target_host: str) -> str:
|
|
50
|
+
"""Build an Origin that exploits a `*.example.com` regex written as
|
|
51
|
+
`.endsWith('.example.com')` — appending a different parent domain.
|
|
52
|
+
"""
|
|
53
|
+
# If target is api.example.com, bypass = api.example.com.attacker.com
|
|
54
|
+
return f"https://{target_host}.attacker.com"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _probe(sess, method: str, url: str, origin: str | None, timeout: float):
|
|
58
|
+
headers = {}
|
|
59
|
+
if origin is not None:
|
|
60
|
+
headers["Origin"] = origin
|
|
61
|
+
try:
|
|
62
|
+
resp = sess.request(method, url, headers=headers, timeout=timeout, allow_redirects=False)
|
|
63
|
+
except Exception:
|
|
64
|
+
return None
|
|
65
|
+
return resp
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _check_baseline(resp, target: str) -> list[Finding]:
|
|
69
|
+
findings = []
|
|
70
|
+
if resp is None:
|
|
71
|
+
return findings
|
|
72
|
+
allow_origin = resp.headers.get("Access-Control-Allow-Origin")
|
|
73
|
+
allow_creds = resp.headers.get("Access-Control-Allow-Credentials", "").lower() == "true"
|
|
74
|
+
if allow_origin == "*" and allow_creds:
|
|
75
|
+
findings.append(
|
|
76
|
+
Finding(
|
|
77
|
+
skill_id=SKILL_ID,
|
|
78
|
+
title="Allow-Credentials:true with Allow-Origin:* (browser rejects, server asserts worst)",
|
|
79
|
+
severity=Severity.CRITICAL,
|
|
80
|
+
target=target,
|
|
81
|
+
detail=(
|
|
82
|
+
"The server returns Access-Control-Allow-Origin:* AND "
|
|
83
|
+
"Access-Control-Allow-Credentials:true on the same response. "
|
|
84
|
+
"Browsers reject this combination per Fetch standard, but the "
|
|
85
|
+
"server is asserting it would allow ANY origin to read "
|
|
86
|
+
"credentialed responses if the browser cooperated. This "
|
|
87
|
+
"signals the developer intent is wrong — fix immediately."
|
|
88
|
+
),
|
|
89
|
+
remediation=(
|
|
90
|
+
"If the endpoint needs credentials cross-origin: replace * "
|
|
91
|
+
"with a specific allow-list of trusted origins, set "
|
|
92
|
+
"Vary:Origin, and validate Origin against the allow-list "
|
|
93
|
+
"server-side. If credentials aren't needed: drop "
|
|
94
|
+
"Allow-Credentials:true."
|
|
95
|
+
),
|
|
96
|
+
cwe_id="CWE-942",
|
|
97
|
+
owasp_category="A05:2021",
|
|
98
|
+
affected_control="OWASP A05:2021",
|
|
99
|
+
references=("https://fetch.spec.whatwg.org/#cors-protocol-and-credentials",),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return findings
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _check_reflection(resp_attacker, resp_safe, target: str) -> list[Finding]:
|
|
106
|
+
"""If the attacker-origin probe gets that origin back in Allow-Origin,
|
|
107
|
+
the server is doing blind reflection. Pair with credentials = critical.
|
|
108
|
+
"""
|
|
109
|
+
findings = []
|
|
110
|
+
if resp_attacker is None:
|
|
111
|
+
return findings
|
|
112
|
+
allow_origin_attacker = resp_attacker.headers.get("Access-Control-Allow-Origin", "")
|
|
113
|
+
allow_creds_attacker = resp_attacker.headers.get("Access-Control-Allow-Credentials", "").lower() == "true"
|
|
114
|
+
if allow_origin_attacker == ATTACKER_ORIGIN:
|
|
115
|
+
sev = Severity.CRITICAL if allow_creds_attacker else Severity.HIGH
|
|
116
|
+
findings.append(
|
|
117
|
+
Finding(
|
|
118
|
+
skill_id=SKILL_ID,
|
|
119
|
+
title="Origin header reflected without validation",
|
|
120
|
+
severity=sev,
|
|
121
|
+
target=target,
|
|
122
|
+
detail=(
|
|
123
|
+
"The server echoed the synthetic Origin "
|
|
124
|
+
f"({ATTACKER_ORIGIN}) into Access-Control-Allow-Origin. "
|
|
125
|
+
+ (
|
|
126
|
+
"Combined with Allow-Credentials:true, ANY origin can "
|
|
127
|
+
"read authenticated responses from this endpoint — full "
|
|
128
|
+
"session theft is possible via a malicious page the "
|
|
129
|
+
"victim visits while logged in."
|
|
130
|
+
if allow_creds_attacker
|
|
131
|
+
else "Any origin can read unauthenticated responses; "
|
|
132
|
+
"consequences are bounded but the configuration is wrong."
|
|
133
|
+
)
|
|
134
|
+
),
|
|
135
|
+
remediation=(
|
|
136
|
+
"Replace reflection logic with an allow-list check. Common "
|
|
137
|
+
"framework patterns: Express cors() with `origin: ['https://a',"
|
|
138
|
+
" 'https://b']`; Spring `@CrossOrigin(origins = {\"https://a\"})`;"
|
|
139
|
+
' FastAPI `CORSMiddleware(allow_origins=["https://a"])`. '
|
|
140
|
+
"Always set Vary:Origin when serving per-origin responses."
|
|
141
|
+
),
|
|
142
|
+
cwe_id="CWE-942",
|
|
143
|
+
owasp_category="A05:2021",
|
|
144
|
+
affected_control="OWASP A05:2021",
|
|
145
|
+
references=("https://cwe.mitre.org/data/definitions/942.html",),
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return findings
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _check_subdomain_bypass(resp_bypass, bypass_origin: str, target: str) -> list[Finding]:
|
|
152
|
+
if resp_bypass is None:
|
|
153
|
+
return []
|
|
154
|
+
if resp_bypass.headers.get("Access-Control-Allow-Origin") == bypass_origin:
|
|
155
|
+
return [
|
|
156
|
+
Finding(
|
|
157
|
+
skill_id=SKILL_ID,
|
|
158
|
+
title="Subdomain-pattern CORS check bypassed via parent-domain append",
|
|
159
|
+
severity=Severity.HIGH,
|
|
160
|
+
target=target,
|
|
161
|
+
detail=(
|
|
162
|
+
f"Origin {bypass_origin!r} accepted. The server's allow-list "
|
|
163
|
+
"logic likely uses substring or endsWith() matching against "
|
|
164
|
+
"the trusted parent domain, which fails on attacker-controlled "
|
|
165
|
+
"subdomains under a different root."
|
|
166
|
+
),
|
|
167
|
+
remediation=(
|
|
168
|
+
"Replace string-suffix matching with exact-equal or proper "
|
|
169
|
+
"URL parsing: extract the hostname from the Origin and check "
|
|
170
|
+
"exact equality against an allow-list."
|
|
171
|
+
),
|
|
172
|
+
cwe_id="CWE-942",
|
|
173
|
+
)
|
|
174
|
+
]
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _check_null_origin(resp_null, target: str) -> list[Finding]:
|
|
179
|
+
if resp_null is None:
|
|
180
|
+
return []
|
|
181
|
+
if resp_null.headers.get("Access-Control-Allow-Origin") == "null":
|
|
182
|
+
creds = resp_null.headers.get("Access-Control-Allow-Credentials", "").lower() == "true"
|
|
183
|
+
return [
|
|
184
|
+
Finding(
|
|
185
|
+
skill_id=SKILL_ID,
|
|
186
|
+
title="Allow-Origin:null trusted (sandboxed iframes, data: URLs)",
|
|
187
|
+
severity=Severity.CRITICAL if creds else Severity.HIGH,
|
|
188
|
+
target=target,
|
|
189
|
+
detail=(
|
|
190
|
+
"The server returns Allow-Origin:null. Sandboxed iframes "
|
|
191
|
+
"and data: URLs send Origin:null; an attacker can host "
|
|
192
|
+
"a malicious page in a sandboxed iframe to satisfy this "
|
|
193
|
+
"check and read responses."
|
|
194
|
+
),
|
|
195
|
+
remediation="Never trust Origin:null. Remove it from the allow-list.",
|
|
196
|
+
cwe_id="CWE-942",
|
|
197
|
+
)
|
|
198
|
+
]
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _check_vary(resp_safe, target: str) -> list[Finding]:
|
|
203
|
+
if resp_safe is None:
|
|
204
|
+
return []
|
|
205
|
+
allow_origin = resp_safe.headers.get("Access-Control-Allow-Origin")
|
|
206
|
+
vary = resp_safe.headers.get("Vary", "")
|
|
207
|
+
# If Allow-Origin is anything other than * (i.e., per-origin), Vary:Origin
|
|
208
|
+
# must be set, else CDNs cache one origin's response for everyone.
|
|
209
|
+
if allow_origin and allow_origin != "*" and "origin" not in vary.lower():
|
|
210
|
+
return [
|
|
211
|
+
Finding(
|
|
212
|
+
skill_id=SKILL_ID,
|
|
213
|
+
title="Per-origin Allow-Origin without Vary:Origin (CDN poisoning risk)",
|
|
214
|
+
severity=Severity.MEDIUM,
|
|
215
|
+
target=target,
|
|
216
|
+
detail=(
|
|
217
|
+
"The response varies CORS headers by Origin but does not "
|
|
218
|
+
"include Origin in the Vary header. CDNs and shared caches "
|
|
219
|
+
"may serve one origin's Allow-Origin response to a different "
|
|
220
|
+
"origin's requests."
|
|
221
|
+
),
|
|
222
|
+
remediation=(
|
|
223
|
+
"Add `Vary: Origin` to every response that varies CORS "
|
|
224
|
+
"headers by Origin. nginx: `add_header Vary Origin always;`."
|
|
225
|
+
),
|
|
226
|
+
affected_control="RFC 7234",
|
|
227
|
+
references=("https://datatracker.ietf.org/doc/html/rfc7234#section-4.1",),
|
|
228
|
+
)
|
|
229
|
+
]
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _check_preflight(resp_preflight, target: str) -> list[Finding]:
|
|
234
|
+
if resp_preflight is None:
|
|
235
|
+
return []
|
|
236
|
+
findings = []
|
|
237
|
+
max_age = resp_preflight.headers.get("Access-Control-Max-Age", "")
|
|
238
|
+
try:
|
|
239
|
+
max_age_int = int(max_age) if max_age else 0
|
|
240
|
+
except ValueError:
|
|
241
|
+
max_age_int = 0
|
|
242
|
+
if max_age_int > 86400:
|
|
243
|
+
findings.append(
|
|
244
|
+
Finding(
|
|
245
|
+
skill_id=SKILL_ID,
|
|
246
|
+
title=f"Preflight cache exceeds 24h ({max_age_int}s)",
|
|
247
|
+
severity=Severity.LOW,
|
|
248
|
+
target=target,
|
|
249
|
+
detail=(
|
|
250
|
+
"Access-Control-Max-Age is set very high, which prevents the "
|
|
251
|
+
"browser from re-requesting preflight when CORS policy "
|
|
252
|
+
"changes. Revocation agility is reduced."
|
|
253
|
+
),
|
|
254
|
+
remediation="Cap Access-Control-Max-Age at 86400 (24h) or lower.",
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
allow_methods = resp_preflight.headers.get("Access-Control-Allow-Methods", "")
|
|
258
|
+
if "*" in allow_methods:
|
|
259
|
+
findings.append(
|
|
260
|
+
Finding(
|
|
261
|
+
skill_id=SKILL_ID,
|
|
262
|
+
title="Allow-Methods:* permits arbitrary HTTP methods",
|
|
263
|
+
severity=Severity.MEDIUM,
|
|
264
|
+
target=target,
|
|
265
|
+
detail=(
|
|
266
|
+
"The preflight response permits ALL methods. Pair with any "
|
|
267
|
+
"CORS misconfiguration above for cross-origin CSRF on state-"
|
|
268
|
+
"changing methods."
|
|
269
|
+
),
|
|
270
|
+
remediation=(
|
|
271
|
+
"Enumerate explicit methods the endpoint actually supports: "
|
|
272
|
+
"`Access-Control-Allow-Methods: GET, POST, PUT, DELETE`."
|
|
273
|
+
),
|
|
274
|
+
owasp_category="A05:2021",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
return findings
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def main(argv: list[str] | None = None) -> int:
|
|
281
|
+
parser = argparse.ArgumentParser(description="CORS policy auditor")
|
|
282
|
+
parser.add_argument("url")
|
|
283
|
+
parser.add_argument("--authorized", action="store_true")
|
|
284
|
+
parser.add_argument("--output", default=None)
|
|
285
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
286
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
287
|
+
parser.add_argument("--timeout", type=float, default=10.0)
|
|
288
|
+
parser.add_argument("--method", default="GET", help="HTTP method for the main probe (default GET)")
|
|
289
|
+
args = parser.parse_args(argv)
|
|
290
|
+
|
|
291
|
+
require_authorization(args.url, args.authorized)
|
|
292
|
+
|
|
293
|
+
parsed = urllib.parse.urlparse(args.url)
|
|
294
|
+
target_host = parsed.hostname or "unknown"
|
|
295
|
+
target = args.url
|
|
296
|
+
|
|
297
|
+
sess = make_session(timeout=args.timeout)
|
|
298
|
+
bypass_origin = _subdomain_bypass_origin(target_host)
|
|
299
|
+
safe_origin = "https://allowed.example.com"
|
|
300
|
+
|
|
301
|
+
# Six probes
|
|
302
|
+
resp_baseline = _probe(sess, args.method, args.url, None, args.timeout)
|
|
303
|
+
resp_safe = _probe(sess, args.method, args.url, safe_origin, args.timeout)
|
|
304
|
+
resp_attacker = _probe(sess, args.method, args.url, ATTACKER_ORIGIN, args.timeout)
|
|
305
|
+
resp_bypass = _probe(sess, args.method, args.url, bypass_origin, args.timeout)
|
|
306
|
+
resp_null = _probe(sess, args.method, args.url, NULL_ORIGIN, args.timeout)
|
|
307
|
+
resp_preflight = safe_options(
|
|
308
|
+
sess,
|
|
309
|
+
args.url,
|
|
310
|
+
timeout=args.timeout,
|
|
311
|
+
headers={
|
|
312
|
+
"Origin": safe_origin,
|
|
313
|
+
"Access-Control-Request-Method": "PUT",
|
|
314
|
+
"Access-Control-Request-Headers": "Authorization,Content-Type",
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if resp_baseline is None:
|
|
319
|
+
sys.stderr.write(f"ERROR: target {target!r} unreachable\n")
|
|
320
|
+
return 2
|
|
321
|
+
|
|
322
|
+
findings: list[Finding] = []
|
|
323
|
+
findings.extend(_check_baseline(resp_baseline, target))
|
|
324
|
+
findings.extend(_check_reflection(resp_attacker, resp_safe, target))
|
|
325
|
+
findings.extend(_check_subdomain_bypass(resp_bypass, bypass_origin, target))
|
|
326
|
+
findings.extend(_check_null_origin(resp_null, target))
|
|
327
|
+
findings.extend(_check_vary(resp_safe, target))
|
|
328
|
+
findings.extend(_check_preflight(resp_preflight, target))
|
|
329
|
+
|
|
330
|
+
floor = Severity(args.min_severity)
|
|
331
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
332
|
+
|
|
333
|
+
if not findings:
|
|
334
|
+
findings.append(
|
|
335
|
+
Finding(
|
|
336
|
+
skill_id=SKILL_ID,
|
|
337
|
+
title="No CORS misconfiguration detected in standard probe set",
|
|
338
|
+
severity=Severity.INFO,
|
|
339
|
+
target=target,
|
|
340
|
+
detail="The 6-probe sweep did not surface any threshold violations.",
|
|
341
|
+
remediation="No action needed; re-run on any CORS-config change.",
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
emit(findings, args.output, args.format, target)
|
|
346
|
+
return exit_code(findings)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
sys.exit(main())
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auditing-npm-dependencies
|
|
3
|
+
description: |
|
|
4
|
+
Audit a Node.js project's installed npm dependency tree for known
|
|
5
|
+
CVEs by wrapping the npm audit JSON output and emitting findings in
|
|
6
|
+
the canonical penetration-tester schema. Detects direct AND transitive
|
|
7
|
+
vulnerabilities, normalizes npm's severity scale (info/low/moderate/
|
|
8
|
+
high/critical) to the shared Severity enum, and parses both v1 and
|
|
9
|
+
v2 audit output formats so the skill works against npm 6 and npm
|
|
10
|
+
7+ lockfiles.
|
|
11
|
+
Use when: pre-merge gate on a Node project, post-incident sweep
|
|
12
|
+
after a transitive package compromise (e.g. event-stream, ua-parser,
|
|
13
|
+
node-ipc, color.js), SOC2 vendor-management evidence collection,
|
|
14
|
+
or auditing an inherited or acquired Node codebase.
|
|
15
|
+
Threshold: any HIGH or CRITICAL CVE in the resolved dependency
|
|
16
|
+
tree. MODERATE / LOW reported informationally.
|
|
17
|
+
Trigger with: "audit npm deps", "npm vulnerability scan", "check
|
|
18
|
+
node packages for CVEs", "npm audit".
|
|
19
|
+
allowed-tools:
|
|
20
|
+
- Read
|
|
21
|
+
- Bash(npm:*)
|
|
22
|
+
- Bash(python3:*)
|
|
23
|
+
- Glob
|
|
24
|
+
disallowed-tools:
|
|
25
|
+
- Bash(rm:*)
|
|
26
|
+
- Bash(curl:*)
|
|
27
|
+
- Bash(wget:*)
|
|
28
|
+
- Write(.env)
|
|
29
|
+
- Edit(.env)
|
|
30
|
+
- Bash(npm publish:*)
|
|
31
|
+
- Bash(npm install:*)
|
|
32
|
+
version: 3.0.0-dev
|
|
33
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
34
|
+
license: MIT
|
|
35
|
+
compatibility: Designed for Claude Code
|
|
36
|
+
tags:
|
|
37
|
+
- security
|
|
38
|
+
- dependency-audit
|
|
39
|
+
- npm
|
|
40
|
+
- cve
|
|
41
|
+
- pentest
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
# Auditing npm Dependencies
|
|
45
|
+
|
|
46
|
+
## Overview
|
|
47
|
+
|
|
48
|
+
Modern Node.js applications pull in hundreds of transitive packages
|
|
49
|
+
through a single `npm install`. The ratio of direct-to-transitive
|
|
50
|
+
dependencies on a typical app is around 1:50 — install 30 packages,
|
|
51
|
+
end up with 1,500. Every one of those packages can ship a CVE, get
|
|
52
|
+
maintainer-takeover-attacked, or contain a typosquatted near-name
|
|
53
|
+
package that someone slipped into your lockfile.
|
|
54
|
+
|
|
55
|
+
The published-CVE feed for npm is among the busiest in the ecosystem
|
|
56
|
+
because the registry is shared, public, and trivially installable.
|
|
57
|
+
`npm audit` queries the same advisory database GitHub's Dependabot
|
|
58
|
+
uses, returning per-package vulnerability records with CVE ID,
|
|
59
|
+
severity, affected version range, and fix-available version. Running
|
|
60
|
+
it is free and fast; the friction is interpreting the output and
|
|
61
|
+
deciding which findings actually block your release.
|
|
62
|
+
|
|
63
|
+
This skill standardizes that interpretation. It wraps `npm audit
|
|
64
|
+
--json`, parses both the v1 (npm 6) and v2 (npm 7+) output shapes,
|
|
65
|
+
maps npm's severity vocabulary to the shared Severity enum, and
|
|
66
|
+
emits Findings in the canonical penetration-tester JSON shape so
|
|
67
|
+
downstream tooling (CI gates, security dashboards, SOC2 evidence
|
|
68
|
+
collection) gets uniform records regardless of which package
|
|
69
|
+
manager surfaced them.
|
|
70
|
+
|
|
71
|
+
## When the skill produces findings
|
|
72
|
+
|
|
73
|
+
| Finding | Severity | Threshold | Affected control |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| Critical CVE in direct dep | **CRITICAL** | npm `severity: critical` AND package in `dependencies` of root `package.json` | CWE-1104 |
|
|
76
|
+
| Critical CVE in transitive dep | **CRITICAL** | npm `severity: critical` AND package NOT in root `dependencies` | CWE-1104 |
|
|
77
|
+
| High CVE in direct dep | **HIGH** | npm `severity: high` AND direct | CWE-1104 |
|
|
78
|
+
| High CVE in transitive dep | **HIGH** | npm `severity: high` AND transitive | CWE-1104 |
|
|
79
|
+
| Moderate CVE | **MEDIUM** | npm `severity: moderate` | CWE-1104 |
|
|
80
|
+
| Low CVE | **LOW** | npm `severity: low` | CWE-1104 |
|
|
81
|
+
| Info advisory | **INFO** | npm `severity: info` | CWE-1104 |
|
|
82
|
+
| Vulnerable package with no patch | **HIGH** | finding has no `fix.available` and severity ≥ moderate | CWE-1395 |
|
|
83
|
+
| Audit registry unreachable | **INFO** | npm exits non-zero with network error | (operational) |
|
|
84
|
+
| Audit returns malformed output | **INFO** | JSON parse fails on `npm audit --json` stdout | (operational) |
|
|
85
|
+
|
|
86
|
+
Direct vs transitive matters: a CVE in `lodash` you require directly
|
|
87
|
+
is fixable by upgrading your `package.json`. A CVE in `lodash` pulled
|
|
88
|
+
in transitively through `aws-sdk` requires either upgrading `aws-sdk`
|
|
89
|
+
to a version with a newer `lodash` floor, or pinning via `overrides`
|
|
90
|
+
in your root `package.json`.
|
|
91
|
+
|
|
92
|
+
## Prerequisites
|
|
93
|
+
|
|
94
|
+
- Node.js + npm installed on the host running the scan (npm 6+
|
|
95
|
+
supported; npm 7+ recommended for richer output)
|
|
96
|
+
- Target project directory containing `package.json` and at minimum
|
|
97
|
+
one of `package-lock.json`, `npm-shrinkwrap.json`
|
|
98
|
+
- Network access to the npm registry (`registry.npmjs.org` by default)
|
|
99
|
+
|
|
100
|
+
## Instructions
|
|
101
|
+
|
|
102
|
+
### Step 1 — Identify the scan target
|
|
103
|
+
|
|
104
|
+
Locate the project directory. The scanner expects `package.json` at
|
|
105
|
+
the directory root. Monorepos with multiple `package.json` files
|
|
106
|
+
should be scanned per package; the scanner does not auto-traverse
|
|
107
|
+
workspaces (use `npm audit --workspaces` separately for that case).
|
|
108
|
+
|
|
109
|
+
### Step 2 — Run the audit
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python3 ./scripts/audit_npm.py /path/to/node-project
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Options:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Usage: audit_npm.py PATH [OPTIONS]
|
|
119
|
+
|
|
120
|
+
Options:
|
|
121
|
+
--output FILE Write findings to FILE (default: stdout)
|
|
122
|
+
--format FMT json | jsonl | markdown (default: markdown)
|
|
123
|
+
--min-severity SEV (default: info)
|
|
124
|
+
--include-dev Audit `devDependencies` too (default: prod only)
|
|
125
|
+
--no-cache Pass --no-audit-cache to npm (slower; fresh data)
|
|
126
|
+
--json-only Print raw `npm audit --json` and exit (debug)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The scanner shells out to `npm audit --json` in the target directory,
|
|
130
|
+
parses the output, deduplicates per-CVE across direct and transitive
|
|
131
|
+
paths, and emits one Finding per CVE.
|
|
132
|
+
|
|
133
|
+
### Step 3 — Interpret findings
|
|
134
|
+
|
|
135
|
+
CRITICAL / HIGH = block the release. Either bump the vulnerable
|
|
136
|
+
package to the fix version (most common), or apply an npm `overrides`
|
|
137
|
+
entry if the transitive dep can't be reached through a parent bump.
|
|
138
|
+
|
|
139
|
+
MEDIUM / LOW = file a remediation ticket but don't block. These often
|
|
140
|
+
require waiting for the upstream maintainer to ship a fix.
|
|
141
|
+
|
|
142
|
+
INFO = log only. Informational advisories sometimes flag deprecated
|
|
143
|
+
packages without an active vulnerability.
|
|
144
|
+
|
|
145
|
+
### Step 4 — Remediation
|
|
146
|
+
|
|
147
|
+
For a CVE in a DIRECT dep:
|
|
148
|
+
|
|
149
|
+
1. Run `npm audit fix` — npm attempts a non-breaking upgrade.
|
|
150
|
+
2. If `npm audit fix` says "requires manual review" (semver-major
|
|
151
|
+
bump), evaluate the breaking changes and decide whether to upgrade
|
|
152
|
+
or accept the risk. Document the decision.
|
|
153
|
+
3. Pin the resolved version in `package-lock.json`; commit the diff.
|
|
154
|
+
|
|
155
|
+
For a CVE in a TRANSITIVE dep:
|
|
156
|
+
|
|
157
|
+
1. Identify the path: `npm ls <vulnerable-package>` shows which
|
|
158
|
+
parent(s) pull it in.
|
|
159
|
+
2. Check whether bumping the parent picks up the fix: `npm view
|
|
160
|
+
<parent> dependencies` lists the parent's declared range.
|
|
161
|
+
3. If parent has a newer version that floors the vulnerable dep above
|
|
162
|
+
the fix-version, upgrade the parent.
|
|
163
|
+
4. Otherwise add an `overrides` block in your root `package.json`:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
"overrides": {
|
|
167
|
+
"<vulnerable-package>": "<fix-version>"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This requires npm 8.3+ and forces the resolution. Document why
|
|
172
|
+
you're overriding — overrides are easy to forget about.
|
|
173
|
+
|
|
174
|
+
For a CVE with NO fix available:
|
|
175
|
+
|
|
176
|
+
1. Subscribe to the GitHub Security Advisory for that CVE.
|
|
177
|
+
2. If exploitable in your usage context, replace the package or
|
|
178
|
+
vendor it with a private patch.
|
|
179
|
+
3. Document the exception with a date for re-evaluation.
|
|
180
|
+
|
|
181
|
+
## Examples
|
|
182
|
+
|
|
183
|
+
### Example 1 — Pre-merge gate
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python3 ./scripts/audit_npm.py . --min-severity high --format json --output npm-audit.json
|
|
187
|
+
jq -e '. == []' npm-audit.json || { echo "High/critical npm CVE — fix before merge"; exit 1; }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Example 2 — CI scan on every push
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
- name: npm dependency audit
|
|
194
|
+
run: |
|
|
195
|
+
python3 plugins/security/penetration-tester/skills/auditing-npm-dependencies/scripts/audit_npm.py \
|
|
196
|
+
. --min-severity high --format markdown --output npm-audit.md
|
|
197
|
+
- name: Upload audit
|
|
198
|
+
uses: actions/upload-artifact@v4
|
|
199
|
+
with:
|
|
200
|
+
name: npm-audit
|
|
201
|
+
path: npm-audit.md
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Example 3 — SOC2 evidence collection
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
python3 ./scripts/audit_npm.py . --include-dev --no-cache --format json \
|
|
208
|
+
--output evidence/CC7-npm-audit-$(date +%Y%m%d).json
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`--include-dev` is important for SOC2 evidence: auditors want the
|
|
212
|
+
full picture, not just production deps. `--no-cache` ensures the
|
|
213
|
+
evidence reflects current advisory data, not yesterday's cache.
|
|
214
|
+
|
|
215
|
+
## Output
|
|
216
|
+
|
|
217
|
+
JSON / JSONL / Markdown per `lib/report.py`. Exit codes: 0 clean, 1
|
|
218
|
+
high/critical, 2 error.
|
|
219
|
+
|
|
220
|
+
Each Finding includes:
|
|
221
|
+
|
|
222
|
+
- `id` — synthesized as `npm-audit::<cve-id>` (or `npm-audit::<advisory-id>` when no CVE assigned)
|
|
223
|
+
- `severity` — CRITICAL / HIGH / MEDIUM / LOW / INFO
|
|
224
|
+
- `category` — `dependency-vulnerability`
|
|
225
|
+
- `summary` — short CVE title
|
|
226
|
+
- `evidence` — affected package, affected version range, fix version (if any), dependency path
|
|
227
|
+
- `references` — GHSA URL, CVE URL, npm advisory URL
|
|
228
|
+
|
|
229
|
+
## Error Handling
|
|
230
|
+
|
|
231
|
+
- **npm not installed** → exits 2 with operational error advising
|
|
232
|
+
the operator to install Node.js.
|
|
233
|
+
- **No `package.json`** → exits 2 with "target is not a Node project"
|
|
234
|
+
error.
|
|
235
|
+
- **npm registry unreachable** → emits an INFO Finding documenting
|
|
236
|
+
the outage and exits 0 (no actionable security finding).
|
|
237
|
+
- **npm audit returns non-JSON garbage** → emits an INFO Finding and
|
|
238
|
+
exits 2. Sometimes happens with corrupt npm cache; advise the
|
|
239
|
+
operator to run `npm cache clean --force` and retry.
|
|
240
|
+
- **Lockfile out of sync with `package.json`** → npm warns and
|
|
241
|
+
may produce partial results; the scanner emits an INFO Finding
|
|
242
|
+
flagging the desync and proceeds with whatever data npm returns.
|
|
243
|
+
|
|
244
|
+
## Resources
|
|
245
|
+
|
|
246
|
+
- `references/THEORY.md` — Why npm's dependency graph is the largest
|
|
247
|
+
CVE surface in modern software, history of npm supply-chain attacks
|
|
248
|
+
(event-stream, ua-parser-js, color.js, node-ipc), direct-vs-transitive
|
|
249
|
+
remediation theory, when `overrides` are safe, npm audit v1 vs v2
|
|
250
|
+
output schema diff
|
|
251
|
+
- `references/PLAYBOOK.md` — Per-runtime remediation patterns
|
|
252
|
+
(frontend webpack/vite, Node server, Electron desktop, Lambda),
|
|
253
|
+
parent-bump decision matrix, override-block templates, GitHub
|
|
254
|
+
Dependabot integration, SOC2 evidence retention policy
|