@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,263 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HTTP method probe — flags methods that shouldn't be enabled.
|
|
3
|
+
|
|
4
|
+
Companion to skill `probing-dangerous-http-methods`. Sends each
|
|
5
|
+
canonical "dangerous" method against the target and grades the response.
|
|
6
|
+
|
|
7
|
+
Methods probed:
|
|
8
|
+
TRACE — XST attack vector (RFC 7231 §4.3.8)
|
|
9
|
+
PUT — unrestricted upload (CWE-434)
|
|
10
|
+
DELETE — unauthorized resource removal
|
|
11
|
+
CONNECT — proxy abuse (CWE-441)
|
|
12
|
+
DEBUG — legacy IIS/dev-server diagnostic
|
|
13
|
+
PROPFIND — WebDAV directory listing (RFC 4918)
|
|
14
|
+
MKCOL — WebDAV directory creation
|
|
15
|
+
COPY — WebDAV file copy
|
|
16
|
+
MOVE — WebDAV file move
|
|
17
|
+
OPTIONS — enumerate Allow header (informational, can disclose)
|
|
18
|
+
|
|
19
|
+
References:
|
|
20
|
+
RFC 7231 §4.3 — HTTP method semantics
|
|
21
|
+
RFC 4918 — WebDAV
|
|
22
|
+
OWASP WSTG-CONF-06 — Test HTTP Methods
|
|
23
|
+
CWE-441 Unintended Proxy or Intermediary
|
|
24
|
+
CWE-538 File and Directory Information Exposure
|
|
25
|
+
CWE-693 Protection Mechanism Failure (XST)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
35
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
36
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
37
|
+
|
|
38
|
+
from lib.authz_check import require_authorization # noqa: E402
|
|
39
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
40
|
+
from lib.http_client import make_session # noqa: E402
|
|
41
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
42
|
+
|
|
43
|
+
SKILL_ID = "probing-dangerous-http-methods"
|
|
44
|
+
|
|
45
|
+
# Probe set — (method, expected_failure_codes, severity_if_succeeds, finding_template)
|
|
46
|
+
DANGEROUS_METHODS = [
|
|
47
|
+
("TRACE", {405, 403, 404, 400, 501}, Severity.HIGH, "TRACE method enabled (XST attack vector)"),
|
|
48
|
+
("CONNECT", {405, 403, 400, 501, 502}, Severity.CRITICAL, "CONNECT method enabled (proxy abuse)"),
|
|
49
|
+
("DEBUG", {405, 403, 404, 501}, Severity.HIGH, "DEBUG method enabled (legacy diagnostic)"),
|
|
50
|
+
("PROPFIND", {405, 403, 404, 501}, Severity.HIGH, "WebDAV PROPFIND enabled"),
|
|
51
|
+
("MKCOL", {405, 403, 404, 501}, Severity.HIGH, "WebDAV MKCOL enabled"),
|
|
52
|
+
("COPY", {405, 403, 404, 501}, Severity.HIGH, "WebDAV COPY enabled"),
|
|
53
|
+
("MOVE", {405, 403, 404, 501}, Severity.HIGH, "WebDAV MOVE enabled"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# These are only "dangerous" outside API endpoints
|
|
57
|
+
API_DEPENDENT_METHODS = [
|
|
58
|
+
("PUT", {405, 403, 404, 401}, Severity.HIGH, "PUT method enabled outside API path"),
|
|
59
|
+
("DELETE", {405, 403, 404, 401}, Severity.HIGH, "DELETE method enabled outside API path"),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _probe_method(sess, method: str, url: str, timeout: float):
|
|
64
|
+
try:
|
|
65
|
+
# Use sess.request to handle non-standard methods (PROPFIND, MKCOL, etc.)
|
|
66
|
+
resp = sess.request(method, url, timeout=timeout, allow_redirects=False)
|
|
67
|
+
return resp
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _grade_response(
|
|
73
|
+
method: str, resp, expected_fail: set, severity: Severity, title: str, target: str, is_xst: bool = False
|
|
74
|
+
) -> list[Finding]:
|
|
75
|
+
if resp is None:
|
|
76
|
+
return []
|
|
77
|
+
if resp.status_code in expected_fail:
|
|
78
|
+
return [] # Method correctly blocked
|
|
79
|
+
if resp.status_code >= 500:
|
|
80
|
+
return [
|
|
81
|
+
Finding(
|
|
82
|
+
skill_id=SKILL_ID,
|
|
83
|
+
title=f"{method} returns {resp.status_code} (error handling concern)",
|
|
84
|
+
severity=Severity.INFO,
|
|
85
|
+
target=target,
|
|
86
|
+
detail=(
|
|
87
|
+
f"The {method} method returned {resp.status_code}. While "
|
|
88
|
+
"blocking is the intended behavior, a 500 suggests the server "
|
|
89
|
+
"tried to handle the method and crashed — better to return "
|
|
90
|
+
"405 cleanly."
|
|
91
|
+
),
|
|
92
|
+
remediation=f"Configure the server to return 405 Method Not Allowed for {method}.",
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
# Status 2xx, 3xx, or 405-like — the method was handled successfully or
|
|
96
|
+
# at least not cleanly rejected. This is the finding.
|
|
97
|
+
detail = (
|
|
98
|
+
f"The {method} method returned status {resp.status_code}. "
|
|
99
|
+
f"This method should be rejected with 405 Method Not Allowed."
|
|
100
|
+
)
|
|
101
|
+
if is_xst:
|
|
102
|
+
# Check if the response body echoes the request — confirms XST
|
|
103
|
+
if "TRACE / HTTP" in (resp.text or "") or method.encode() in (resp.content or b""):
|
|
104
|
+
detail += (
|
|
105
|
+
" Response body echoes the request, confirming XST viability. "
|
|
106
|
+
"An attacker who can execute JavaScript on the origin can "
|
|
107
|
+
"use TRACE-via-XHR to read HttpOnly cookies."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
Finding(
|
|
112
|
+
skill_id=SKILL_ID,
|
|
113
|
+
title=title,
|
|
114
|
+
severity=severity,
|
|
115
|
+
target=target,
|
|
116
|
+
detail=detail,
|
|
117
|
+
remediation=_remediation_for(method),
|
|
118
|
+
cwe_id=_cwe_for(method),
|
|
119
|
+
owasp_category="A05:2021",
|
|
120
|
+
evidence=(("status_code", resp.status_code), ("response_len", len(resp.content or b""))),
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _remediation_for(method: str) -> str:
|
|
126
|
+
base = {
|
|
127
|
+
"TRACE": (
|
|
128
|
+
"Disable TRACE explicitly. nginx: `if ($request_method = TRACE) "
|
|
129
|
+
"{ return 405; }`. Apache: `TraceEnable Off`. "
|
|
130
|
+
"Load balancers (ALB/Cloudflare): block at the LB level."
|
|
131
|
+
),
|
|
132
|
+
"CONNECT": (
|
|
133
|
+
"Disable CONNECT. nginx and Apache reject by default; if you see "
|
|
134
|
+
"this enabled, you have a misconfigured proxy. Audit your reverse "
|
|
135
|
+
"proxy rules for `proxy_method` or `ProxyRequests On`."
|
|
136
|
+
),
|
|
137
|
+
"DEBUG": (
|
|
138
|
+
"Legacy IIS / dev-server method. Disable in production. "
|
|
139
|
+
"IIS: remove DEBUG verb from handler mappings. Express dev "
|
|
140
|
+
"middleware: ensure NODE_ENV=production."
|
|
141
|
+
),
|
|
142
|
+
"PUT": (
|
|
143
|
+
"If this endpoint shouldn't accept PUT, return 405. nginx: "
|
|
144
|
+
"`limit_except GET POST { deny all; }`. Express: ensure no "
|
|
145
|
+
"PUT route handler is registered."
|
|
146
|
+
),
|
|
147
|
+
"DELETE": (
|
|
148
|
+
"If this endpoint shouldn't accept DELETE, return 405. Same "
|
|
149
|
+
"pattern as PUT above. If DELETE should be available, ensure "
|
|
150
|
+
"authentication + authorization are wired."
|
|
151
|
+
),
|
|
152
|
+
"PROPFIND": "Disable WebDAV. nginx: `dav_methods off;`. Apache: `<Limit PROPFIND>Require all denied</Limit>`.",
|
|
153
|
+
"MKCOL": "Disable WebDAV — see PROPFIND remediation.",
|
|
154
|
+
"COPY": "Disable WebDAV — see PROPFIND remediation.",
|
|
155
|
+
"MOVE": "Disable WebDAV — see PROPFIND remediation.",
|
|
156
|
+
}
|
|
157
|
+
return base.get(method, f"Disable {method} method at the server level.")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _cwe_for(method: str) -> str:
|
|
161
|
+
mapping = {
|
|
162
|
+
"TRACE": "CWE-693",
|
|
163
|
+
"CONNECT": "CWE-441",
|
|
164
|
+
"DEBUG": "CWE-489",
|
|
165
|
+
"PUT": "CWE-434",
|
|
166
|
+
"DELETE": "CWE-285",
|
|
167
|
+
"PROPFIND": "CWE-538",
|
|
168
|
+
"MKCOL": "CWE-538",
|
|
169
|
+
"COPY": "CWE-538",
|
|
170
|
+
"MOVE": "CWE-538",
|
|
171
|
+
}
|
|
172
|
+
return mapping.get(method, "CWE-200")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _check_options(sess, url: str, timeout: float, target: str) -> list[Finding]:
|
|
176
|
+
try:
|
|
177
|
+
resp = sess.options(url, timeout=timeout, allow_redirects=False)
|
|
178
|
+
except Exception:
|
|
179
|
+
return []
|
|
180
|
+
if resp is None:
|
|
181
|
+
return []
|
|
182
|
+
allow = resp.headers.get("Allow", "")
|
|
183
|
+
if not allow:
|
|
184
|
+
return []
|
|
185
|
+
if "*" in allow:
|
|
186
|
+
return [
|
|
187
|
+
Finding(
|
|
188
|
+
skill_id=SKILL_ID,
|
|
189
|
+
title="OPTIONS Allow header is wildcard",
|
|
190
|
+
severity=Severity.LOW,
|
|
191
|
+
target=target,
|
|
192
|
+
detail="Server advertises Allow:* — information disclosure.",
|
|
193
|
+
remediation="Configure server to return explicit allowed-method list.",
|
|
194
|
+
cwe_id="CWE-200",
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
methods = [m.strip().upper() for m in allow.split(",")]
|
|
198
|
+
unused = [m for m in methods if m in {"DEBUG", "TRACE", "PROPFIND", "MKCOL", "COPY", "MOVE", "CONNECT"}]
|
|
199
|
+
if unused:
|
|
200
|
+
return [
|
|
201
|
+
Finding(
|
|
202
|
+
skill_id=SKILL_ID,
|
|
203
|
+
title=f"OPTIONS Allow header discloses unused methods: {', '.join(unused)}",
|
|
204
|
+
severity=Severity.LOW,
|
|
205
|
+
target=target,
|
|
206
|
+
detail=(
|
|
207
|
+
"The Allow header lists methods that are typically not used "
|
|
208
|
+
"in a modern web app. Either disable them or remove from "
|
|
209
|
+
"the Allow advertisement."
|
|
210
|
+
),
|
|
211
|
+
remediation="See findings on individual methods for remediation steps.",
|
|
212
|
+
cwe_id="CWE-200",
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(argv: list[str] | None = None) -> int:
|
|
219
|
+
parser = argparse.ArgumentParser(description="HTTP method probe")
|
|
220
|
+
parser.add_argument("url")
|
|
221
|
+
parser.add_argument("--authorized", action="store_true")
|
|
222
|
+
parser.add_argument("--output", default=None)
|
|
223
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
224
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
225
|
+
parser.add_argument("--timeout", type=float, default=10.0)
|
|
226
|
+
parser.add_argument("--is-api", action="store_true", help="Target is an API endpoint (PUT/DELETE are expected)")
|
|
227
|
+
args = parser.parse_args(argv)
|
|
228
|
+
|
|
229
|
+
require_authorization(args.url, args.authorized)
|
|
230
|
+
|
|
231
|
+
sess = make_session(timeout=args.timeout)
|
|
232
|
+
target = args.url
|
|
233
|
+
findings: list[Finding] = []
|
|
234
|
+
|
|
235
|
+
method_set = list(DANGEROUS_METHODS)
|
|
236
|
+
if not args.is_api:
|
|
237
|
+
method_set.extend(API_DEPENDENT_METHODS)
|
|
238
|
+
|
|
239
|
+
for method, expected_fail, sev, title in method_set:
|
|
240
|
+
resp = _probe_method(sess, method, args.url, args.timeout)
|
|
241
|
+
findings.extend(
|
|
242
|
+
_grade_response(
|
|
243
|
+
method,
|
|
244
|
+
resp,
|
|
245
|
+
expected_fail,
|
|
246
|
+
sev,
|
|
247
|
+
title,
|
|
248
|
+
target,
|
|
249
|
+
is_xst=(method == "TRACE"),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
findings.extend(_check_options(sess, args.url, args.timeout, target))
|
|
254
|
+
|
|
255
|
+
floor = Severity(args.min_severity)
|
|
256
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
257
|
+
|
|
258
|
+
emit(findings, args.output, args.format, target)
|
|
259
|
+
return exit_code(findings)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
sys.exit(main())
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: recording-pentest-engagement
|
|
3
|
+
description: |
|
|
4
|
+
Package an engagement's findings, scan outputs, evidence, and
|
|
5
|
+
signed ROE into a timestamped archive with a SHA-256 manifest
|
|
6
|
+
covering every file. Establishes chain of custody so legal
|
|
7
|
+
counsel, internal audit, or an outside SOC can verify the archive
|
|
8
|
+
hasn't been modified after closeout. Optionally signs the
|
|
9
|
+
manifest with GPG for cryptographic attestation.
|
|
10
|
+
Use when: closing an engagement, snapshotting evidence after
|
|
11
|
+
each scan day, before handing artifacts to customer, or after
|
|
12
|
+
an emergency-stop event.
|
|
13
|
+
Threshold: file in tree without a manifest entry, hash mismatch,
|
|
14
|
+
out-of-tree path referenced in findings, unsigned manifest when
|
|
15
|
+
signing was requested.
|
|
16
|
+
Trigger with: "record engagement", "archive evidence", "create
|
|
17
|
+
chain of custody", "package pentest artifacts".
|
|
18
|
+
allowed-tools:
|
|
19
|
+
- Read
|
|
20
|
+
- Bash(python3:*)
|
|
21
|
+
- Bash(tar:*)
|
|
22
|
+
- Bash(gpg:*)
|
|
23
|
+
- Glob
|
|
24
|
+
disallowed-tools:
|
|
25
|
+
- Bash(rm:*)
|
|
26
|
+
- Bash(curl:*)
|
|
27
|
+
- Bash(wget:*)
|
|
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
|
+
- evidence
|
|
38
|
+
- chain-of-custody
|
|
39
|
+
- pentest
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# Recording Pentest Engagement
|
|
43
|
+
|
|
44
|
+
## Overview
|
|
45
|
+
|
|
46
|
+
A penetration test produces a lot of artifacts: scan outputs in
|
|
47
|
+
multiple formats, screenshots showing the state of vulnerable
|
|
48
|
+
pages, raw tool logs (nmap, Burp, custom scripts), the ROE and any
|
|
49
|
+
amendments, exec-summary docs, and the findings themselves. Six
|
|
50
|
+
months after the engagement closes, a question arises — sometimes
|
|
51
|
+
benign ("can you remind me what we found?"), sometimes adversarial
|
|
52
|
+
("the customer claims we accessed an out-of-scope system; show us
|
|
53
|
+
the logs"). The answer needs to be: "yes, here's the archive, and
|
|
54
|
+
here's cryptographic proof it hasn't been touched since the
|
|
55
|
+
engagement closed."
|
|
56
|
+
|
|
57
|
+
This skill packages the engagement directory into a single
|
|
58
|
+
timestamped archive with a SHA-256 manifest covering every file.
|
|
59
|
+
Optionally signs the manifest with GPG. The result is a portable,
|
|
60
|
+
self-verifying record of what the engagement produced.
|
|
61
|
+
|
|
62
|
+
The skill also surfaces inconsistencies that would weaken the
|
|
63
|
+
chain-of-custody claim: out-of-tree paths referenced in findings
|
|
64
|
+
(meaning the finding refers to a file not actually in the archive),
|
|
65
|
+
files in the directory not listed in the manifest, manifest
|
|
66
|
+
entries whose hashes don't match. These are HIGH findings — they
|
|
67
|
+
mean the archive is incomplete or has been modified after the
|
|
68
|
+
fact, neither of which is acceptable for evidence purposes.
|
|
69
|
+
|
|
70
|
+
## When the skill produces findings
|
|
71
|
+
|
|
72
|
+
| Finding | Severity | Threshold | Affected control |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| File in tree not in manifest | **HIGH** | Found during walk; manifest entry missing | (evidence integrity) |
|
|
75
|
+
| Manifest entry hash mismatch | **CRITICAL** | Computed SHA-256 differs from manifest | (evidence integrity) |
|
|
76
|
+
| Manifest entry without file | **HIGH** | Manifest lists a path that doesn't exist | (evidence integrity) |
|
|
77
|
+
| Findings reference out-of-tree path | **MEDIUM** | A finding's `evidence` field points to a file not in the archive | (evidence completeness) |
|
|
78
|
+
| Symlink in tree | **MEDIUM** | Symlinks break archive portability and integrity | (evidence integrity) |
|
|
79
|
+
| Empty file in tree | **INFO** | 0-byte file; possibly an export error | (operational) |
|
|
80
|
+
| Archive package complete | **INFO** | All checks pass | (positive confirmation) |
|
|
81
|
+
| Manifest signed | **INFO** | GPG signature present and valid form | (positive confirmation) |
|
|
82
|
+
|
|
83
|
+
## Prerequisites
|
|
84
|
+
|
|
85
|
+
- Python 3.9+
|
|
86
|
+
- An engagement directory laid out as recommended (see structure
|
|
87
|
+
below)
|
|
88
|
+
- Optional GPG installed for manifest signing
|
|
89
|
+
|
|
90
|
+
## Recommended engagement directory structure
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
engagements/acme-2026-q2/
|
|
94
|
+
├── roe.yaml
|
|
95
|
+
├── roe.amendments/
|
|
96
|
+
│ └── amendment-001-20260615.yaml
|
|
97
|
+
├── scope/
|
|
98
|
+
│ ├── allowed-ips.txt
|
|
99
|
+
│ └── normalized-targets.json
|
|
100
|
+
├── findings/
|
|
101
|
+
│ ├── cluster1-tls-2026-06-05.json
|
|
102
|
+
│ ├── cluster1-headers-2026-06-05.json
|
|
103
|
+
│ ├── cluster3-secrets-2026-06-07.json
|
|
104
|
+
│ └── ...
|
|
105
|
+
├── evidence/
|
|
106
|
+
│ ├── screenshots/
|
|
107
|
+
│ ├── tool-logs/
|
|
108
|
+
│ └── raw-scan-output/
|
|
109
|
+
├── reports/
|
|
110
|
+
│ ├── vulnerability-report.md
|
|
111
|
+
│ ├── owasp-mapping.json
|
|
112
|
+
│ └── executive-summary.md
|
|
113
|
+
└── manifest.sha256 # produced by this skill
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Instructions
|
|
117
|
+
|
|
118
|
+
### Step 1 — Identify the engagement directory
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 ./scripts/record_engagement.py engagements/acme-2026-q2/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The skill walks the directory recursively, builds a SHA-256
|
|
125
|
+
manifest, and produces a chain-of-custody report.
|
|
126
|
+
|
|
127
|
+
### Step 2 — Run the packager
|
|
128
|
+
|
|
129
|
+
Options:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Usage: record_engagement.py PATH [OPTIONS]
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
--output FILE Findings output
|
|
136
|
+
--format FMT json | jsonl | markdown (default: markdown)
|
|
137
|
+
--min-severity SEV default info
|
|
138
|
+
--manifest FILE Manifest output path (default: PATH/manifest.sha256)
|
|
139
|
+
--tar FILE Also create a .tar.gz archive at this path
|
|
140
|
+
--sign Sign the manifest with GPG (uses default identity)
|
|
141
|
+
--signer KEY GPG key ID to sign with
|
|
142
|
+
--exclude GLOB Skip files matching glob (repeatable)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Step 3 — Verify the manifest
|
|
146
|
+
|
|
147
|
+
The skill emits a SHA-256 manifest in standard `sha256sum` format
|
|
148
|
+
(one line per file: hash + two-space + path). The manifest is
|
|
149
|
+
verifiable independent of the skill:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sha256sum -c engagements/acme-2026-q2/manifest.sha256
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Anyone with access to the archive can run this check; if the
|
|
156
|
+
output is "all OK," the archive is intact.
|
|
157
|
+
|
|
158
|
+
### Step 4 — Sign the manifest (optional)
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python3 ./scripts/record_engagement.py engagements/acme-2026-q2/ --sign
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The skill shells out to `gpg --detach-sign` against the manifest,
|
|
165
|
+
producing `manifest.sha256.asc`. Later verification:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gpg --verify manifest.sha256.asc manifest.sha256
|
|
169
|
+
sha256sum -c manifest.sha256
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Both checks together: the manifest's contents match the archive,
|
|
173
|
+
AND the manifest itself is signed by an identifiable party.
|
|
174
|
+
|
|
175
|
+
### Step 5 — Create the portable archive (optional)
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python3 ./scripts/record_engagement.py engagements/acme-2026-q2/ \
|
|
179
|
+
--tar engagements/archives/acme-2026-q2.tar.gz
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The tarball contains the engagement directory + manifest +
|
|
183
|
+
signature. Hand the tarball to legal / archive / customer.
|
|
184
|
+
|
|
185
|
+
## Examples
|
|
186
|
+
|
|
187
|
+
### Example 1 — End-of-engagement closeout
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
python3 ./scripts/record_engagement.py engagements/acme-2026-q2/ \
|
|
191
|
+
--sign \
|
|
192
|
+
--tar engagements/archives/acme-2026-q2.tar.gz \
|
|
193
|
+
--output engagements/acme-2026-q2/chain-of-custody.md
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Example 2 — Daily snapshot during a long engagement
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
DATE=$(date +%Y%m%d)
|
|
200
|
+
python3 ./scripts/record_engagement.py engagements/acme-2026-q2/ \
|
|
201
|
+
--manifest engagements/acme-2026-q2/manifest-$DATE.sha256 \
|
|
202
|
+
--output engagements/acme-2026-q2/snapshot-$DATE.md
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Daily snapshots build a time-series of the engagement's state
|
|
206
|
+
which can be useful in incident-investigation contexts.
|
|
207
|
+
|
|
208
|
+
### Example 3 — Pre-handoff integrity check
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Customer claims the archive is incomplete; verify before disputing
|
|
212
|
+
python3 ./scripts/record_engagement.py engagements/acme-2026-q2/ --min-severity high
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
If exit code is 0, the archive is internally consistent.
|
|
216
|
+
|
|
217
|
+
## Output
|
|
218
|
+
|
|
219
|
+
JSON / JSONL / Markdown per `lib/report.py`. Exit codes: 0 clean,
|
|
220
|
+
1 high/critical, 2 error.
|
|
221
|
+
|
|
222
|
+
Each Finding includes:
|
|
223
|
+
|
|
224
|
+
- `id` — `evidence::<issue>::<path>`
|
|
225
|
+
- `severity` — CRITICAL / HIGH / MEDIUM / INFO
|
|
226
|
+
- `category` — `evidence-chain`
|
|
227
|
+
- `summary` — what's wrong with the artifact
|
|
228
|
+
- `evidence` — file path, expected hash, observed hash, manifest entry
|
|
229
|
+
|
|
230
|
+
## Error Handling
|
|
231
|
+
|
|
232
|
+
- **Path doesn't exist** → exits 2 with operational error.
|
|
233
|
+
- **Permission denied reading a file** → emits HIGH finding,
|
|
234
|
+
continues walking other files.
|
|
235
|
+
- **GPG not installed** with `--sign` requested → emits HIGH
|
|
236
|
+
finding, manifest written unsigned, exits 1.
|
|
237
|
+
- **GPG signing fails** (no default identity, etc.) → emits HIGH
|
|
238
|
+
finding, manifest written unsigned, exits 1.
|
|
239
|
+
- **tar command fails** with `--tar` requested → manifest still
|
|
240
|
+
written; archive creation failure surfaces as HIGH finding.
|
|
241
|
+
|
|
242
|
+
## Resources
|
|
243
|
+
|
|
244
|
+
- `references/THEORY.md` — Chain of custody as a legal concept,
|
|
245
|
+
evidence-integrity standards (NIST SP 800-86), SHA-256 vs
|
|
246
|
+
SHA-3 vs SHA-512 tradeoffs, GPG detached-signature semantics,
|
|
247
|
+
archive format choices (tar vs zip vs WORM), retention horizons
|
|
248
|
+
per jurisdiction
|
|
249
|
+
- `references/PLAYBOOK.md` — Engagement directory templates per
|
|
250
|
+
engagement type, daily-snapshot cron pattern, customer-handoff
|
|
251
|
+
protocol, dispute-resolution playbook (when the customer
|
|
252
|
+
challenges the archive), long-term storage and access-control
|
|
253
|
+
patterns
|