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