@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/README.md +8 -0
  3. package/commands/pentest.md +5 -0
  4. package/package.json +8 -3
  5. package/skills/analyzing-tls-config/SKILL.md +221 -0
  6. package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
  7. package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
  8. package/skills/analyzing-tls-config/references/THEORY.md +128 -0
  9. package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
  10. package/skills/auditing-cors-policy/SKILL.md +186 -0
  11. package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
  12. package/skills/auditing-cors-policy/references/THEORY.md +142 -0
  13. package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
  14. package/skills/auditing-npm-dependencies/SKILL.md +254 -0
  15. package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
  16. package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
  17. package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
  18. package/skills/auditing-python-dependencies/SKILL.md +251 -0
  19. package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
  20. package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
  21. package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
  22. package/skills/checking-http-security-headers/SKILL.md +176 -0
  23. package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
  24. package/skills/checking-http-security-headers/references/THEORY.md +137 -0
  25. package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
  26. package/skills/checking-license-compliance/SKILL.md +225 -0
  27. package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
  28. package/skills/checking-license-compliance/references/THEORY.md +152 -0
  29. package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
  30. package/skills/composing-vulnerability-report/SKILL.md +212 -0
  31. package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
  32. package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
  33. package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
  34. package/skills/confirming-pentest-authorization/SKILL.md +247 -0
  35. package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
  36. package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
  37. package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
  38. package/skills/defining-pentest-scope/SKILL.md +227 -0
  39. package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
  40. package/skills/defining-pentest-scope/references/THEORY.md +170 -0
  41. package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
  42. package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
  43. package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
  44. package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
  45. package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
  46. package/skills/detecting-debug-endpoints/SKILL.md +207 -0
  47. package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
  48. package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
  49. package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
  50. package/skills/detecting-directory-listing/SKILL.md +206 -0
  51. package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
  52. package/skills/detecting-directory-listing/references/THEORY.md +203 -0
  53. package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
  54. package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
  55. package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
  56. package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
  57. package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
  58. package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
  59. package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
  60. package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
  61. package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
  62. package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
  63. package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
  64. package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
  65. package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
  66. package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
  67. package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
  68. package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
  69. package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
  70. package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
  71. package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
  72. package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
  73. package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
  74. package/skills/detecting-weak-cryptography/SKILL.md +147 -0
  75. package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
  76. package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
  77. package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
  78. package/skills/fingerprinting-server-software/SKILL.md +191 -0
  79. package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
  80. package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
  81. package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
  82. package/skills/generating-executive-summary/SKILL.md +261 -0
  83. package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
  84. package/skills/generating-executive-summary/references/THEORY.md +195 -0
  85. package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
  86. package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
  87. package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
  88. package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
  89. package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
  90. package/skills/performing-penetration-testing/SKILL.md +282 -190
  91. package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
  92. package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
  93. package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
  94. package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
  95. package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
  96. package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
  97. package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
  98. package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
  99. package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
  100. package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
  101. package/skills/recording-pentest-engagement/SKILL.md +253 -0
  102. package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
  103. package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
  104. package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
  105. package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
  106. package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
  107. package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
  108. package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
  109. package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
  110. package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
  111. package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
  112. package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
@@ -0,0 +1,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