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