@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,459 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""auditing-python-dependencies — wrap `pip-audit` into canonical Findings.
|
|
3
|
+
|
|
4
|
+
Auto-detects the project's requirement source (poetry.lock, Pipfile.lock,
|
|
5
|
+
requirements.txt, pyproject.toml, or installed environment), runs pip-audit,
|
|
6
|
+
parses the JSON output, and emits Findings via lib/finding.py. When pip-audit
|
|
7
|
+
is not installed, falls back to `pip list --outdated` and emits INFO findings
|
|
8
|
+
explaining the degraded scan.
|
|
9
|
+
|
|
10
|
+
Output formats and exit-code semantics are shared with the rest of the
|
|
11
|
+
penetration-tester v3 pack via lib/report.py.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 audit_python.py PATH [--output FILE] [--format json|jsonl|markdown]
|
|
15
|
+
[--min-severity sev] [--requirement FILE]
|
|
16
|
+
[--include-dev] [--strict]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
# --- Make lib/ importable regardless of CWD ----------------------------------
|
|
30
|
+
_LIB_ROOT = Path(__file__).resolve().parents[3]
|
|
31
|
+
sys.path.insert(0, str(_LIB_ROOT))
|
|
32
|
+
|
|
33
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
34
|
+
from lib import report # noqa: E402
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
SKILL_ID = "auditing-python-dependencies"
|
|
38
|
+
CATEGORY = "dependency-vulnerability"
|
|
39
|
+
CWE_DEFAULT = "CWE-1104"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --- Tool detection ----------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _pip_audit_present() -> bool:
|
|
46
|
+
return shutil.which("pip-audit") is not None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _pip_present() -> bool:
|
|
50
|
+
return shutil.which("pip") is not None or shutil.which("pip3") is not None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _pip_binary() -> str:
|
|
54
|
+
return "pip" if shutil.which("pip") is not None else "pip3"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- Requirement-file detection ---------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def detect_requirement_sources(directory: Path) -> list[Path]:
|
|
61
|
+
"""Return ordered list of plausible requirement sources for the project."""
|
|
62
|
+
candidates: list[Path] = []
|
|
63
|
+
for name in ("poetry.lock", "Pipfile.lock"):
|
|
64
|
+
p = directory / name
|
|
65
|
+
if p.exists():
|
|
66
|
+
candidates.append(p)
|
|
67
|
+
# requirements*.txt (top-level)
|
|
68
|
+
for p in sorted(directory.glob("requirements*.txt")):
|
|
69
|
+
candidates.append(p)
|
|
70
|
+
# pyproject.toml — only useful when poetry.lock isn't present and the
|
|
71
|
+
# project actually pins deps in pyproject (PEP 621 or poetry-without-lock).
|
|
72
|
+
pyproject = directory / "pyproject.toml"
|
|
73
|
+
if pyproject.exists() and not (directory / "poetry.lock").exists():
|
|
74
|
+
candidates.append(pyproject)
|
|
75
|
+
return candidates
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- pip-audit invocation ----------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_pip_audit(requirement_path: Path | None, strict: bool) -> tuple[list[dict[str, Any]] | None, str]:
|
|
82
|
+
"""Run pip-audit on the given requirement file (or installed env if None).
|
|
83
|
+
|
|
84
|
+
Returns (records, raw_stdout) or (None, raw_stdout) on parse failure.
|
|
85
|
+
"""
|
|
86
|
+
cmd: list[str] = ["pip-audit", "--format", "json", "--progress-spinner", "off"]
|
|
87
|
+
if strict:
|
|
88
|
+
cmd.append("--strict")
|
|
89
|
+
if requirement_path is not None:
|
|
90
|
+
requirement_path.suffix.lower()
|
|
91
|
+
# pip-audit's --requirement flag accepts requirements.txt; for
|
|
92
|
+
# poetry.lock / Pipfile.lock / pyproject.toml, pip-audit reads them
|
|
93
|
+
# via --requirement too as of v2.7+.
|
|
94
|
+
cmd.extend(["--requirement", str(requirement_path)])
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
proc = subprocess.run( # noqa: S603 — pip-audit is the audit tool
|
|
98
|
+
cmd,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=240,
|
|
102
|
+
check=False,
|
|
103
|
+
)
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
return None, "pip-audit timed out after 240s"
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
return None, "pip-audit binary not found"
|
|
108
|
+
|
|
109
|
+
stdout = proc.stdout or ""
|
|
110
|
+
try:
|
|
111
|
+
records = json.loads(stdout)
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
return None, stdout
|
|
114
|
+
|
|
115
|
+
# pip-audit returns a list of {name, version, vulns} per package
|
|
116
|
+
# (v2.x format); older versions returned a dependency-keyed dict.
|
|
117
|
+
if isinstance(records, list):
|
|
118
|
+
return records, stdout
|
|
119
|
+
if isinstance(records, dict) and "dependencies" in records:
|
|
120
|
+
return records.get("dependencies", []), stdout
|
|
121
|
+
return [], stdout
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --- pip-audit output parsing ------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _osv_severity_to_enum(severity_str: str) -> Severity:
|
|
128
|
+
"""OSV emits severity strings or CVSS strings; map to Severity."""
|
|
129
|
+
s = (severity_str or "").strip().lower()
|
|
130
|
+
if s.startswith("crit"):
|
|
131
|
+
return Severity.CRITICAL
|
|
132
|
+
if s.startswith("high"):
|
|
133
|
+
return Severity.HIGH
|
|
134
|
+
if s.startswith("med") or s.startswith("moderate"):
|
|
135
|
+
return Severity.MEDIUM
|
|
136
|
+
if s.startswith("low"):
|
|
137
|
+
return Severity.LOW
|
|
138
|
+
return Severity.INFO
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _extract_cvss(vuln: dict[str, Any]) -> float | None:
|
|
142
|
+
"""Best-effort CVSS score extraction from a pip-audit vuln record."""
|
|
143
|
+
severities = vuln.get("severity") or []
|
|
144
|
+
for entry in severities:
|
|
145
|
+
if not isinstance(entry, dict):
|
|
146
|
+
continue
|
|
147
|
+
# OSV severity may be {"type": "CVSS_V3", "score": "9.8/CVSS..."}
|
|
148
|
+
score_str = str(entry.get("score", ""))
|
|
149
|
+
# CVSS string looks like "9.8/CVSS:3.1/AV:N..." — pull the leading number.
|
|
150
|
+
head = score_str.split("/", 1)[0].strip()
|
|
151
|
+
try:
|
|
152
|
+
return float(head)
|
|
153
|
+
except ValueError:
|
|
154
|
+
continue
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_pip_audit_records(records: list[dict[str, Any]], target_label: str) -> list[Finding]:
|
|
159
|
+
findings: list[Finding] = []
|
|
160
|
+
for record in records:
|
|
161
|
+
pkg_name = record.get("name") or record.get("package") or "<unknown>"
|
|
162
|
+
installed_version = record.get("version") or "<unknown>"
|
|
163
|
+
vulns = record.get("vulns") or record.get("vulnerabilities") or []
|
|
164
|
+
for vuln in vulns:
|
|
165
|
+
adv_id = vuln.get("id") or vuln.get("ghsa") or vuln.get("pypa_id") or "ADVISORY"
|
|
166
|
+
aliases = vuln.get("aliases") or []
|
|
167
|
+
cve_id = next(
|
|
168
|
+
(a for a in aliases if isinstance(a, str) and a.upper().startswith("CVE-")),
|
|
169
|
+
None,
|
|
170
|
+
)
|
|
171
|
+
fix_versions = vuln.get("fix_versions") or []
|
|
172
|
+
description = vuln.get("description") or ""
|
|
173
|
+
|
|
174
|
+
cvss_score = _extract_cvss(vuln)
|
|
175
|
+
if cvss_score is not None:
|
|
176
|
+
severity = Severity.from_cvss(cvss_score)
|
|
177
|
+
else:
|
|
178
|
+
severity = _osv_severity_to_enum(str(vuln.get("severity_label", "")))
|
|
179
|
+
|
|
180
|
+
title = (
|
|
181
|
+
f"{adv_id} in {pkg_name}=={installed_version}"
|
|
182
|
+
if description == ""
|
|
183
|
+
else f"{adv_id}: {description.splitlines()[0][:100]}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
detail_lines = [
|
|
187
|
+
f"Affected package: {pkg_name}",
|
|
188
|
+
f"Installed version: {installed_version}",
|
|
189
|
+
f"Advisory: {adv_id}",
|
|
190
|
+
]
|
|
191
|
+
if cve_id:
|
|
192
|
+
detail_lines.append(f"CVE: {cve_id}")
|
|
193
|
+
if cvss_score is not None:
|
|
194
|
+
detail_lines.append(f"CVSS v3.1: {cvss_score}")
|
|
195
|
+
if description:
|
|
196
|
+
detail_lines.append(f"Summary: {description[:300]}")
|
|
197
|
+
|
|
198
|
+
if fix_versions:
|
|
199
|
+
remediation = (
|
|
200
|
+
f"1. Bump {pkg_name} to one of: "
|
|
201
|
+
f"{', '.join(fix_versions)}.\n"
|
|
202
|
+
f"2. Update the requirement file pin and run "
|
|
203
|
+
f"`pip install -U {pkg_name}` (or `poetry update {pkg_name}`).\n"
|
|
204
|
+
f"3. Run the test suite; CVE fixes sometimes include "
|
|
205
|
+
f"behavioral changes.\n"
|
|
206
|
+
f"4. Commit the lock-file diff."
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
remediation = (
|
|
210
|
+
"NO FIX AVAILABLE.\n"
|
|
211
|
+
"1. Subscribe to PyPA / GHSA notifications for this advisory.\n"
|
|
212
|
+
"2. If exploitable in your usage, replace the package or vendor + patch.\n"
|
|
213
|
+
"3. Document the exception with a re-evaluation date."
|
|
214
|
+
)
|
|
215
|
+
# Bump severity to HIGH if moderate or higher when no fix.
|
|
216
|
+
if severity.numeric >= 3:
|
|
217
|
+
severity = max(severity, Severity.HIGH, key=lambda s: s.numeric)
|
|
218
|
+
|
|
219
|
+
evidence_items: list[tuple[str, Any]] = [
|
|
220
|
+
("package", pkg_name),
|
|
221
|
+
("installed", installed_version),
|
|
222
|
+
("advisory", adv_id),
|
|
223
|
+
]
|
|
224
|
+
if fix_versions:
|
|
225
|
+
evidence_items.append(("fix_versions", ", ".join(fix_versions)))
|
|
226
|
+
if cve_id:
|
|
227
|
+
evidence_items.append(("cve", cve_id))
|
|
228
|
+
if cvss_score is not None:
|
|
229
|
+
evidence_items.append(("cvss", cvss_score))
|
|
230
|
+
|
|
231
|
+
references_list: list[str] = []
|
|
232
|
+
for r in vuln.get("references") or []:
|
|
233
|
+
if isinstance(r, dict) and r.get("url"):
|
|
234
|
+
references_list.append(r["url"])
|
|
235
|
+
elif isinstance(r, str):
|
|
236
|
+
references_list.append(r)
|
|
237
|
+
# Add OSV deeplink if it looks like an OSV/GHSA-style ID.
|
|
238
|
+
if adv_id.startswith("GHSA-"):
|
|
239
|
+
references_list.append(f"https://osv.dev/vulnerability/{adv_id}")
|
|
240
|
+
if cve_id:
|
|
241
|
+
references_list.append(f"https://nvd.nist.gov/vuln/detail/{cve_id}")
|
|
242
|
+
|
|
243
|
+
findings.append(
|
|
244
|
+
Finding(
|
|
245
|
+
skill_id=SKILL_ID,
|
|
246
|
+
title=title,
|
|
247
|
+
severity=severity,
|
|
248
|
+
target=f"{target_label}::{pkg_name}",
|
|
249
|
+
detail="\n".join(detail_lines),
|
|
250
|
+
remediation=remediation,
|
|
251
|
+
cvss_score=cvss_score,
|
|
252
|
+
cve_id=cve_id,
|
|
253
|
+
cwe_id=CWE_DEFAULT,
|
|
254
|
+
references=tuple(references_list),
|
|
255
|
+
evidence=tuple(evidence_items),
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
return findings
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# --- Fallback: pip list --outdated -------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _run_pip_outdated() -> list[dict[str, Any]]:
|
|
265
|
+
"""Run `pip list --outdated --format=json` as a degraded fallback."""
|
|
266
|
+
if not _pip_present():
|
|
267
|
+
return []
|
|
268
|
+
try:
|
|
269
|
+
proc = subprocess.run( # noqa: S603
|
|
270
|
+
[_pip_binary(), "list", "--outdated", "--format=json"],
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
timeout=60,
|
|
274
|
+
check=False,
|
|
275
|
+
)
|
|
276
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
277
|
+
return []
|
|
278
|
+
try:
|
|
279
|
+
return json.loads(proc.stdout or "[]")
|
|
280
|
+
except json.JSONDecodeError:
|
|
281
|
+
return []
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _outdated_to_findings(records: list[dict[str, Any]], target_label: str) -> list[Finding]:
|
|
285
|
+
findings: list[Finding] = []
|
|
286
|
+
for r in records:
|
|
287
|
+
name = r.get("name", "<unknown>")
|
|
288
|
+
installed = r.get("version", "?")
|
|
289
|
+
latest = r.get("latest_version", "?")
|
|
290
|
+
findings.append(
|
|
291
|
+
Finding(
|
|
292
|
+
skill_id=SKILL_ID,
|
|
293
|
+
title=f"{name} is outdated (no CVE data; pip-audit not installed)",
|
|
294
|
+
severity=Severity.INFO,
|
|
295
|
+
target=f"{target_label}::{name}",
|
|
296
|
+
detail=(
|
|
297
|
+
f"pip-audit was not found on PATH; falling back to pip list --outdated.\n"
|
|
298
|
+
f"Package {name} is at {installed}; latest is {latest}.\n"
|
|
299
|
+
f"Install pip-audit (`pip install pip-audit`) and re-run for accurate CVE detection."
|
|
300
|
+
),
|
|
301
|
+
remediation=(
|
|
302
|
+
"Install pip-audit and re-run this skill for vulnerability data.\n`pip install pip-audit`"
|
|
303
|
+
),
|
|
304
|
+
cwe_id=CWE_DEFAULT,
|
|
305
|
+
references=(
|
|
306
|
+
"https://pypi.org/project/pip-audit/",
|
|
307
|
+
"https://github.com/pypa/pip-audit",
|
|
308
|
+
),
|
|
309
|
+
evidence=(
|
|
310
|
+
("package", name),
|
|
311
|
+
("installed", installed),
|
|
312
|
+
("latest", latest),
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
return findings
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# --- Operational helpers -----------------------------------------------------
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _info_finding(title: str, detail: str, target: str) -> Finding:
|
|
323
|
+
return Finding(
|
|
324
|
+
skill_id=SKILL_ID,
|
|
325
|
+
title=title,
|
|
326
|
+
severity=Severity.INFO,
|
|
327
|
+
target=target,
|
|
328
|
+
detail=detail,
|
|
329
|
+
remediation="Operational issue; no security action required.",
|
|
330
|
+
references=(),
|
|
331
|
+
evidence=(),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def audit_directory(
|
|
336
|
+
directory: Path,
|
|
337
|
+
requirement_paths: list[Path] | None,
|
|
338
|
+
strict: bool,
|
|
339
|
+
) -> list[Finding]:
|
|
340
|
+
if not _pip_audit_present():
|
|
341
|
+
outdated = _run_pip_outdated()
|
|
342
|
+
if outdated:
|
|
343
|
+
findings = _outdated_to_findings(outdated, directory.name)
|
|
344
|
+
findings.insert(
|
|
345
|
+
0,
|
|
346
|
+
_info_finding(
|
|
347
|
+
"pip-audit not installed — degraded scan",
|
|
348
|
+
"Falling back to pip list --outdated. Install pip-audit for true vulnerability detection.",
|
|
349
|
+
str(directory),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
return findings
|
|
353
|
+
return [
|
|
354
|
+
_info_finding(
|
|
355
|
+
"pip-audit not installed and no fallback data available",
|
|
356
|
+
"Install pip-audit (`pip install pip-audit`) and re-run.",
|
|
357
|
+
str(directory),
|
|
358
|
+
)
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
sources = requirement_paths or detect_requirement_sources(directory)
|
|
362
|
+
if not sources:
|
|
363
|
+
# No requirement file found; pip-audit can audit the installed env.
|
|
364
|
+
records, raw = _run_pip_audit(None, strict)
|
|
365
|
+
if records is None:
|
|
366
|
+
return [
|
|
367
|
+
_info_finding(
|
|
368
|
+
"pip-audit returned non-JSON output",
|
|
369
|
+
f"Raw stdout (first 500 chars): {raw[:500]}",
|
|
370
|
+
str(directory),
|
|
371
|
+
)
|
|
372
|
+
]
|
|
373
|
+
if not records:
|
|
374
|
+
return [
|
|
375
|
+
_info_finding(
|
|
376
|
+
"no Python vulnerabilities found (installed-env scan)",
|
|
377
|
+
"pip-audit found no advisories against the currently installed packages.",
|
|
378
|
+
str(directory),
|
|
379
|
+
)
|
|
380
|
+
]
|
|
381
|
+
return _parse_pip_audit_records(records, directory.name)
|
|
382
|
+
|
|
383
|
+
# Iterate over detected sources, accumulate findings.
|
|
384
|
+
all_findings: list[Finding] = []
|
|
385
|
+
seen_fingerprints: set[str] = set()
|
|
386
|
+
for src in sources:
|
|
387
|
+
records, raw = _run_pip_audit(src, strict)
|
|
388
|
+
if records is None:
|
|
389
|
+
all_findings.append(
|
|
390
|
+
_info_finding(
|
|
391
|
+
f"pip-audit non-JSON output for {src.name}",
|
|
392
|
+
f"Raw stdout (first 500 chars): {raw[:500]}",
|
|
393
|
+
str(src),
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
continue
|
|
397
|
+
for f in _parse_pip_audit_records(records, f"{directory.name}/{src.name}"):
|
|
398
|
+
fp = f.fingerprint()
|
|
399
|
+
if fp in seen_fingerprints:
|
|
400
|
+
continue
|
|
401
|
+
seen_fingerprints.add(fp)
|
|
402
|
+
all_findings.append(f)
|
|
403
|
+
|
|
404
|
+
if not all_findings:
|
|
405
|
+
all_findings = [
|
|
406
|
+
_info_finding(
|
|
407
|
+
"no Python vulnerabilities found",
|
|
408
|
+
"pip-audit found no advisories across the detected requirement sources.",
|
|
409
|
+
str(directory),
|
|
410
|
+
)
|
|
411
|
+
]
|
|
412
|
+
return all_findings
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# --- CLI ---------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
419
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
420
|
+
p.add_argument("path", help="Path to Python project root")
|
|
421
|
+
p.add_argument("--output", default=None)
|
|
422
|
+
p.add_argument(
|
|
423
|
+
"--format",
|
|
424
|
+
default="markdown",
|
|
425
|
+
choices=["json", "jsonl", "markdown"],
|
|
426
|
+
)
|
|
427
|
+
p.add_argument(
|
|
428
|
+
"--min-severity",
|
|
429
|
+
default="info",
|
|
430
|
+
choices=["info", "low", "medium", "high", "critical"],
|
|
431
|
+
)
|
|
432
|
+
p.add_argument(
|
|
433
|
+
"--requirement",
|
|
434
|
+
action="append",
|
|
435
|
+
help="Override auto-detected requirements (repeatable)",
|
|
436
|
+
)
|
|
437
|
+
p.add_argument("--include-dev", action="store_true")
|
|
438
|
+
p.add_argument("--strict", action="store_true")
|
|
439
|
+
return p
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _filter_min_severity(findings: list[Finding], min_sev: str) -> list[Finding]:
|
|
443
|
+
floor = Severity(min_sev).numeric
|
|
444
|
+
return [f for f in findings if f.severity.numeric >= floor]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def main(argv: list[str] | None = None) -> int:
|
|
448
|
+
args = _build_arg_parser().parse_args(argv)
|
|
449
|
+
directory = Path(args.path).resolve()
|
|
450
|
+
req_paths = [Path(p).resolve() for p in (args.requirement or [])] or None
|
|
451
|
+
|
|
452
|
+
findings = audit_directory(directory, req_paths, args.strict)
|
|
453
|
+
findings = _filter_min_severity(findings, args.min_severity)
|
|
454
|
+
report.emit(findings, args.output, args.format, scan_target=str(directory))
|
|
455
|
+
return report.exit_code(findings)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
sys.exit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: checking-http-security-headers
|
|
3
|
+
description: |
|
|
4
|
+
Audit a target's HTTP security headers — CSP, HSTS, X-Frame-Options,
|
|
5
|
+
X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and the
|
|
6
|
+
Cross-Origin trio (COOP, COEP, CORP).
|
|
7
|
+
Use when: SOC2 / PCI auditor flagged "missing security headers" or a
|
|
8
|
+
Mozilla Observatory grade is below B, OR you need HSTS preload
|
|
9
|
+
eligibility for chrome://net-internals.
|
|
10
|
+
Threshold: any missing required header on production HTML response,
|
|
11
|
+
HSTS max-age below 31536000s (preload requirement), CSP with
|
|
12
|
+
'unsafe-inline' or 'unsafe-eval', X-Frame-Options absent AND CSP
|
|
13
|
+
frame-ancestors absent (clickjacking), Cache-Control allowing public
|
|
14
|
+
cache on authenticated endpoint.
|
|
15
|
+
Trigger with: "audit security headers", "check csp", "hsts check",
|
|
16
|
+
"header posture".
|
|
17
|
+
allowed-tools:
|
|
18
|
+
- Read
|
|
19
|
+
- Bash(python3:*)
|
|
20
|
+
- Bash(curl:*)
|
|
21
|
+
disallowed-tools:
|
|
22
|
+
- Bash(rm:*)
|
|
23
|
+
- Edit(/etc/*)
|
|
24
|
+
version: 3.0.0-dev
|
|
25
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
26
|
+
license: MIT
|
|
27
|
+
compatibility: Designed for Claude Code
|
|
28
|
+
tags:
|
|
29
|
+
- security
|
|
30
|
+
- http-headers
|
|
31
|
+
- csp
|
|
32
|
+
- hsts
|
|
33
|
+
- pentest
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Checking HTTP Security Headers
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
|
|
40
|
+
HTTP response headers are the cheapest defense-in-depth layer most web
|
|
41
|
+
apps ship. Each header closes one specific attack class — HSTS forces
|
|
42
|
+
HTTPS, CSP blocks script injection, X-Frame-Options blocks clickjacking,
|
|
43
|
+
etc. Missing headers don't break the app; they just leave the attack
|
|
44
|
+
class open. This skill probes for the presence + value correctness of
|
|
45
|
+
the canonical security-relevant headers.
|
|
46
|
+
|
|
47
|
+
## When the skill produces findings
|
|
48
|
+
|
|
49
|
+
| Finding | Severity | Threshold | Affected control |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| HSTS header missing | **HIGH** | No Strict-Transport-Security on HTTPS response | OWASP A05:2021 |
|
|
52
|
+
| HSTS max-age below preload threshold | **MEDIUM** | max-age under 31536000s (1y) | hstspreload.org |
|
|
53
|
+
| HSTS includeSubDomains missing for preload | **LOW** | preload directive without includeSubDomains | hstspreload.org |
|
|
54
|
+
| CSP header missing | **HIGH** | No Content-Security-Policy header | OWASP A03:2021 |
|
|
55
|
+
| CSP allows unsafe-inline | **MEDIUM** | script-src or style-src includes 'unsafe-inline' | OWASP A03:2021 |
|
|
56
|
+
| CSP allows unsafe-eval | **MEDIUM** | script-src includes 'unsafe-eval' | OWASP A03:2021 |
|
|
57
|
+
| CSP frame-ancestors AND X-Frame-Options both missing | **HIGH** | Clickjacking open | CWE-1021 |
|
|
58
|
+
| X-Content-Type-Options:nosniff missing | **MEDIUM** | MIME-sniff attack open | OWASP A05:2021 |
|
|
59
|
+
| Referrer-Policy missing or unsafe-url | **MEDIUM** | Cross-origin URL leakage | OWASP A05:2021 |
|
|
60
|
+
| Permissions-Policy missing | **LOW** | Camera/mic/geo permissions unrestricted | Permissions Policy spec |
|
|
61
|
+
| Server: header discloses version | **LOW** | nginx/1.18.0 → fingerprintable | CWE-200 |
|
|
62
|
+
| Cache-Control public on authenticated response | **HIGH** | Shared cache may serve user A's response to user B | CWE-525 |
|
|
63
|
+
|
|
64
|
+
## Prerequisites
|
|
65
|
+
|
|
66
|
+
- Python 3.9+
|
|
67
|
+
- Authorization for non-local targets
|
|
68
|
+
|
|
69
|
+
## Instructions
|
|
70
|
+
|
|
71
|
+
### Step 1 — Confirm authorization
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
"Do you have authorization to perform header testing on this target?
|
|
75
|
+
I need confirmation before proceeding."
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Step 2 — Run the scanner
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/checking-http-security-headers/scripts/check_headers.py \
|
|
82
|
+
https://example.com \
|
|
83
|
+
--authorized
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Usage: check_headers.py URL [OPTIONS]
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
--authorized Attest authorization (required for non-local)
|
|
93
|
+
--output FILE
|
|
94
|
+
--format FMT json | jsonl | markdown (default: markdown)
|
|
95
|
+
--min-severity SEV (default: info)
|
|
96
|
+
--timeout SECS Per-probe timeout (default: 10)
|
|
97
|
+
--authenticated Treat as authenticated endpoint (stricter Cache-Control gate)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Step 3 — Interpret findings
|
|
101
|
+
|
|
102
|
+
HIGH = open exploitable class (no HSTS = MITM downgrade open; no CSP =
|
|
103
|
+
XSS class wide open; no clickjacking guard = UI-redress attacks).
|
|
104
|
+
MEDIUM/LOW = posture hardening.
|
|
105
|
+
|
|
106
|
+
### Step 4 — Cross-skill chaining
|
|
107
|
+
|
|
108
|
+
- After this skill, suggest `auditing-cors-policy` (#3) — CSP and CORS
|
|
109
|
+
interact; certain CSP directives need matching CORS headers.
|
|
110
|
+
- For HSTS preload submission, see `references/PLAYBOOK.md` § HSTS
|
|
111
|
+
preload checklist.
|
|
112
|
+
|
|
113
|
+
## Examples
|
|
114
|
+
|
|
115
|
+
### Example 1 — Mozilla Observatory grade improvement
|
|
116
|
+
|
|
117
|
+
User: "Observatory gives us a D. What's missing?"
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/checking-http-security-headers/scripts/check_headers.py \
|
|
121
|
+
https://example.com \
|
|
122
|
+
--authorized \
|
|
123
|
+
--format markdown
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The Markdown report groups by severity; map each finding to the
|
|
127
|
+
`PLAYBOOK.md` snippet for the target server type. Observatory grade
|
|
128
|
+
typically moves D → B after addressing all HIGH findings.
|
|
129
|
+
|
|
130
|
+
### Example 2 — HSTS preload eligibility pre-submission
|
|
131
|
+
|
|
132
|
+
User: "We want to submit to hstspreload.org. Is our HSTS config ready?"
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/checking-http-security-headers/scripts/check_headers.py \
|
|
136
|
+
https://example.com \
|
|
137
|
+
--authorized --min-severity low
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Look for "HSTS max-age below preload threshold" and "includeSubDomains
|
|
141
|
+
missing" — both must clear before submission, OR hstspreload.org will
|
|
142
|
+
reject.
|
|
143
|
+
|
|
144
|
+
### Example 3 — Authenticated-endpoint Cache-Control sweep
|
|
145
|
+
|
|
146
|
+
User: "We had a Cache-Control bug last quarter where authenticated
|
|
147
|
+
responses got cached publicly. Audit /api/* to make sure it's fixed."
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/checking-http-security-headers/scripts/check_headers.py \
|
|
151
|
+
https://api.example.com/me \
|
|
152
|
+
--authorized --authenticated
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The `--authenticated` flag bumps Cache-Control posture from MEDIUM to
|
|
156
|
+
HIGH and adds a check for `Cache-Control: public` (forbidden on
|
|
157
|
+
authenticated content).
|
|
158
|
+
|
|
159
|
+
## Output
|
|
160
|
+
|
|
161
|
+
JSON / JSONL / Markdown. Exit codes 0 / 1 / 2 per `lib/report.py`.
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
- **No HTML response** → INFO finding noting headers may not apply
|
|
166
|
+
(JSON APIs use a subset of headers).
|
|
167
|
+
- **Redirect to login** → follows once, audits the destination page.
|
|
168
|
+
- **Connection error** → exit 2.
|
|
169
|
+
|
|
170
|
+
## Resources
|
|
171
|
+
|
|
172
|
+
- `references/THEORY.md` — Per-header reasoning, attack-class mapping
|
|
173
|
+
- `references/PLAYBOOK.md` — Config snippets per server type for each
|
|
174
|
+
required header
|
|
175
|
+
- `../analyzing-tls-config/references/AUTHORIZATION.md` — Active-scan
|
|
176
|
+
authorization
|