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