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