@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,396 @@
1
+ #!/usr/bin/env python3
2
+ """composing-vulnerability-report — render cluster 1-4 findings into a deliverable.
3
+
4
+ Reads JSON/JSONL findings files, deduplicates by Finding.fingerprint(),
5
+ groups by severity, and writes a single deliverable-grade markdown
6
+ vulnerability report. Emits operational Findings via lib/finding.py for any
7
+ parse / structural issue encountered while composing.
8
+
9
+ Usage:
10
+ python3 compose_report.py PATH [--source FILE]
11
+ [--report-output FILE]
12
+ [--engagement-id ID]
13
+ [--output FILE] [--format json|jsonl|markdown]
14
+ [--min-severity sev] [--include-info]
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ from collections import Counter, defaultdict
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ # --- lib/ import -------------------------------------------------------------
28
+ _LIB_ROOT = Path(__file__).resolve().parents[3]
29
+ sys.path.insert(0, str(_LIB_ROOT))
30
+
31
+ from lib.finding import Finding, Severity, from_json as finding_from_json # noqa: E402
32
+ from lib import report # noqa: E402
33
+
34
+
35
+ SKILL_ID = "composing-vulnerability-report"
36
+ CATEGORY = "report-composition"
37
+ REQUIRED_FIELDS = ("title", "severity", "target", "detail", "remediation")
38
+
39
+
40
+ # --- Helpers ----------------------------------------------------------------
41
+
42
+
43
+ def _f(
44
+ severity: Severity,
45
+ title: str,
46
+ target: str,
47
+ detail: str,
48
+ remediation: str,
49
+ evidence: tuple[tuple[str, Any], ...] = (),
50
+ ) -> Finding:
51
+ return Finding(
52
+ skill_id=SKILL_ID,
53
+ title=title,
54
+ severity=severity,
55
+ target=target,
56
+ detail=detail,
57
+ remediation=remediation,
58
+ evidence=evidence,
59
+ )
60
+
61
+
62
+ # --- Source discovery -------------------------------------------------------
63
+
64
+
65
+ def discover_sources(root: Path, explicit: list[str]) -> list[Path]:
66
+ if explicit:
67
+ return [Path(p).resolve() for p in explicit]
68
+ out: list[Path] = []
69
+ findings_dir = root / "findings"
70
+ if findings_dir.is_dir():
71
+ out.extend(sorted(findings_dir.glob("**/*.json")))
72
+ out.extend(sorted(findings_dir.glob("**/*.jsonl")))
73
+ return out
74
+
75
+
76
+ def load_finding_records(path: Path) -> tuple[list[dict[str, Any]], str | None]:
77
+ """Load findings from a file. Returns (records, error_message_or_None)."""
78
+ try:
79
+ text = path.read_text(encoding="utf-8")
80
+ except OSError as e:
81
+ return [], f"read failed: {e}"
82
+ text = text.strip()
83
+ if not text:
84
+ return [], None
85
+ out: list[dict[str, Any]] = []
86
+ if path.suffix == ".jsonl" or "\n{" in text:
87
+ for line in text.splitlines():
88
+ line = line.strip()
89
+ if not line:
90
+ continue
91
+ try:
92
+ out.append(json.loads(line))
93
+ except json.JSONDecodeError as e:
94
+ return out, f"jsonl line parse error: {e}"
95
+ return out, None
96
+ try:
97
+ data = json.loads(text)
98
+ except json.JSONDecodeError as e:
99
+ return [], f"json parse error: {e}"
100
+ if isinstance(data, list):
101
+ out = [r for r in data if isinstance(r, dict)]
102
+ elif isinstance(data, dict) and "findings" in data:
103
+ out = [r for r in data["findings"] if isinstance(r, dict)]
104
+ elif isinstance(data, dict):
105
+ out = [data]
106
+ return out, None
107
+
108
+
109
+ # --- Finding normalization + dedup ------------------------------------------
110
+
111
+
112
+ def normalize_record(record: dict[str, Any]) -> tuple[Finding | None, str | None]:
113
+ """Convert a record dict into a Finding. Returns (finding, error_or_None)."""
114
+ missing = [f for f in REQUIRED_FIELDS if f not in record]
115
+ if missing:
116
+ return None, f"missing required fields: {', '.join(missing)}"
117
+ try:
118
+ return finding_from_json(record), None
119
+ except (KeyError, ValueError, TypeError) as e:
120
+ return None, f"finding parse error: {e}"
121
+
122
+
123
+ # --- Engagement-id detection ------------------------------------------------
124
+
125
+
126
+ def detect_engagement_id(root: Path) -> str:
127
+ roe = root / "roe.yaml"
128
+ if not roe.exists():
129
+ return root.name
130
+ try:
131
+ text = roe.read_text(encoding="utf-8")
132
+ except OSError:
133
+ return root.name
134
+ for line in text.splitlines():
135
+ line = line.strip()
136
+ if line.startswith("engagement_id:"):
137
+ return line.split(":", 1)[1].strip().strip('"').strip("'")
138
+ return root.name
139
+
140
+
141
+ # --- Report rendering -------------------------------------------------------
142
+
143
+
144
+ def render_summary_table(
145
+ by_severity: dict[Severity, list[Finding]],
146
+ ) -> str:
147
+ lines = ["| Severity | Count |", "|---|---|"]
148
+ for sev in (Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW, Severity.INFO):
149
+ count = len(by_severity.get(sev, []))
150
+ lines.append(f"| **{sev.value.upper()}** | {count} |")
151
+ return "\n".join(lines)
152
+
153
+
154
+ def render_finding_section(f: Finding) -> str:
155
+ anchor = f.fingerprint()
156
+ refs = "\n".join(f"- {r}" for r in f.references) if f.references else "(none)"
157
+ evidence_lines = "\n".join(f"- **{k}**: `{v}`" for k, v in f.evidence) if f.evidence else "(none)"
158
+ sev = f.severity.value.upper()
159
+ cvss = f"\n- **CVSS v3.1:** {f.cvss_score:.1f}" if f.cvss_score else ""
160
+ cve = f"\n- **CVE:** {f.cve_id}" if f.cve_id else ""
161
+ cwe = f"\n- **CWE:** {f.cwe_id}" if f.cwe_id else ""
162
+ owasp = f"\n- **OWASP:** {f.owasp_category}" if f.owasp_category else ""
163
+
164
+ return f"""### {f.title}
165
+ <a id="finding-{anchor}"></a>
166
+
167
+ - **Severity:** {sev}
168
+ - **Affected target:** `{f.target}`
169
+ - **Source skill:** `{f.skill_id}`{cvss}{cve}{cwe}{owasp}
170
+
171
+ #### Detail
172
+
173
+ {f.detail}
174
+
175
+ #### Remediation
176
+
177
+ {f.remediation}
178
+
179
+ #### Evidence
180
+
181
+ {evidence_lines}
182
+
183
+ #### References
184
+
185
+ {refs}
186
+
187
+ ---
188
+ """
189
+
190
+
191
+ def render_report(
192
+ findings: list[Finding],
193
+ engagement_id: str,
194
+ source_files: list[Path],
195
+ include_info: bool,
196
+ ) -> str:
197
+ by_severity: dict[Severity, list[Finding]] = defaultdict(list)
198
+ for f in findings:
199
+ if not include_info and f.severity == Severity.INFO:
200
+ continue
201
+ by_severity[f.severity].append(f)
202
+
203
+ now = datetime.now(timezone.utc).isoformat()
204
+
205
+ header = f"""# Vulnerability Report — {engagement_id}
206
+
207
+ > Generated by `{SKILL_ID}` at {now}.
208
+ > Source files: {", ".join(str(s.name) for s in source_files) or "(none)"}.
209
+
210
+ ## Summary
211
+
212
+ {render_summary_table(by_severity)}
213
+
214
+ """
215
+
216
+ sections: list[str] = []
217
+ for sev in (Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW, Severity.INFO):
218
+ bucket = by_severity.get(sev, [])
219
+ if not bucket:
220
+ continue
221
+ if sev == Severity.INFO and not include_info:
222
+ continue
223
+ sections.append(f"## {sev.value.upper()} ({len(bucket)})\n")
224
+ # Stable sort: by skill_id then title
225
+ for f in sorted(bucket, key=lambda x: (x.skill_id, x.title)):
226
+ sections.append(render_finding_section(f))
227
+ return header + "\n".join(sections)
228
+
229
+
230
+ # --- CLI ---------------------------------------------------------------------
231
+
232
+
233
+ def _build_arg_parser() -> argparse.ArgumentParser:
234
+ p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
235
+ p.add_argument("path", help="Engagement directory")
236
+ p.add_argument("--source", action="append", default=[], help="Findings file (repeatable)")
237
+ p.add_argument("--report-output", default=None)
238
+ p.add_argument("--engagement-id", default=None)
239
+ p.add_argument("--output", default=None)
240
+ p.add_argument("--format", default="markdown", choices=["json", "jsonl", "markdown"])
241
+ p.add_argument(
242
+ "--min-severity",
243
+ default="info",
244
+ choices=["info", "low", "medium", "high", "critical"],
245
+ )
246
+ p.add_argument("--include-info", action="store_true")
247
+ return p
248
+
249
+
250
+ def _filter_min_severity_for_report(findings: list[Finding], min_sev: str) -> list[Finding]:
251
+ floor = Severity(min_sev).numeric
252
+ return [f for f in findings if f.severity.numeric >= floor]
253
+
254
+
255
+ def main(argv: list[str] | None = None) -> int:
256
+ args = _build_arg_parser().parse_args(argv)
257
+ root = Path(args.path).resolve()
258
+ if not root.exists():
259
+ f = _f(
260
+ Severity.CRITICAL,
261
+ f"engagement path missing: {root}",
262
+ str(root),
263
+ f"PATH `{root}` does not exist; cannot compose report.",
264
+ "Verify the engagement directory and re-run.",
265
+ )
266
+ report.emit([f], args.output, args.format, scan_target=str(root))
267
+ return 1
268
+
269
+ sources = discover_sources(root, args.source)
270
+ if not sources:
271
+ f = _f(
272
+ Severity.HIGH,
273
+ "no findings sources",
274
+ str(root),
275
+ f"No JSON or JSONL findings files were found under `{root}`.",
276
+ "Run at least one cluster 1-4 scan skill to produce findings, or pass explicit --source paths.",
277
+ )
278
+ report.emit([f], args.output, args.format, scan_target=str(root))
279
+ return 1
280
+
281
+ op_findings: list[Finding] = []
282
+ all_findings: list[Finding] = []
283
+ fp_counts: Counter[str] = Counter()
284
+
285
+ for src in sources:
286
+ records, err = load_finding_records(src)
287
+ if err:
288
+ op_findings.append(
289
+ _f(
290
+ Severity.HIGH,
291
+ f"source unparseable: {src.name}",
292
+ str(src),
293
+ err,
294
+ "Fix the source file or exclude it via explicit --source list.",
295
+ )
296
+ )
297
+ continue
298
+ if not records:
299
+ op_findings.append(
300
+ _f(
301
+ Severity.INFO,
302
+ f"source empty: {src.name}",
303
+ str(src),
304
+ "Source file contained no records.",
305
+ "Skipped.",
306
+ )
307
+ )
308
+ continue
309
+ for rec in records:
310
+ f, ferr = normalize_record(rec)
311
+ if f is None:
312
+ op_findings.append(
313
+ _f(
314
+ Severity.HIGH,
315
+ f"record dropped from {src.name}",
316
+ str(src),
317
+ ferr or "unknown normalization error",
318
+ "Fix the record's structure; re-run.",
319
+ )
320
+ )
321
+ continue
322
+ fp = f.fingerprint()
323
+ fp_counts[fp] += 1
324
+ if fp_counts[fp] == 1:
325
+ all_findings.append(f)
326
+
327
+ if fp_counts:
328
+ dup_count = sum(1 for c in fp_counts.values() if c > 1)
329
+ if dup_count:
330
+ op_findings.append(
331
+ _f(
332
+ Severity.INFO,
333
+ f"deduplicated {dup_count} duplicate fingerprint(s)",
334
+ str(root),
335
+ f"{dup_count} unique findings appeared in more than one source; each was included exactly once.",
336
+ "No action required.",
337
+ evidence=(("unique_findings", len(fp_counts)), ("duplicates_collapsed", dup_count)),
338
+ )
339
+ )
340
+
341
+ if not all_findings:
342
+ op_findings.append(
343
+ _f(
344
+ Severity.HIGH,
345
+ "no findings to report",
346
+ str(root),
347
+ "All sources were empty, malformed, or contained only records missing required fields.",
348
+ "Resolve source issues and re-run.",
349
+ )
350
+ )
351
+
352
+ engagement_id = args.engagement_id or detect_engagement_id(root)
353
+ filtered_for_report = _filter_min_severity_for_report(all_findings, args.min_severity)
354
+ report_md = render_report(filtered_for_report, engagement_id, sources, args.include_info)
355
+
356
+ report_path = (
357
+ Path(args.report_output).resolve() if args.report_output else root / "reports" / "vulnerability-report.md"
358
+ )
359
+ try:
360
+ report_path.parent.mkdir(parents=True, exist_ok=True)
361
+ report_path.write_text(report_md, encoding="utf-8")
362
+ op_findings.append(
363
+ _f(
364
+ Severity.INFO,
365
+ f"vulnerability report written: {report_path.name}",
366
+ str(report_path),
367
+ f"Report contains {len(filtered_for_report)} findings across "
368
+ f"{sum(1 for f in filtered_for_report if f.severity == Severity.CRITICAL)} critical, "
369
+ f"{sum(1 for f in filtered_for_report if f.severity == Severity.HIGH)} high, "
370
+ f"{sum(1 for f in filtered_for_report if f.severity == Severity.MEDIUM)} medium, "
371
+ f"{sum(1 for f in filtered_for_report if f.severity == Severity.LOW)} low.",
372
+ "Hand off to customer per the engagement closeout protocol.",
373
+ evidence=(
374
+ ("report_path", str(report_path)),
375
+ ("finding_count", len(filtered_for_report)),
376
+ ("source_count", len(sources)),
377
+ ),
378
+ )
379
+ )
380
+ except OSError as e:
381
+ op_findings.append(
382
+ _f(
383
+ Severity.HIGH,
384
+ "report write failed",
385
+ str(report_path),
386
+ f"OSError: {e}",
387
+ "Resolve permissions/path; re-run.",
388
+ )
389
+ )
390
+
391
+ report.emit(op_findings, args.output, args.format, scan_target=str(root))
392
+ return report.exit_code(op_findings)
393
+
394
+
395
+ if __name__ == "__main__":
396
+ sys.exit(main())
@@ -0,0 +1,247 @@
1
+ ---
2
+ name: confirming-pentest-authorization
3
+ description: |
4
+ Verify that a penetration test has explicit, written, signed
5
+ authorization before any scanning begins. Reads a Rules-of-
6
+ Engagement (ROE) attestation file, validates required fields
7
+ (authorizer, in-scope targets, time window, emergency contact,
8
+ signature), checks the signer against an allowlist, and emits a
9
+ CRITICAL finding if anything is missing. Designed as the first
10
+ skill the orchestrator routes to.
11
+ Use when: starting a new engagement, after a scope change, or
12
+ before any cluster 1-4 scan skill runs.
13
+ Threshold: any missing or unsigned ROE field; any time-window
14
+ expiry; any in-scope target outside the authorized list.
15
+ Trigger with: "confirm authorization", "verify ROE", "check
16
+ pentest authz", "pre-flight authorization".
17
+ allowed-tools:
18
+ - Read
19
+ - Bash(python3:*)
20
+ - Glob
21
+ disallowed-tools:
22
+ - Bash(rm:*)
23
+ - Bash(curl:*)
24
+ - Bash(wget:*)
25
+ - Bash(nmap:*)
26
+ - Bash(nikto:*)
27
+ - Bash(sqlmap:*)
28
+ - Write(.env)
29
+ - Edit(.env)
30
+ version: 3.0.0-dev
31
+ author: Jeremy Longshore <jeremy@intentsolutions.io>
32
+ license: MIT
33
+ compatibility: Designed for Claude Code
34
+ tags:
35
+ - security
36
+ - engagement-governance
37
+ - authorization
38
+ - roe
39
+ - pentest
40
+ ---
41
+
42
+ # Confirming Pentest Authorization
43
+
44
+ ## Overview
45
+
46
+ Penetration testing is computer access. Without explicit
47
+ authorization from the owner of the system under test, that
48
+ access is a crime — Computer Fraud and Abuse Act in the US,
49
+ Computer Misuse Act in the UK, equivalent laws everywhere
50
+ else. The line between an authorized pentester and an
51
+ unauthorized attacker is one signature on one document.
52
+
53
+ The penetration-tester pack's other skills (TLS analysis, CORS
54
+ audit, dependency CVE scan, etc.) all assume that line has been
55
+ crossed correctly. This skill is the first gate the orchestrator
56
+ routes to. It refuses to declare an engagement authorized until
57
+ a Rules of Engagement (ROE) attestation file exists, is signed,
58
+ and contains the fields any real-world legal review will look for.
59
+
60
+ This is not paranoia or paperwork theater. Engagements DO go
61
+ sideways: scope creeps mid-test, a tester probes an out-of-scope
62
+ adjacent system, an SOC team escalates a "real" attack to legal,
63
+ and the question "show us the ROE" comes up. If the ROE is in
64
+ order, the answer is "here it is." If not, the conversation gets
65
+ expensive fast.
66
+
67
+ ## When the skill produces findings
68
+
69
+ | Finding | Severity | Threshold | Affected control |
70
+ |---|---|---|---|
71
+ | ROE file missing | **CRITICAL** | No attestation file at the expected path | (legal) |
72
+ | Required field missing | **CRITICAL** | authorizer, in_scope_targets, time_window, emergency_contact, or signature absent | (legal) |
73
+ | Signature missing | **CRITICAL** | No signature_block in ROE | (legal) |
74
+ | Signer not in allowlist | **CRITICAL** | signer email/key id not in `.allowed-authorizers` | (legal) |
75
+ | Time window expired | **HIGH** | current time outside `time_window.start` / `time_window.end` | (legal) |
76
+ | Time window not yet active | **HIGH** | current time before `time_window.start` | (legal) |
77
+ | In-scope target list empty | **HIGH** | `in_scope_targets` field present but empty | (legal) |
78
+ | Out-of-scope override (manual flag) | **MEDIUM** | tester requests a target not in the in-scope list | (legal) |
79
+ | Stale ROE (>30 days from sign date) | **MEDIUM** | last_signed_at older than 30 days; suggests refresh | (operational) |
80
+ | ROE present + signed + in window | **INFO** | All gates pass; engagement is authorized | (positive confirmation) |
81
+
82
+ ## Prerequisites
83
+
84
+ - Python 3.9+
85
+ - ROE attestation file at `./roe.yaml` (or pass `--roe FILE`).
86
+ - Optional `.allowed-authorizers` file listing email addresses or
87
+ GPG key fingerprints permitted to sign ROEs.
88
+
89
+ ## ROE attestation file schema
90
+
91
+ ```yaml
92
+ engagement_id: ACME-2026-Q2-PENTEST-001
93
+ authorizer:
94
+ name: Jane Doe
95
+ email: jane.doe@acme.example
96
+ role: CISO
97
+ organization: ACME Corp
98
+ in_scope_targets:
99
+ - host: app.acme.example
100
+ notes: production web app, full-stack pentest authorized
101
+ - host: api.acme.example
102
+ - cidr: 10.50.0.0/16
103
+ notes: internal corporate range
104
+ out_of_scope_targets:
105
+ - host: payments.acme.example
106
+ reason: PCI scope, separate authz required
107
+ - cidr: 10.99.0.0/16
108
+ reason: production database tier, separate authz required
109
+ time_window:
110
+ start: 2026-06-01T00:00:00Z
111
+ end: 2026-06-30T23:59:59Z
112
+ emergency_contact:
113
+ name: SOC On-Call
114
+ phone: "+1-555-555-5555"
115
+ email: soc@acme.example
116
+ rules:
117
+ - No exploitation of confirmed findings without written prompt approval
118
+ - No password cracking against production accounts
119
+ - All testing pauses on declared business-hours blackouts
120
+ signature_block:
121
+ signer: jane.doe@acme.example
122
+ signed_at: 2026-05-30T14:22:00Z
123
+ signature: |
124
+ -----BEGIN PGP SIGNATURE-----
125
+ ...
126
+ -----END PGP SIGNATURE-----
127
+ ```
128
+
129
+ ## Instructions
130
+
131
+ ### Step 1 — Locate the ROE
132
+
133
+ The skill looks for `./roe.yaml` by default. Override with
134
+ `--roe FILE`. The ROE file should live with the engagement
135
+ artifacts, NOT in the repo under test — typical layout is
136
+ `engagements/<client>-<date>/roe.yaml`.
137
+
138
+ ### Step 2 — Run the verification
139
+
140
+ ```bash
141
+ python3 ./scripts/check_authorization.py --roe engagements/acme-2026-q2/roe.yaml
142
+ ```
143
+
144
+ Options:
145
+
146
+ ```
147
+ Usage: check_authorization.py [OPTIONS]
148
+
149
+ Options:
150
+ --roe FILE Path to ROE attestation YAML (default: ./roe.yaml)
151
+ --allowed FILE Path to allowed-authorizers list (default: .allowed-authorizers)
152
+ --check-target HOST Verify HOST is in the in-scope list (repeatable)
153
+ --output FILE Write findings to FILE
154
+ --format FMT json | jsonl | markdown (default: markdown)
155
+ --min-severity SEV Default info
156
+ ```
157
+
158
+ ### Step 3 — Interpret findings
159
+
160
+ CRITICAL = engagement is NOT authorized. Halt all testing
161
+ immediately. Resolve the missing requirement before any further
162
+ skill runs.
163
+
164
+ HIGH = the engagement was authorized at some point but the
165
+ current state is out-of-window. Either extend the time window
166
+ with a new signature or wait.
167
+
168
+ MEDIUM = operational concerns that warrant attention but don't
169
+ block testing. A stale ROE should be refreshed; a manual
170
+ out-of-scope target request needs explicit additional authz.
171
+
172
+ INFO = positive confirmation that the engagement is authorized.
173
+
174
+ ### Step 4 — Save the result
175
+
176
+ The skill's output IS the authorization record. Save the markdown
177
+ report alongside the ROE itself; it becomes part of the engagement
178
+ evidence chain (see `recording-pentest-engagement` for the
179
+ storage pattern).
180
+
181
+ ## Examples
182
+
183
+ ### Example 1 — Pre-flight check before any scan
184
+
185
+ ```bash
186
+ python3 ./scripts/check_authorization.py --roe engagements/acme-2026-q2/roe.yaml --format markdown
187
+ # If exit code != 0, halt testing.
188
+ ```
189
+
190
+ ### Example 2 — Confirm a specific target is in-scope before probing
191
+
192
+ ```bash
193
+ python3 ./scripts/check_authorization.py \
194
+ --roe engagements/acme-2026-q2/roe.yaml \
195
+ --check-target app.acme.example \
196
+ --check-target api.acme.example
197
+ ```
198
+
199
+ The skill returns an explicit INFO Finding per target if all
200
+ checks pass, and a HIGH/CRITICAL Finding per target if any are
201
+ out-of-scope.
202
+
203
+ ### Example 3 — Generate authorization evidence for the audit trail
204
+
205
+ ```bash
206
+ python3 ./scripts/check_authorization.py --roe engagements/acme-2026-q2/roe.yaml \
207
+ --format json \
208
+ --output engagements/acme-2026-q2/evidence/authz-check-$(date +%Y%m%d).json
209
+ ```
210
+
211
+ ## Output
212
+
213
+ JSON / JSONL / Markdown per `lib/report.py`. Exit codes: 0 clean,
214
+ 1 high/critical (engagement NOT authorized), 2 error.
215
+
216
+ Each Finding includes:
217
+
218
+ - `id` — `authz::<field>` or `authz::target::<target>`
219
+ - `severity` — CRITICAL / HIGH / MEDIUM / INFO
220
+ - `category` — `engagement-authorization`
221
+ - `summary` — what's missing or wrong
222
+ - `evidence` — engagement_id, authorizer email, time window, target
223
+
224
+ ## Error Handling
225
+
226
+ - **ROE file not found** → emits a CRITICAL finding and exits 1.
227
+ - **Unparseable YAML** → emits a CRITICAL finding with the parser
228
+ error and exits 2.
229
+ - **Allowed-authorizers file missing** → emits an INFO finding
230
+ (allowlist is recommended but not required) and proceeds with
231
+ field-level verification only.
232
+ - **Signature block present but unparseable** → emits a CRITICAL
233
+ finding flagging the issue; does NOT attempt to verify
234
+ cryptographically (separate `gpg --verify` step recommended for
235
+ signature validation; this skill validates the structural
236
+ presence and signer-identity claim).
237
+
238
+ ## Resources
239
+
240
+ - `references/THEORY.md` — Why pentest authorization is a legal
241
+ primitive (CFAA, CMA, equivalent statutes), ROE structure
242
+ history (OSSTMM / PTES origins), signature options
243
+ (PGP / S/MIME / DocuSign), scope-creep failure modes
244
+ - `references/PLAYBOOK.md` — ROE templates per engagement type
245
+ (external pentest, internal pentest, red team, purple team),
246
+ authorization escalation flow, time-window extension procedures,
247
+ emergency-stop protocol