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