@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,347 @@
1
+ #!/usr/bin/env python3
2
+ """Server-software fingerprinting via HTTP response signatures.
3
+
4
+ Companion to skill `fingerprinting-server-software`. Sends a baseline
5
+ GET + an OPTIONS + optionally a malformed-request error probe, then
6
+ parses every standard fingerprinting header and Set-Cookie name
7
+ pattern.
8
+
9
+ Each match grades against the threshold table in the skill body:
10
+ explicit-version disclosures are MEDIUM (CWE-200), framework
11
+ identification without version is LOW, stack-trace disclosure from
12
+ error pages is HIGH (CWE-209).
13
+
14
+ References:
15
+ OWASP WSTG-INFO-02 Fingerprint Web Server
16
+ OWASP WSTG-INFO-08 Fingerprint Web Application Framework
17
+ CWE-200 Information Exposure
18
+ CWE-209 Information Exposure Through an Error Message
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ _PLUGIN_ROOT = Path(__file__).resolve().parents[3]
29
+ if str(_PLUGIN_ROOT) not in sys.path:
30
+ sys.path.insert(0, str(_PLUGIN_ROOT))
31
+
32
+ from lib.authz_check import require_authorization # noqa: E402
33
+ from lib.finding import Finding, Severity # noqa: E402
34
+ from lib.http_client import make_session, safe_get, safe_options # noqa: E402
35
+ from lib.report import emit, exit_code # noqa: E402
36
+
37
+ SKILL_ID = "fingerprinting-server-software"
38
+
39
+
40
+ # Headers that commonly leak version
41
+ VERSION_HEADERS = [
42
+ "Server",
43
+ "X-Powered-By",
44
+ "X-AspNet-Version",
45
+ "X-AspNetMvc-Version",
46
+ "X-Runtime",
47
+ "X-Generator",
48
+ "X-Drupal-Cache",
49
+ "X-Drupal-Dynamic-Cache",
50
+ "X-Backend-Server",
51
+ "X-Server-Powered-By",
52
+ "X-Joomla-Version",
53
+ ]
54
+ # Framework identification headers (lower severity — no version)
55
+ FRAMEWORK_HEADERS = [
56
+ "X-Rails-Version",
57
+ "X-Django-Version",
58
+ "X-Frame-Options", # only if present along with other framework signals
59
+ "Via",
60
+ ]
61
+ # Framework-default Set-Cookie names (low — identifies stack)
62
+ DEFAULT_COOKIES = {
63
+ "PHPSESSID": "PHP",
64
+ "JSESSIONID": "Java EE / Tomcat / WebSphere / WebLogic",
65
+ "ASP.NET_SessionId": "ASP.NET",
66
+ "ASPSESSIONID": "Classic ASP",
67
+ "ASPSESSIONID*": "Classic ASP (pattern)",
68
+ "connect.sid": "Express + connect.session",
69
+ "session": "Generic — multiple frameworks",
70
+ "_session_id": "Rails",
71
+ "laravel_session": "Laravel",
72
+ "ci_session": "CodeIgniter",
73
+ "frontend": "Magento",
74
+ "X-Mapping-*": "Sun ONE / iPlanet load-balancer",
75
+ "BIGipServer*": "F5 BIG-IP load-balancer",
76
+ "AWSALB": "AWS ALB",
77
+ "AWSELB": "AWS ELB classic",
78
+ }
79
+ # Error-page stack-trace fingerprints
80
+ STACK_TRACE_SIGS = [
81
+ (r"^[ \t]*at\s+[\w.<>$]+\([\w./\\:?]+:\d+\)", "Java stack trace"),
82
+ (r"File\s+\"[/\\][\w/.\\]+\.py\",\s+line\s+\d+", "Python traceback"),
83
+ (r"in\s+/[^\s]+\.php on line \d+", "PHP error trace"),
84
+ (r"at\s+(?:[\w$]+\.)+[\w$]+\s+\([\w./:\\]+:\d+:\d+\)", "JavaScript stack trace"),
85
+ (r"\(in /[\w/.-]+\.rb:\d+", "Ruby exception"),
86
+ (r"goroutine\s+\d+\s+\[running\]:", "Go panic"),
87
+ (r"System\.[\w.]+Exception:", ".NET exception"),
88
+ (r"<title>[^<]*(stack trace|exception|fatal error)", "Generic error-page banner"),
89
+ ]
90
+
91
+
92
+ def _version_in(value: str) -> bool:
93
+ """Heuristic: header value contains an explicit version number."""
94
+ return bool(re.search(r"\d+\.\d+", value))
95
+
96
+
97
+ def _check_version_headers(headers, target, source_label):
98
+ findings = []
99
+ for h in VERSION_HEADERS:
100
+ v = headers.get(h)
101
+ if not v:
102
+ continue
103
+ has_version = _version_in(v)
104
+ sev = Severity.MEDIUM if has_version else Severity.LOW
105
+ # Specific: X-AspNet-Version always shows runtime version — HIGH
106
+ if h.lower().startswith("x-aspnet") and has_version:
107
+ sev = Severity.HIGH
108
+ findings.append(
109
+ Finding(
110
+ skill_id=SKILL_ID,
111
+ title=f"{h} header discloses {'version' if has_version else 'product'}: {v[:80]}",
112
+ severity=sev,
113
+ target=target,
114
+ detail=(
115
+ f"The {source_label} response includes {h}: {v!r}. "
116
+ "Attackers query published CVE catalogs for the disclosed "
117
+ "version + family to enumerate exploitable conditions "
118
+ "without further probing."
119
+ ),
120
+ remediation=_remediation_for_header(h),
121
+ cwe_id="CWE-200",
122
+ affected_control="OWASP A05:2021",
123
+ references=("https://cwe.mitre.org/data/definitions/200.html",),
124
+ evidence=((f"header:{h}", v),),
125
+ )
126
+ )
127
+ return findings
128
+
129
+
130
+ def _check_framework_headers(headers, target, source_label):
131
+ findings = []
132
+ for h in FRAMEWORK_HEADERS:
133
+ v = headers.get(h)
134
+ if not v:
135
+ continue
136
+ findings.append(
137
+ Finding(
138
+ skill_id=SKILL_ID,
139
+ title=f"{h} header discloses stack identification: {v[:80]}",
140
+ severity=Severity.LOW,
141
+ target=target,
142
+ detail=(
143
+ f"The {source_label} response includes {h}: {v!r}. "
144
+ "Framework identification without version. Combined with "
145
+ "version-bearing headers, informs CVE lookup."
146
+ ),
147
+ remediation=_remediation_for_header(h),
148
+ cwe_id="CWE-200",
149
+ affected_control="OWASP A05:2021",
150
+ evidence=((f"header:{h}", v),),
151
+ )
152
+ )
153
+ return findings
154
+
155
+
156
+ def _check_cookies(headers, target):
157
+ findings = []
158
+ set_cookie = headers.get("Set-Cookie", "")
159
+ if not set_cookie:
160
+ return findings
161
+ # requests joins multiple Set-Cookie headers with comma in .headers; iterate
162
+ # cookie names separately
163
+ for cookie_blob in set_cookie.split(","):
164
+ # cookie name is the first token before '='
165
+ name = cookie_blob.split("=", 1)[0].strip()
166
+ for default_name, framework in DEFAULT_COOKIES.items():
167
+ if default_name.endswith("*"):
168
+ if name.startswith(default_name.rstrip("*")):
169
+ break
170
+ elif name == default_name:
171
+ break
172
+ else:
173
+ continue
174
+ findings.append(
175
+ Finding(
176
+ skill_id=SKILL_ID,
177
+ title=f"Framework-default cookie name discloses stack: {name} ({framework})",
178
+ severity=Severity.LOW,
179
+ target=target,
180
+ detail=(
181
+ f"Set-Cookie name {name!r} matches the conventional "
182
+ f"default for {framework}. Combined with other signals, "
183
+ "informs framework + version inference."
184
+ ),
185
+ remediation=(
186
+ "Rename the session cookie to a non-default value. In "
187
+ "Express: app.use(session({name: 'sid', ...})). In Spring: "
188
+ "server.servlet.session.cookie.name=sid. In Rails: "
189
+ "config.session_store :cookie_store, key: '_sid'."
190
+ ),
191
+ cwe_id="CWE-200",
192
+ affected_control="OWASP A05:2021",
193
+ )
194
+ )
195
+ return findings
196
+
197
+
198
+ def _check_etag(headers, target):
199
+ etag = headers.get("ETag", "")
200
+ if not etag:
201
+ return []
202
+ # Apache hex-ETag format: "<inode>-<size>-<mtime>"
203
+ if re.match(r'^"[0-9a-f]+-[0-9a-f]+-[0-9a-f]+"$', etag):
204
+ return [
205
+ Finding(
206
+ skill_id=SKILL_ID,
207
+ title="ETag format reveals Apache inode-size-mtime triple (cluster-member fingerprint)",
208
+ severity=Severity.LOW,
209
+ target=target,
210
+ detail=(
211
+ f"The ETag {etag!r} matches Apache's default "
212
+ "inode-size-mtime format. Across a load-balanced cluster, "
213
+ "the inode portion distinguishes individual nodes, enabling "
214
+ "node-by-node probing."
215
+ ),
216
+ remediation=(
217
+ "Apache: `FileETag MTime Size` (drop inode). Or `FileETag None` to disable ETags entirely."
218
+ ),
219
+ cwe_id="CWE-200",
220
+ )
221
+ ]
222
+ return []
223
+
224
+
225
+ def _check_error_disclosure(body_text, target):
226
+ findings = []
227
+ sample = (body_text or "")[:8192]
228
+ for pattern, name in STACK_TRACE_SIGS:
229
+ if re.search(pattern, sample, re.MULTILINE | re.IGNORECASE):
230
+ findings.append(
231
+ Finding(
232
+ skill_id=SKILL_ID,
233
+ title=f"Error page leaks {name}",
234
+ severity=Severity.HIGH,
235
+ target=target,
236
+ detail=(
237
+ f"The error response body contains content matching "
238
+ f"the {name} pattern. Server-internal file paths, "
239
+ "function names, and framework details are visible to "
240
+ "any external requestor that triggers an error."
241
+ ),
242
+ remediation=(
243
+ "Disable detailed error pages in production. Per-stack: "
244
+ "Django DEBUG=False, Rails Rails.application.config."
245
+ "consider_all_requests_local=false, Spring "
246
+ "server.error.include-stacktrace=never, ASP.NET "
247
+ "customErrors mode='RemoteOnly', Express "
248
+ "app.use(errorHandler({log:false,debug:false}))."
249
+ ),
250
+ cwe_id="CWE-209",
251
+ affected_control="OWASP A05:2021",
252
+ )
253
+ )
254
+ break # only flag the first stack-trace pattern that matches
255
+ return findings
256
+
257
+
258
+ def _remediation_for_header(header: str) -> str:
259
+ return {
260
+ "Server": (
261
+ "nginx: `server_tokens off;`. Apache: `ServerTokens Prod`. "
262
+ "Caddy: omitted by default (Caddy 2.x). IIS: "
263
+ "URL Rewrite module → outbound rule to strip header. "
264
+ "ALB / CloudFront: response-header policy to drop Server."
265
+ ),
266
+ "X-Powered-By": (
267
+ "PHP: `expose_php = Off` in php.ini. "
268
+ "Express: `app.disable('x-powered-by')` or `helmet({hidePoweredBy:true})`. "
269
+ "ASP.NET: web.config customHeaders → removeHeader X-Powered-By."
270
+ ),
271
+ "X-AspNet-Version": ('web.config: `<httpRuntime enableVersionHeader="false" />`.'),
272
+ "X-AspNetMvc-Version": ("Global.asax: `MvcHandler.DisableMvcResponseHeader = true;`."),
273
+ "X-Runtime": ("Rails: `config.action_dispatch.runtime_response_header = nil`."),
274
+ "X-Generator": (
275
+ "Drupal: settings.php → set $config['system.performance']['cache']['page']['max_age']. "
276
+ "Wordpress: remove_action('wp_head', 'wp_generator')."
277
+ ),
278
+ "X-Drupal-Cache": "Drupal: configure reverse proxy to strip the header before serving.",
279
+ "X-Drupal-Dynamic-Cache": "Drupal: same as above.",
280
+ "X-Joomla-Version": "Joomla: remove from template metadata.",
281
+ "Via": (
282
+ "If the Via header is from your reverse proxy, configure the proxy "
283
+ "to omit. nginx: `proxy_set_header Via '';`. CloudFront: "
284
+ "response-header policy to strip Via."
285
+ ),
286
+ "X-Rails-Version": "Rails: middleware customization to strip header.",
287
+ "X-Django-Version": "Django: middleware to strip header.",
288
+ }.get(header, f"Configure the web stack to omit the {header} header on outbound responses.")
289
+
290
+
291
+ def main(argv: list[str] | None = None) -> int:
292
+ parser = argparse.ArgumentParser(description="Server-software fingerprinter")
293
+ parser.add_argument("url")
294
+ parser.add_argument("--authorized", action="store_true")
295
+ parser.add_argument("--output", default=None)
296
+ parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
297
+ parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
298
+ parser.add_argument("--timeout", type=float, default=10.0)
299
+ parser.add_argument(
300
+ "--trigger-error", action="store_true", help="Send a malformed request to surface error-page disclosure"
301
+ )
302
+ args = parser.parse_args(argv)
303
+
304
+ require_authorization(args.url, args.authorized)
305
+
306
+ sess = make_session(timeout=args.timeout)
307
+ findings: list[Finding] = []
308
+
309
+ # Baseline GET
310
+ resp = safe_get(sess, args.url, timeout=args.timeout, allow_redirects=False)
311
+ if resp is None:
312
+ sys.stderr.write(f"ERROR: target {args.url!r} unreachable\n")
313
+ return 2
314
+ headers = dict(resp.headers)
315
+ findings.extend(_check_version_headers(headers, args.url, "baseline GET"))
316
+ findings.extend(_check_framework_headers(headers, args.url, "baseline GET"))
317
+ findings.extend(_check_cookies(headers, args.url))
318
+ findings.extend(_check_etag(headers, args.url))
319
+
320
+ # OPTIONS — different code path may reveal different headers
321
+ options_resp = safe_options(sess, args.url, timeout=args.timeout)
322
+ if options_resp is not None:
323
+ opt_headers = dict(options_resp.headers)
324
+ # Only flag headers that DIDN'T appear in the GET (dedupe)
325
+ for h in VERSION_HEADERS + FRAMEWORK_HEADERS:
326
+ if h in opt_headers and h not in headers:
327
+ findings.extend(_check_version_headers({h: opt_headers[h]}, args.url, "OPTIONS"))
328
+ findings.extend(_check_framework_headers({h: opt_headers[h]}, args.url, "OPTIONS"))
329
+
330
+ # Optional: error-page disclosure
331
+ if args.trigger_error:
332
+ # Send a request with deliberately-malformed path
333
+ err_url = args.url.rstrip("/") + "/this-route-should-not-exist-" + ("X" * 200)
334
+ err_resp = safe_get(sess, err_url, timeout=args.timeout, allow_redirects=False)
335
+ if err_resp is not None and err_resp.status_code >= 400:
336
+ findings.extend(_check_error_disclosure(err_resp.text, args.url))
337
+
338
+ # Severity floor
339
+ floor = Severity(args.min_severity)
340
+ findings = [f for f in findings if f.severity.numeric >= floor.numeric]
341
+
342
+ emit(findings, args.output, args.format, args.url)
343
+ return exit_code(findings)
344
+
345
+
346
+ if __name__ == "__main__":
347
+ sys.exit(main())
@@ -0,0 +1,261 @@
1
+ ---
2
+ name: generating-executive-summary
3
+ description: |
4
+ Compose an exec-readable summary from a unified findings JSONL
5
+ plus the OWASP coverage report. Computes a single engagement
6
+ risk score (0-100, severity-weighted with OWASP-breadth and
7
+ governance terms), rolls up findings into headline counts, names
8
+ the top-3 remediation priorities with effort + impact estimates,
9
+ and produces a 1-2 page markdown document for a C-level or board
10
+ audience. Elides technical detail; the vulnerability report is
11
+ the deep document.
12
+ Use when: closing an engagement, preparing the exec-readout
13
+ meeting, packaging for board review, or producing a one-page
14
+ narrative for auditor / insurer / board.
15
+ Threshold: input findings missing produces CRITICAL operational
16
+ finding; otherwise the deliverable is the document itself.
17
+ Trigger with: "generate exec summary", "executive summary",
18
+ "C-level readout", "board pentest summary".
19
+ allowed-tools:
20
+ - Read
21
+ - Write
22
+ - Bash(python3:*)
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
+ - reporting
37
+ - executive-summary
38
+ - risk-score
39
+ - pentest
40
+ ---
41
+
42
+ # Generating Executive Summary
43
+
44
+ ## Overview
45
+
46
+ The vulnerability report is comprehensive — every finding, full
47
+ detail, every reference. The C-level reader doesn't open it. They
48
+ ask their security lead "what should I tell the board?" The
49
+ security lead needs a one-page answer.
50
+
51
+ That one-page answer is the executive summary. It states the
52
+ engagement's bottom line:
53
+
54
+ - A single risk score (0-100)
55
+ - Headline counts by severity
56
+ - Top-3 remediation priorities, each with rough effort + impact
57
+ - OWASP Top 10 coverage (where the work landed)
58
+ - Engagement scope and authorization summary (what was tested,
59
+ under what authority, in what window)
60
+ - Next steps the customer's organization should take
61
+
62
+ The summary doesn't omit anything important; it just compresses.
63
+ The vulnerability report remains the deep artifact for anyone who
64
+ needs the technical detail.
65
+
66
+ This skill consumes the enriched findings JSONL (after OWASP
67
+ mapping) + the OWASP coverage report + the ROE, computes the risk
68
+ score, picks the top remediation priorities deterministically,
69
+ and renders the document.
70
+
71
+ ## When the skill produces findings
72
+
73
+ | Finding | Severity | Threshold | Affected control |
74
+ |---|---|---|---|
75
+ | Input findings file missing | **CRITICAL** | Source JSONL doesn't exist | (operational) |
76
+ | OWASP coverage report missing | **HIGH** | Coverage referenced but not present | (operational) |
77
+ | ROE missing | **MEDIUM** | Can still generate summary but lacks scope/authz context | (operational) |
78
+ | Exec summary written cleanly | **INFO** | Confirmation | (informational) |
79
+ | Risk score >75 (high engagement risk) | **HIGH** | Computed risk score elevated | (advisory) |
80
+ | Risk score >90 (critical engagement risk) | **CRITICAL** | Engagement exposed material risk; needs urgent action | (advisory) |
81
+
82
+ ## Risk score (0-100) composition
83
+
84
+ The single risk score is the headline number on the exec summary.
85
+ The composition is deterministic and documented:
86
+
87
+ ```
88
+ risk = clamp(0, 100,
89
+ 20 * count(CRITICAL)
90
+ + 10 * count(HIGH)
91
+ + 3 * count(MEDIUM)
92
+ + 1 * count(LOW)
93
+ + 0 * count(INFO)
94
+ + 5 * (count(distinct OWASP categories touched) - 5 if >5 else 0)
95
+ - 10 * 1 if engagement was authorized cleanly and in-scope (governance bonus)
96
+ )
97
+ ```
98
+
99
+ The first five terms weight by severity. The OWASP-coverage term
100
+ adds 5 points per category beyond 5 (a broader-finding engagement
101
+ implies broader risk surface). The governance bonus is a -10
102
+ adjustment when ROE was clean — explicit recognition that finding
103
+ problems in a well-governed engagement is HEALTHIER than finding
104
+ the same problems in a chaotic engagement.
105
+
106
+ Score interpretation:
107
+
108
+ | Score | Reading |
109
+ |---|---|
110
+ | 0-25 | Low risk: clean engagement OR very narrow scope |
111
+ | 26-50 | Moderate risk: typical engagement with manageable findings |
112
+ | 51-75 | Elevated risk: significant findings, remediation planning required |
113
+ | 76-90 | High risk: material findings; executive attention warranted |
114
+ | 91-100 | Critical risk: urgent remediation required; consider treating as incident |
115
+
116
+ ## Top-3 remediation priorities
117
+
118
+ The skill picks top-3 priorities deterministically by:
119
+
120
+ 1. Severity (CRITICAL > HIGH > MEDIUM > LOW)
121
+ 2. Reachability — findings affecting many targets weight higher
122
+ 3. Tie-breaker: alphabetical by title for stable output
123
+
124
+ Each priority gets:
125
+
126
+ - A one-line headline
127
+ - Estimated effort (Hours / Days / Weeks)
128
+ - Estimated impact (Limited / Significant / Material)
129
+ - Pointer to the corresponding finding section in the
130
+ vulnerability report
131
+
132
+ Effort + impact are heuristic estimates based on the source
133
+ skill's category — operator can override via `--priority-overrides`
134
+ for cases where the heuristic is wrong.
135
+
136
+ ## Prerequisites
137
+
138
+ - Python 3.9+
139
+ - Findings JSONL at `engagement/findings/all-with-owasp.jsonl`
140
+ (output of `mapping-findings-to-owasp-top10`) OR an explicit
141
+ `--source FILE`
142
+ - OWASP coverage report at `engagement/reports/owasp-coverage.md`
143
+ (referenced; optional)
144
+ - ROE at `engagement/roe.yaml` (referenced for scope summary)
145
+
146
+ ## Instructions
147
+
148
+ ### Step 1 — Verify the inputs are present
149
+
150
+ ```bash
151
+ ls engagements/acme-2026-q2/findings/all-with-owasp.jsonl
152
+ ls engagements/acme-2026-q2/reports/owasp-coverage.md
153
+ ls engagements/acme-2026-q2/roe.yaml
154
+ ```
155
+
156
+ All three should exist for a complete summary. The skill works
157
+ without the coverage report or ROE but the summary is less
158
+ complete.
159
+
160
+ ### Step 2 — Generate the summary
161
+
162
+ ```bash
163
+ python3 ./scripts/exec_summary.py engagements/acme-2026-q2/
164
+ ```
165
+
166
+ Options:
167
+
168
+ ```
169
+ Usage: exec_summary.py PATH [OPTIONS]
170
+
171
+ Options:
172
+ --source FILE Findings JSONL (default: PATH/findings/all-with-owasp.jsonl)
173
+ --coverage FILE OWASP coverage report (default: PATH/reports/owasp-coverage.md)
174
+ --roe FILE ROE (default: PATH/roe.yaml)
175
+ --summary-output FILE Output path (default: PATH/reports/executive-summary.md)
176
+ --output FILE Operational findings output
177
+ --format FMT json | jsonl | markdown (default: markdown)
178
+ --min-severity SEV default info
179
+ --priority-overrides FILE YAML overriding the top-3 priorities
180
+ ```
181
+
182
+ ### Step 3 — Review the risk score
183
+
184
+ If the score is in 76-100 range, the operator should sanity-check
185
+ before delivering: did the underlying findings actually warrant
186
+ the elevated reading, or did a few INFO-tagged findings get
187
+ mis-categorized as HIGH?
188
+
189
+ ### Step 4 — Hand off
190
+
191
+ The exec summary is intended as a standalone artifact. Deliver to
192
+ the customer's exec readout meeting, along with the full
193
+ vulnerability report.
194
+
195
+ ## Examples
196
+
197
+ ### Example 1 — End-of-engagement summary
198
+
199
+ ```bash
200
+ python3 ./scripts/exec_summary.py engagements/acme-2026-q2/
201
+ ```
202
+
203
+ ### Example 2 — Board-ready summary (force-includes governance section)
204
+
205
+ ```bash
206
+ python3 ./scripts/exec_summary.py engagements/acme-2026-q2/ \
207
+ --summary-output engagements/acme-2026-q2/reports/board-summary.md
208
+ ```
209
+
210
+ ### Example 3 — Override priorities
211
+
212
+ ```yaml
213
+ # priorities-override.yaml
214
+ - title: "Hardcoded AWS access key in source"
215
+ effort: Hours
216
+ impact: Material
217
+ rationale: This is the single highest-priority remediation regardless of count.
218
+ ```
219
+
220
+ ```bash
221
+ python3 ./scripts/exec_summary.py engagements/acme-2026-q2/ \
222
+ --priority-overrides priorities-override.yaml
223
+ ```
224
+
225
+ ## Output
226
+
227
+ JSON / JSONL / Markdown per `lib/report.py` for operational
228
+ findings. PRIMARY output: the executive-summary markdown
229
+ document.
230
+
231
+ Operational Finding includes:
232
+
233
+ - `id` — `exec::<issue>`
234
+ - `severity` — varies
235
+ - `category` — `executive-summary`
236
+ - `summary` — what was generated
237
+ - `evidence` — risk score, finding count, top priorities, output path
238
+
239
+ ## Error Handling
240
+
241
+ - **No findings source** → CRITICAL operational finding, exits 1.
242
+ - **Source JSONL unparseable** → HIGH, exits 1.
243
+ - **No findings at all** → emits LOW operational finding noting
244
+ the empty engagement; the document is generated but says so.
245
+ - **Coverage report missing** → MEDIUM, document is generated
246
+ without the coverage-narrative section.
247
+ - **ROE missing** → MEDIUM, document is generated without the
248
+ scope/authorization section.
249
+
250
+ ## Resources
251
+
252
+ - `references/THEORY.md` — Executive-summary writing as a
253
+ technical-communication discipline, single-number risk
254
+ scoring tradeoffs, why deterministic priority selection beats
255
+ human-curated for reproducibility, how the score interpretation
256
+ bands were chosen, comparison with CVSS / DREAD / STRIDE risk
257
+ models
258
+ - `references/PLAYBOOK.md` — Per-audience customizations (board,
259
+ C-suite, security leadership, customer auditor), summary length
260
+ guidelines, common rewrite patterns, integration with the
261
+ composing + mapping skills, post-delivery follow-up cadence