@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,362 @@
1
+ #!/usr/bin/env python3
2
+ """HTTP security headers auditor.
3
+
4
+ Companion to skill `checking-http-security-headers`. Probes the target
5
+ GET response and grades each canonical security header.
6
+
7
+ Checks performed:
8
+ 1. Strict-Transport-Security — presence, max-age, includeSubDomains, preload
9
+ 2. Content-Security-Policy — presence, unsafe-inline, unsafe-eval,
10
+ frame-ancestors
11
+ 3. X-Frame-Options — present OR CSP frame-ancestors set
12
+ 4. X-Content-Type-Options:nosniff
13
+ 5. Referrer-Policy — present, not unsafe-url
14
+ 6. Permissions-Policy
15
+ 7. Server: header version disclosure
16
+ 8. Cache-Control on authenticated endpoint
17
+
18
+ References:
19
+ MDN — HTTP security headers
20
+ OWASP Secure Headers Project (https://owasp.org/www-project-secure-headers/)
21
+ Mozilla Observatory
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import re
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ _PLUGIN_ROOT = Path(__file__).resolve().parents[3]
32
+ if str(_PLUGIN_ROOT) not in sys.path:
33
+ sys.path.insert(0, str(_PLUGIN_ROOT))
34
+
35
+ from lib.authz_check import require_authorization # noqa: E402
36
+ from lib.finding import Finding, Severity # noqa: E402
37
+ from lib.http_client import make_session, safe_get # noqa: E402
38
+ from lib.report import emit, exit_code # noqa: E402
39
+
40
+ SKILL_ID = "checking-http-security-headers"
41
+
42
+ PRELOAD_MIN_MAX_AGE = 31536000 # 1 year (hstspreload.org requirement)
43
+
44
+
45
+ def _check_hsts(headers: dict, target: str, is_https: bool) -> list[Finding]:
46
+ findings: list[Finding] = []
47
+ if not is_https:
48
+ return findings
49
+ hsts = headers.get("Strict-Transport-Security")
50
+ if not hsts:
51
+ return [
52
+ Finding(
53
+ skill_id=SKILL_ID,
54
+ title="Strict-Transport-Security header missing",
55
+ severity=Severity.HIGH,
56
+ target=target,
57
+ detail=(
58
+ "No HSTS header on the HTTPS response. The first time a "
59
+ "client visits the site over HTTPS (or any first-visit after "
60
+ "their HSTS cache expires), an attacker on the network can "
61
+ "rewrite the response to use HTTP — and clients have no "
62
+ "pinning to refuse the downgrade."
63
+ ),
64
+ remediation=(
65
+ "Add: `Strict-Transport-Security: max-age=31536000; "
66
+ "includeSubDomains; preload`. nginx: `add_header "
67
+ 'Strict-Transport-Security "max-age=31536000; '
68
+ 'includeSubDomains; preload" always;`.'
69
+ ),
70
+ cwe_id="CWE-319",
71
+ owasp_category="A05:2021",
72
+ references=("https://hstspreload.org/",),
73
+ )
74
+ ]
75
+ # Parse max-age
76
+ m = re.search(r"max-age\s*=\s*(\d+)", hsts)
77
+ if m:
78
+ max_age = int(m.group(1))
79
+ if max_age < PRELOAD_MIN_MAX_AGE:
80
+ findings.append(
81
+ Finding(
82
+ skill_id=SKILL_ID,
83
+ title=f"HSTS max-age ({max_age}s) below preload threshold",
84
+ severity=Severity.MEDIUM,
85
+ target=target,
86
+ detail=(
87
+ f"HSTS max-age is {max_age}s. hstspreload.org requires "
88
+ f"≥{PRELOAD_MIN_MAX_AGE}s (1 year) for preload-list "
89
+ "submission."
90
+ ),
91
+ remediation=f"Increase max-age to {PRELOAD_MIN_MAX_AGE}.",
92
+ )
93
+ )
94
+ if "preload" in hsts.lower() and "includesubdomains" not in hsts.lower():
95
+ findings.append(
96
+ Finding(
97
+ skill_id=SKILL_ID,
98
+ title="HSTS preload directive without includeSubDomains",
99
+ severity=Severity.LOW,
100
+ target=target,
101
+ detail=("The preload directive requires includeSubDomains per hstspreload.org policy."),
102
+ remediation="Add `includeSubDomains` to the HSTS header value.",
103
+ )
104
+ )
105
+ return findings
106
+
107
+
108
+ def _check_csp(headers: dict, target: str) -> list[Finding]:
109
+ findings: list[Finding] = []
110
+ csp = headers.get("Content-Security-Policy") or headers.get("Content-Security-Policy-Report-Only")
111
+ if not csp:
112
+ return [
113
+ Finding(
114
+ skill_id=SKILL_ID,
115
+ title="Content-Security-Policy header missing",
116
+ severity=Severity.HIGH,
117
+ target=target,
118
+ detail=(
119
+ "No CSP. The browser will execute any inline script the "
120
+ "server (or any injection vector) returns. Reflected and "
121
+ "stored XSS classes are unmitigated."
122
+ ),
123
+ remediation=(
124
+ "Start with a report-only policy: "
125
+ "`Content-Security-Policy-Report-Only: default-src 'self'; "
126
+ "report-uri /csp-report`. Move to enforcing once violations "
127
+ "settle. See references/PLAYBOOK.md § CSP rollout."
128
+ ),
129
+ cwe_id="CWE-79",
130
+ owasp_category="A03:2021",
131
+ )
132
+ ]
133
+ if "'unsafe-inline'" in csp:
134
+ findings.append(
135
+ Finding(
136
+ skill_id=SKILL_ID,
137
+ title="CSP includes 'unsafe-inline'",
138
+ severity=Severity.MEDIUM,
139
+ target=target,
140
+ detail=(
141
+ "'unsafe-inline' permits inline <script> and onclick= "
142
+ "handlers. This is the most common XSS-protection bypass."
143
+ ),
144
+ remediation=(
145
+ "Replace inline handlers with addEventListener; replace "
146
+ "inline styles with classes; if migration is gradual, use "
147
+ "nonce-source or hash-source CSP entries per script block."
148
+ ),
149
+ cwe_id="CWE-79",
150
+ owasp_category="A03:2021",
151
+ )
152
+ )
153
+ if "'unsafe-eval'" in csp:
154
+ findings.append(
155
+ Finding(
156
+ skill_id=SKILL_ID,
157
+ title="CSP includes 'unsafe-eval'",
158
+ severity=Severity.MEDIUM,
159
+ target=target,
160
+ detail=(
161
+ "'unsafe-eval' permits eval(), new Function(), and similar. "
162
+ "Most modern frameworks (React/Vue/Angular in production "
163
+ "mode) don't need this."
164
+ ),
165
+ remediation=(
166
+ "Audit dependencies for eval usage; replace or upgrade. "
167
+ "Common offenders: older Angular dev mode, older Vue "
168
+ "with template-runtime."
169
+ ),
170
+ )
171
+ )
172
+ return findings
173
+
174
+
175
+ def _check_clickjacking(headers: dict, target: str) -> list[Finding]:
176
+ xfo = headers.get("X-Frame-Options", "").lower()
177
+ csp = (headers.get("Content-Security-Policy") or "").lower()
178
+ if xfo or "frame-ancestors" in csp:
179
+ return []
180
+ return [
181
+ Finding(
182
+ skill_id=SKILL_ID,
183
+ title="No clickjacking protection (X-Frame-Options + frame-ancestors both absent)",
184
+ severity=Severity.HIGH,
185
+ target=target,
186
+ detail=(
187
+ "Neither X-Frame-Options nor CSP frame-ancestors is set. The "
188
+ "page can be embedded in an attacker's iframe and used for "
189
+ "UI-redress (clickjacking) attacks against authenticated "
190
+ "users."
191
+ ),
192
+ remediation=(
193
+ "Add `X-Frame-Options: DENY` for pages never embedded, or "
194
+ "`Content-Security-Policy: frame-ancestors 'self' "
195
+ "https://embedded-by.example.com` for selective embedding."
196
+ ),
197
+ cwe_id="CWE-1021",
198
+ )
199
+ ]
200
+
201
+
202
+ def _check_nosniff(headers: dict, target: str) -> list[Finding]:
203
+ if headers.get("X-Content-Type-Options", "").lower() == "nosniff":
204
+ return []
205
+ return [
206
+ Finding(
207
+ skill_id=SKILL_ID,
208
+ title="X-Content-Type-Options:nosniff missing",
209
+ severity=Severity.MEDIUM,
210
+ target=target,
211
+ detail=(
212
+ "Without nosniff, browsers may MIME-sniff a response served "
213
+ "as text/plain and execute it as JavaScript if it looks "
214
+ "script-shaped. Closes a class of file-upload XSS."
215
+ ),
216
+ remediation="Add `X-Content-Type-Options: nosniff` to every response.",
217
+ cwe_id="CWE-79",
218
+ )
219
+ ]
220
+
221
+
222
+ def _check_referrer(headers: dict, target: str) -> list[Finding]:
223
+ rp = headers.get("Referrer-Policy", "").lower()
224
+ if not rp:
225
+ return [
226
+ Finding(
227
+ skill_id=SKILL_ID,
228
+ title="Referrer-Policy missing",
229
+ severity=Severity.MEDIUM,
230
+ target=target,
231
+ detail=(
232
+ "Without a Referrer-Policy, the browser uses no-referrer-"
233
+ "when-downgrade by default — internal URLs leak to external "
234
+ "sites the user navigates to."
235
+ ),
236
+ remediation=("Add `Referrer-Policy: strict-origin-when-cross-origin` (the modern recommendation)."),
237
+ )
238
+ ]
239
+ if rp in ("unsafe-url",):
240
+ return [
241
+ Finding(
242
+ skill_id=SKILL_ID,
243
+ title=f"Referrer-Policy:{rp} leaks full URL cross-origin",
244
+ severity=Severity.MEDIUM,
245
+ target=target,
246
+ detail="unsafe-url sends the full URL to every cross-origin destination.",
247
+ remediation="Change to `strict-origin-when-cross-origin`.",
248
+ )
249
+ ]
250
+ return []
251
+
252
+
253
+ def _check_permissions_policy(headers: dict, target: str) -> list[Finding]:
254
+ if headers.get("Permissions-Policy"):
255
+ return []
256
+ return [
257
+ Finding(
258
+ skill_id=SKILL_ID,
259
+ title="Permissions-Policy header missing",
260
+ severity=Severity.LOW,
261
+ target=target,
262
+ detail=(
263
+ "Without Permissions-Policy, the browser permits the page to "
264
+ "request all device capabilities (camera, mic, geo, USB, "
265
+ "serial). On a public-content page these should be denied by "
266
+ "default."
267
+ ),
268
+ remediation=(
269
+ "Add `Permissions-Policy: camera=(), microphone=(), "
270
+ "geolocation=(), interest-cohort=()` (deny-all baseline)."
271
+ ),
272
+ )
273
+ ]
274
+
275
+
276
+ def _check_server_disclosure(headers: dict, target: str) -> list[Finding]:
277
+ server = headers.get("Server", "")
278
+ if re.search(r"\d+\.\d+", server):
279
+ return [
280
+ Finding(
281
+ skill_id=SKILL_ID,
282
+ title=f"Server header discloses version: {server}",
283
+ severity=Severity.LOW,
284
+ target=target,
285
+ detail=(
286
+ "The Server header includes a version number, letting "
287
+ "fingerprinters target known CVEs in that exact version."
288
+ ),
289
+ remediation=(
290
+ "nginx: `server_tokens off;`. "
291
+ "Apache: `ServerTokens Prod`. "
292
+ "Caddy: omit version by default (Caddy 2.x doesn't disclose)."
293
+ ),
294
+ cwe_id="CWE-200",
295
+ )
296
+ ]
297
+ return []
298
+
299
+
300
+ def _check_cache_control(headers: dict, target: str, authenticated: bool) -> list[Finding]:
301
+ cc = headers.get("Cache-Control", "").lower()
302
+ if authenticated and ("public" in cc or "max-age" in cc and "private" not in cc and "no-store" not in cc):
303
+ return [
304
+ Finding(
305
+ skill_id=SKILL_ID,
306
+ title="Authenticated endpoint allows shared caching",
307
+ severity=Severity.HIGH,
308
+ target=target,
309
+ detail=(
310
+ "Authenticated content with public-cacheable Cache-Control "
311
+ "can be served by shared caches (CDN, corporate proxy) to "
312
+ "different users — one user's authenticated response leaks "
313
+ "to another."
314
+ ),
315
+ remediation=("Set `Cache-Control: private, no-store` on every authenticated endpoint."),
316
+ cwe_id="CWE-525",
317
+ )
318
+ ]
319
+ return []
320
+
321
+
322
+ def main(argv: list[str] | None = None) -> int:
323
+ parser = argparse.ArgumentParser(description="HTTP security headers auditor")
324
+ parser.add_argument("url")
325
+ parser.add_argument("--authorized", action="store_true")
326
+ parser.add_argument("--output", default=None)
327
+ parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
328
+ parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
329
+ parser.add_argument("--timeout", type=float, default=10.0)
330
+ parser.add_argument("--authenticated", action="store_true", help="Apply stricter Cache-Control checks")
331
+ args = parser.parse_args(argv)
332
+
333
+ require_authorization(args.url, args.authorized)
334
+
335
+ sess = make_session(timeout=args.timeout)
336
+ resp = safe_get(sess, args.url, timeout=args.timeout)
337
+ if resp is None:
338
+ sys.stderr.write(f"ERROR: target {args.url!r} unreachable\n")
339
+ return 2
340
+
341
+ is_https = args.url.lower().startswith("https://")
342
+ target = args.url
343
+
344
+ findings: list[Finding] = []
345
+ findings.extend(_check_hsts(dict(resp.headers), target, is_https))
346
+ findings.extend(_check_csp(dict(resp.headers), target))
347
+ findings.extend(_check_clickjacking(dict(resp.headers), target))
348
+ findings.extend(_check_nosniff(dict(resp.headers), target))
349
+ findings.extend(_check_referrer(dict(resp.headers), target))
350
+ findings.extend(_check_permissions_policy(dict(resp.headers), target))
351
+ findings.extend(_check_server_disclosure(dict(resp.headers), target))
352
+ findings.extend(_check_cache_control(dict(resp.headers), target, args.authenticated))
353
+
354
+ floor = Severity(args.min_severity)
355
+ findings = [f for f in findings if f.severity.numeric >= floor.numeric]
356
+
357
+ emit(findings, args.output, args.format, target)
358
+ return exit_code(findings)
359
+
360
+
361
+ if __name__ == "__main__":
362
+ sys.exit(main())
@@ -0,0 +1,225 @@
1
+ ---
2
+ name: checking-license-compliance
3
+ description: |
4
+ Audit a project's dependency licenses against an explicit policy
5
+ (allow-list / deny-list / review-required) and flag incompatibilities
6
+ before they ship to production. Reads SPDX license identifiers from
7
+ npm package manifests, Python METADATA / PKG-INFO files, and
8
+ pyproject.toml; classifies each license by family (permissive,
9
+ weak-copyleft, strong-copyleft, proprietary, unknown); detects
10
+ copyleft contamination and SPDX-incompatible license combinations.
11
+ Use when: pre-release legal review, M&A code-audit due diligence,
12
+ preparing an OSS attribution NOTICE file, or switching a project's
13
+ own license.
14
+ Threshold: any GPL-family license in a project declaring MIT or
15
+ Apache-2.0; any UNKNOWN-license package; any metadata-vs-source
16
+ license mismatch.
17
+ Trigger with: "check licenses", "license compliance audit",
18
+ "SPDX scan", "GPL contamination check".
19
+ allowed-tools:
20
+ - Read
21
+ - Bash(python3:*)
22
+ - Bash(pip:*)
23
+ - Bash(npm:*)
24
+ - Glob
25
+ disallowed-tools:
26
+ - Bash(rm:*)
27
+ - Bash(curl:*)
28
+ - Bash(wget:*)
29
+ - Write(.env)
30
+ - Edit(.env)
31
+ version: 3.0.0-dev
32
+ author: Jeremy Longshore <jeremy@intentsolutions.io>
33
+ license: MIT
34
+ compatibility: Designed for Claude Code
35
+ tags:
36
+ - security
37
+ - licensing
38
+ - spdx
39
+ - compliance
40
+ - pentest
41
+ ---
42
+
43
+ # Checking License Compliance
44
+
45
+ ## Overview
46
+
47
+ License compliance is a security concern only in the indirect sense
48
+ that an unintended license obligation can force you to release
49
+ proprietary source code, retroactively invalidate a customer
50
+ contract, or render an M&A transaction infeasible. The cost is
51
+ legal and contractual rather than exploitative — but the
52
+ consequence ladder is real.
53
+
54
+ The most-stepped-on landmine is **copyleft contamination**:
55
+ unintentionally including a GPL or AGPL-licensed package in a
56
+ codebase the rest of which is permissively licensed (MIT, Apache-2.0,
57
+ BSD). The terms of the GPL family say that any project distributing
58
+ GPL code MUST itself release source under a GPL-compatible license.
59
+ If your `package.json` says MIT and one of your transitive deps is
60
+ GPL-2.0, you may be obligated to either re-license your code or
61
+ remove the dep.
62
+
63
+ This skill audits the resolved dependency tree against an explicit
64
+ policy file and emits findings for:
65
+
66
+ - Direct deps with deny-listed licenses
67
+ - Transitive deps with deny-listed licenses
68
+ - Packages with UNKNOWN license metadata (no SPDX identifier)
69
+ - License conflicts between metadata and source headers
70
+ - Combinations of licenses that are mutually incompatible (e.g.
71
+ GPL-2.0 + Apache-2.0 without a patent grant)
72
+
73
+ ## When the skill produces findings
74
+
75
+ | Finding | Severity | Threshold | Affected control |
76
+ |---|---|---|---|
77
+ | Strong-copyleft in permissive project | **CRITICAL** | GPL-2.0/3.0, AGPL-3.0, or similar in a project declaring MIT/Apache-2.0/BSD | (legal) |
78
+ | Weak-copyleft requiring source disclosure | **HIGH** | LGPL family in a project where the obligation isn't being met (no source-availability commitment) | (legal) |
79
+ | Custom / non-SPDX license | **HIGH** | License field doesn't match SPDX expression syntax; requires legal review | (legal) |
80
+ | Unknown license | **MEDIUM** | Package has no `license` field, no LICENSE file detected | (legal) |
81
+ | Deny-listed license (per policy) | **HIGH** | Package license is in the explicit deny-list in the policy file | (legal) |
82
+ | Review-required license (per policy) | **MEDIUM** | Package license is in the review-list (e.g. MPL-2.0) | (legal) |
83
+ | Incompatible license combination | **HIGH** | Detected pair of licenses known to conflict (e.g. GPL-2.0-only + Apache-2.0) | (legal) |
84
+ | License declared differently in metadata vs source headers | **MEDIUM** | LICENSE file says one license; per-file SPDX-License-Identifier headers say another | (legal) |
85
+ | Permissive license requiring attribution | **INFO** | MIT/BSD/Apache-2.0 — emit reminder that NOTICE / attribution file should list the package | (informational) |
86
+
87
+ ## Prerequisites
88
+
89
+ - Python 3.9+
90
+ - Target project with EITHER a `package.json` + `node_modules/`
91
+ OR a Python project (`pyproject.toml`/`requirements.txt`/
92
+ installed venv)
93
+ - Policy file at `./.license-policy.json` (auto-detected) or
94
+ passed via `--policy`. If absent, the skill uses a built-in
95
+ default policy that flags strong copyleft for permissive parent
96
+ projects.
97
+
98
+ ## Instructions
99
+
100
+ ### Step 1 — Identify the project's own declared license
101
+
102
+ The skill reads the project's top-level license from:
103
+
104
+ - npm: `package.json`'s `license` field
105
+ - Python: `pyproject.toml`'s `[project].license` table OR
106
+ `setup.cfg`'s `license` field
107
+
108
+ If the project's own license isn't declared, the skill emits a
109
+ FATAL operational finding — license compliance can't be checked
110
+ without a baseline. Add a `license` field before running.
111
+
112
+ ### Step 2 — Identify policy
113
+
114
+ The policy file is JSON:
115
+
116
+ ```json
117
+ {
118
+ "allow": ["MIT", "BSD-3-Clause", "Apache-2.0", "ISC", "BSD-2-Clause"],
119
+ "deny": ["GPL-2.0-only", "GPL-3.0-only", "AGPL-3.0-only", "AGPL-3.0-or-later"],
120
+ "review": ["MPL-2.0", "EPL-2.0", "CDDL-1.0", "LGPL-3.0-or-later"],
121
+ "project_license": "MIT"
122
+ }
123
+ ```
124
+
125
+ `allow`: licenses that pass without comment.
126
+ `deny`: licenses that produce a finding regardless of project license.
127
+ `review`: licenses that produce a MEDIUM-severity finding for legal review.
128
+ `project_license`: enforced — if the project declares this but a dep is in `deny`, finding is CRITICAL.
129
+
130
+ ### Step 3 — Run the scanner
131
+
132
+ ```bash
133
+ python3 ./scripts/check_licenses.py /path/to/project
134
+ ```
135
+
136
+ Options:
137
+
138
+ ```
139
+ Usage: check_licenses.py PATH [OPTIONS]
140
+
141
+ Options:
142
+ --output FILE Write findings to FILE (default: stdout)
143
+ --format FMT json | jsonl | markdown (default: markdown)
144
+ --min-severity SEV (default: info)
145
+ --policy FILE Override default policy
146
+ --emit-attribution Also emit an attribution file (NOTICE.md) listing
147
+ every permissive-licensed dep that requires attribution
148
+ ```
149
+
150
+ ### Step 4 — Interpret findings
151
+
152
+ CRITICAL findings block release pending legal review. Either remove
153
+ the offending dep, replace it with a permissively-licensed
154
+ alternative, or escalate to legal for a written exception.
155
+
156
+ HIGH findings require legal sign-off but don't necessarily block
157
+ release if the legal posture (e.g. service-only deployment under
158
+ AGPL) makes the obligation moot.
159
+
160
+ MEDIUM findings should be reviewed quarterly and either resolved
161
+ or moved into an explicit exception list.
162
+
163
+ INFO findings are reminders that an attribution / NOTICE file
164
+ should reference these packages.
165
+
166
+ ## Examples
167
+
168
+ ### Example 1 — Pre-release legal gate
169
+
170
+ ```bash
171
+ python3 ./scripts/check_licenses.py . --min-severity high --format json --output license-audit.json
172
+ jq -e '. == []' license-audit.json || { echo "License finding — legal review required"; exit 1; }
173
+ ```
174
+
175
+ ### Example 2 — Generate attribution file
176
+
177
+ ```bash
178
+ python3 ./scripts/check_licenses.py . --emit-attribution --format markdown --output NOTICE.md
179
+ ```
180
+
181
+ ### Example 3 — M&A due diligence
182
+
183
+ ```bash
184
+ mkdir -p evidence/legal/
185
+ python3 ./scripts/check_licenses.py target-acquisition-codebase/ \
186
+ --format json \
187
+ --output evidence/legal/license-audit-$(date +%Y%m%d).json
188
+ ```
189
+
190
+ ## Output
191
+
192
+ JSON / JSONL / Markdown per `lib/report.py`. Exit codes: 0 clean,
193
+ 1 high/critical, 2 error.
194
+
195
+ Each Finding includes:
196
+
197
+ - `id` — `license-compliance::<package>::<license-id>`
198
+ - `severity` — CRITICAL / HIGH / MEDIUM / LOW / INFO
199
+ - `category` — `license-compliance`
200
+ - `summary` — what's wrong
201
+ - `evidence` — package name, declared license, project license, policy match
202
+ - `references` — SPDX URL for the license, package home page
203
+
204
+ ## Error Handling
205
+
206
+ - **No project license** → emits an INFO/operational finding
207
+ recommending the operator add a `license` field, exits 2.
208
+ - **Unparseable policy file** → exits 2 with a parser error message.
209
+ - **Package with malformed license field** → treated as UNKNOWN
210
+ license, emits MEDIUM finding.
211
+ - **No SPDX identifier in source headers** → emits INFO finding
212
+ reminding that SPDX header convention catches contamination at
213
+ the file level.
214
+
215
+ ## Resources
216
+
217
+ - `references/THEORY.md` — SPDX license expression syntax, family
218
+ classifications, copyleft propagation theory, common license
219
+ incompatibilities, when LGPL static linking matters, AGPL
220
+ service-distribution clauses, public-domain edge cases (CC0 vs
221
+ unlicense)
222
+ - `references/PLAYBOOK.md` — Default policy templates per project
223
+ type (proprietary product, OSS library, internal-only tool, SaaS
224
+ service), attribution file generation, legal-counsel handoff
225
+ templates, replacing copyleft deps with permissive alternatives