@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,538 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""generating-executive-summary — render exec-readable engagement summary.
|
|
3
|
+
|
|
4
|
+
Reads the unified findings JSONL (post OWASP enrichment), the OWASP coverage
|
|
5
|
+
report, and the ROE; computes a single risk score; selects top-3 remediation
|
|
6
|
+
priorities deterministically; and writes a markdown executive summary intended
|
|
7
|
+
for a C-level / board audience.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 exec_summary.py PATH [--source FILE] [--coverage FILE] [--roe FILE]
|
|
11
|
+
[--summary-output FILE]
|
|
12
|
+
[--priority-overrides FILE]
|
|
13
|
+
[--output FILE] [--format json|jsonl|markdown]
|
|
14
|
+
[--min-severity sev]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from collections import Counter, defaultdict
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
# --- lib/ import -------------------------------------------------------------
|
|
28
|
+
_LIB_ROOT = Path(__file__).resolve().parents[3]
|
|
29
|
+
sys.path.insert(0, str(_LIB_ROOT))
|
|
30
|
+
|
|
31
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
32
|
+
from lib import report # noqa: E402
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import yaml # type: ignore[import-not-found]
|
|
36
|
+
|
|
37
|
+
_HAS_PYYAML = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
yaml = None
|
|
40
|
+
_HAS_PYYAML = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
SKILL_ID = "generating-executive-summary"
|
|
44
|
+
CATEGORY = "executive-summary"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# --- Helpers ----------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _f(
|
|
51
|
+
severity: Severity,
|
|
52
|
+
title: str,
|
|
53
|
+
target: str,
|
|
54
|
+
detail: str,
|
|
55
|
+
remediation: str,
|
|
56
|
+
evidence: tuple[tuple[str, Any], ...] = (),
|
|
57
|
+
) -> Finding:
|
|
58
|
+
return Finding(
|
|
59
|
+
skill_id=SKILL_ID,
|
|
60
|
+
title=title,
|
|
61
|
+
severity=severity,
|
|
62
|
+
target=target,
|
|
63
|
+
detail=detail,
|
|
64
|
+
remediation=remediation,
|
|
65
|
+
evidence=evidence,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --- Source loading ---------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_findings(path: Path) -> tuple[list[dict[str, Any]], str | None]:
|
|
73
|
+
if not path.exists():
|
|
74
|
+
return [], f"file missing: {path}"
|
|
75
|
+
try:
|
|
76
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
77
|
+
except OSError as e:
|
|
78
|
+
return [], f"read error: {e}"
|
|
79
|
+
if not text:
|
|
80
|
+
return [], None
|
|
81
|
+
out: list[dict[str, Any]] = []
|
|
82
|
+
if path.suffix == ".jsonl":
|
|
83
|
+
for line in text.splitlines():
|
|
84
|
+
line = line.strip()
|
|
85
|
+
if not line:
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
out.append(json.loads(line))
|
|
89
|
+
except json.JSONDecodeError as e:
|
|
90
|
+
return out, f"jsonl line parse error: {e}"
|
|
91
|
+
return out, None
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(text)
|
|
94
|
+
except json.JSONDecodeError as e:
|
|
95
|
+
return [], f"json parse error: {e}"
|
|
96
|
+
if isinstance(data, list):
|
|
97
|
+
return [r for r in data if isinstance(r, dict)], None
|
|
98
|
+
if isinstance(data, dict) and "findings" in data:
|
|
99
|
+
return [r for r in data["findings"] if isinstance(r, dict)], None
|
|
100
|
+
return [], None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_roe(path: Path) -> dict[str, Any]:
|
|
104
|
+
if not path.exists():
|
|
105
|
+
return {}
|
|
106
|
+
text = path.read_text(encoding="utf-8")
|
|
107
|
+
if _HAS_PYYAML:
|
|
108
|
+
return yaml.safe_load(text) or {}
|
|
109
|
+
# Minimal extract of key fields without full YAML parse
|
|
110
|
+
out: dict[str, Any] = {}
|
|
111
|
+
for line in text.splitlines():
|
|
112
|
+
line = line.rstrip()
|
|
113
|
+
if line.startswith("engagement_id:"):
|
|
114
|
+
out["engagement_id"] = line.split(":", 1)[1].strip().strip('"').strip("'")
|
|
115
|
+
elif line.startswith(" name:") and "authorizer" not in out:
|
|
116
|
+
out.setdefault("authorizer", {})["name"] = line.split(":", 1)[1].strip().strip('"').strip("'")
|
|
117
|
+
elif line.startswith("authorizer:"):
|
|
118
|
+
out["_in_authorizer"] = True
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- Risk score -------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def severity_counts(findings: list[dict[str, Any]]) -> Counter:
|
|
126
|
+
return Counter(r.get("severity", "info") for r in findings)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def compute_risk_score(findings: list[dict[str, Any]], roe_clean: bool) -> int:
|
|
130
|
+
counts = severity_counts(findings)
|
|
131
|
+
score = (
|
|
132
|
+
20 * counts.get("critical", 0)
|
|
133
|
+
+ 10 * counts.get("high", 0)
|
|
134
|
+
+ 3 * counts.get("medium", 0)
|
|
135
|
+
+ 1 * counts.get("low", 0)
|
|
136
|
+
)
|
|
137
|
+
# OWASP-coverage breadth term
|
|
138
|
+
categories = {r.get("owasp_category", "UNMAPPED") for r in findings}
|
|
139
|
+
if "UNMAPPED" in categories:
|
|
140
|
+
categories.discard("UNMAPPED")
|
|
141
|
+
breadth = len(categories)
|
|
142
|
+
if breadth > 5:
|
|
143
|
+
score += 5 * (breadth - 5)
|
|
144
|
+
# Governance bonus
|
|
145
|
+
if roe_clean:
|
|
146
|
+
score -= 10
|
|
147
|
+
return max(0, min(100, score))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def interpret_risk(score: int) -> str:
|
|
151
|
+
if score <= 25:
|
|
152
|
+
return "Low"
|
|
153
|
+
if score <= 50:
|
|
154
|
+
return "Moderate"
|
|
155
|
+
if score <= 75:
|
|
156
|
+
return "Elevated"
|
|
157
|
+
if score <= 90:
|
|
158
|
+
return "High"
|
|
159
|
+
return "Critical"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# --- Top-3 priorities -------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def pick_top_priorities(findings: list[dict[str, Any]], overrides: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
166
|
+
if overrides:
|
|
167
|
+
return overrides[:3]
|
|
168
|
+
|
|
169
|
+
# Aggregate by title to detect reachability (same finding affecting many targets)
|
|
170
|
+
by_title: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
171
|
+
for r in findings:
|
|
172
|
+
if r.get("severity", "info") in ("critical", "high"):
|
|
173
|
+
by_title[r.get("title", "<no title>")].append(r)
|
|
174
|
+
|
|
175
|
+
if not by_title:
|
|
176
|
+
# No critical/high; fall back to medium
|
|
177
|
+
for r in findings:
|
|
178
|
+
if r.get("severity") == "medium":
|
|
179
|
+
by_title[r.get("title", "<no title>")].append(r)
|
|
180
|
+
|
|
181
|
+
# Score: severity-weight * reachability_factor
|
|
182
|
+
severity_weight = {"critical": 100, "high": 50, "medium": 20, "low": 5}
|
|
183
|
+
|
|
184
|
+
def title_score(title: str) -> int:
|
|
185
|
+
records = by_title[title]
|
|
186
|
+
max_sev = max(records, key=lambda x: severity_weight.get(x.get("severity", "info"), 0))
|
|
187
|
+
base = severity_weight.get(max_sev.get("severity", "info"), 0)
|
|
188
|
+
reach = len({r.get("target", "") for r in records})
|
|
189
|
+
return base + (reach * 5)
|
|
190
|
+
|
|
191
|
+
sorted_titles = sorted(by_title.keys(), key=lambda t: (-title_score(t), t))
|
|
192
|
+
out: list[dict[str, Any]] = []
|
|
193
|
+
for title in sorted_titles[:3]:
|
|
194
|
+
records = by_title[title]
|
|
195
|
+
sample = records[0]
|
|
196
|
+
out.append(
|
|
197
|
+
{
|
|
198
|
+
"title": title,
|
|
199
|
+
"severity": sample.get("severity", "info"),
|
|
200
|
+
"skill_id": sample.get("skill_id", "?"),
|
|
201
|
+
"reach": len({r.get("target", "") for r in records}),
|
|
202
|
+
"effort": estimate_effort(sample),
|
|
203
|
+
"impact": estimate_impact(sample, records),
|
|
204
|
+
"owasp": sample.get("owasp_category", "UNMAPPED"),
|
|
205
|
+
"fingerprint": sample.get("fingerprint", ""),
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def estimate_effort(record: dict[str, Any]) -> str:
|
|
212
|
+
skill = record.get("skill_id", "")
|
|
213
|
+
severity = record.get("severity", "info")
|
|
214
|
+
if "dependencies" in skill or "transitive" in skill:
|
|
215
|
+
return "Hours" if severity in ("critical", "high") else "Days"
|
|
216
|
+
if "secret" in skill or "credential" in skill:
|
|
217
|
+
return "Hours"
|
|
218
|
+
if "config" in skill or "header" in skill or "cors" in skill or "tls" in skill:
|
|
219
|
+
return "Days"
|
|
220
|
+
if "injection" in skill or "deserialization" in skill or "license" in skill:
|
|
221
|
+
return "Weeks"
|
|
222
|
+
return "Days"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def estimate_impact(record: dict[str, Any], all_records: list[dict[str, Any]]) -> str:
|
|
226
|
+
severity = record.get("severity", "info")
|
|
227
|
+
reach = len({r.get("target", "") for r in all_records})
|
|
228
|
+
if severity == "critical":
|
|
229
|
+
return "Material"
|
|
230
|
+
if severity == "high" and reach >= 3:
|
|
231
|
+
return "Material"
|
|
232
|
+
if severity == "high":
|
|
233
|
+
return "Significant"
|
|
234
|
+
if severity == "medium" and reach >= 5:
|
|
235
|
+
return "Significant"
|
|
236
|
+
return "Limited"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def load_priority_overrides(path: Path) -> list[dict[str, Any]]:
|
|
240
|
+
if not path.exists() or not _HAS_PYYAML:
|
|
241
|
+
return []
|
|
242
|
+
text = path.read_text(encoding="utf-8")
|
|
243
|
+
data = yaml.safe_load(text) or []
|
|
244
|
+
return data if isinstance(data, list) else []
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# --- ROE summary extraction -------------------------------------------------
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def summarize_roe(roe: dict[str, Any]) -> str:
|
|
251
|
+
if not roe:
|
|
252
|
+
return "_ROE not available._"
|
|
253
|
+
eng_id = roe.get("engagement_id", "<unknown>")
|
|
254
|
+
auth = roe.get("authorizer") or {}
|
|
255
|
+
auth_name = auth.get("name", "<unknown>")
|
|
256
|
+
auth_role = auth.get("role", "<role>")
|
|
257
|
+
time = roe.get("time_window") or {}
|
|
258
|
+
start = time.get("start", "<start>")
|
|
259
|
+
end = time.get("end", "<end>")
|
|
260
|
+
in_scope = roe.get("in_scope_targets") or []
|
|
261
|
+
return (
|
|
262
|
+
f"Engagement `{eng_id}` was authorized by {auth_name} ({auth_role}) for the time window "
|
|
263
|
+
f"{start} through {end}. Scope: {len(in_scope)} in-scope target(s)."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# --- Coverage report excerpt ------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def summarize_coverage(coverage_path: Path) -> str:
|
|
271
|
+
if not coverage_path.exists():
|
|
272
|
+
return "_OWASP coverage report not available._"
|
|
273
|
+
try:
|
|
274
|
+
text = coverage_path.read_text(encoding="utf-8")
|
|
275
|
+
except OSError:
|
|
276
|
+
return "_OWASP coverage report not readable._"
|
|
277
|
+
# Pull the summary table
|
|
278
|
+
lines = text.splitlines()
|
|
279
|
+
out: list[str] = []
|
|
280
|
+
in_table = False
|
|
281
|
+
table_count = 0
|
|
282
|
+
for line in lines:
|
|
283
|
+
if line.startswith("| Category"):
|
|
284
|
+
in_table = True
|
|
285
|
+
if in_table:
|
|
286
|
+
out.append(line)
|
|
287
|
+
if line.startswith("|"):
|
|
288
|
+
table_count += 1
|
|
289
|
+
if table_count > 11: # header + 10 categories
|
|
290
|
+
break
|
|
291
|
+
return "\n".join(out) if out else "_Coverage table not located._"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# --- Render -----------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def render_summary(
|
|
298
|
+
findings: list[dict[str, Any]],
|
|
299
|
+
risk_score: int,
|
|
300
|
+
risk_band: str,
|
|
301
|
+
counts: Counter,
|
|
302
|
+
priorities: list[dict[str, Any]],
|
|
303
|
+
roe_summary: str,
|
|
304
|
+
coverage_excerpt: str,
|
|
305
|
+
engagement_id: str,
|
|
306
|
+
) -> str:
|
|
307
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
308
|
+
crit = counts.get("critical", 0)
|
|
309
|
+
high = counts.get("high", 0)
|
|
310
|
+
med = counts.get("medium", 0)
|
|
311
|
+
low = counts.get("low", 0)
|
|
312
|
+
|
|
313
|
+
priorities_md = ""
|
|
314
|
+
for i, p in enumerate(priorities, start=1):
|
|
315
|
+
priorities_md += (
|
|
316
|
+
f"### {i}. {p['title']}\n\n"
|
|
317
|
+
f"- **Severity:** {p['severity'].upper()}\n"
|
|
318
|
+
f"- **Reach:** {p['reach']} affected target(s)\n"
|
|
319
|
+
f"- **Estimated effort to remediate:** {p['effort']}\n"
|
|
320
|
+
f"- **Estimated impact if exploited:** {p['impact']}\n"
|
|
321
|
+
f"- **OWASP:** {p['owasp']}\n"
|
|
322
|
+
f"- **Source skill:** `{p['skill_id']}`\n"
|
|
323
|
+
f"- **Cross-reference:** vulnerability-report.md#finding-{p['fingerprint']}\n\n"
|
|
324
|
+
)
|
|
325
|
+
if not priorities_md:
|
|
326
|
+
priorities_md = "_No HIGH or CRITICAL findings identified._\n"
|
|
327
|
+
|
|
328
|
+
return f"""# Executive Summary — {engagement_id}
|
|
329
|
+
|
|
330
|
+
**Generated:** {now}
|
|
331
|
+
|
|
332
|
+
## Risk score: {risk_score} / 100 ({risk_band})
|
|
333
|
+
|
|
334
|
+
| Severity | Count |
|
|
335
|
+
|---|---|
|
|
336
|
+
| CRITICAL | {crit} |
|
|
337
|
+
| HIGH | {high} |
|
|
338
|
+
| MEDIUM | {med} |
|
|
339
|
+
| LOW | {low} |
|
|
340
|
+
|
|
341
|
+
Risk-score composition: severity-weighted finding counts adjusted for
|
|
342
|
+
OWASP-category breadth and engagement governance posture. See the
|
|
343
|
+
vulnerability report for full per-finding detail.
|
|
344
|
+
|
|
345
|
+
## Engagement scope and authorization
|
|
346
|
+
|
|
347
|
+
{roe_summary}
|
|
348
|
+
|
|
349
|
+
## Top remediation priorities
|
|
350
|
+
|
|
351
|
+
{priorities_md}
|
|
352
|
+
|
|
353
|
+
## OWASP Top 10 (2021) coverage
|
|
354
|
+
|
|
355
|
+
{coverage_excerpt}
|
|
356
|
+
|
|
357
|
+
## Suggested next steps
|
|
358
|
+
|
|
359
|
+
1. Address the top remediation priorities above in the order listed.
|
|
360
|
+
Effort estimates are heuristic; refine after a brief planning
|
|
361
|
+
discussion with the responsible engineering team.
|
|
362
|
+
2. File a security-register entry for any MEDIUM-severity finding
|
|
363
|
+
that will not be remediated within the next quarter.
|
|
364
|
+
3. Schedule a re-test for the high-priority items once remediation is
|
|
365
|
+
complete to confirm the fixes hold.
|
|
366
|
+
4. Treat this summary plus the full vulnerability report as the
|
|
367
|
+
engagement's authoritative deliverables for compliance evidence,
|
|
368
|
+
board reporting, and insurance documentation.
|
|
369
|
+
|
|
370
|
+
## Reference artifacts
|
|
371
|
+
|
|
372
|
+
- Full vulnerability report: `reports/vulnerability-report.md`
|
|
373
|
+
- OWASP coverage detail: `reports/owasp-coverage.md`
|
|
374
|
+
- Engagement archive: `manifest.sha256` + manifest signature
|
|
375
|
+
- Rules of Engagement: `roe.yaml`
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
_Generated by `{SKILL_ID}`. The risk-score composition formula and
|
|
379
|
+
priority-selection logic are deterministic and documented in the
|
|
380
|
+
skill's THEORY reference. Re-running with the same source findings
|
|
381
|
+
produces a byte-identical document except for the generation date._
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# --- CLI ---------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
389
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
390
|
+
p.add_argument("path", help="Engagement directory")
|
|
391
|
+
p.add_argument("--source", default=None)
|
|
392
|
+
p.add_argument("--coverage", default=None)
|
|
393
|
+
p.add_argument("--roe", default=None)
|
|
394
|
+
p.add_argument("--summary-output", default=None)
|
|
395
|
+
p.add_argument("--priority-overrides", default=None)
|
|
396
|
+
p.add_argument("--output", default=None)
|
|
397
|
+
p.add_argument("--format", default="markdown", choices=["json", "jsonl", "markdown"])
|
|
398
|
+
p.add_argument(
|
|
399
|
+
"--min-severity",
|
|
400
|
+
default="info",
|
|
401
|
+
choices=["info", "low", "medium", "high", "critical"],
|
|
402
|
+
)
|
|
403
|
+
return p
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _filter_min_severity(findings: list[Finding], min_sev: str) -> list[Finding]:
|
|
407
|
+
floor = Severity(min_sev).numeric
|
|
408
|
+
return [f for f in findings if f.severity.numeric >= floor]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def main(argv: list[str] | None = None) -> int:
|
|
412
|
+
args = _build_arg_parser().parse_args(argv)
|
|
413
|
+
root = Path(args.path).resolve()
|
|
414
|
+
|
|
415
|
+
source_path = Path(args.source).resolve() if args.source else root / "findings" / "all-with-owasp.jsonl"
|
|
416
|
+
coverage_path = Path(args.coverage).resolve() if args.coverage else root / "reports" / "owasp-coverage.md"
|
|
417
|
+
roe_path = Path(args.roe).resolve() if args.roe else root / "roe.yaml"
|
|
418
|
+
|
|
419
|
+
op_findings: list[Finding] = []
|
|
420
|
+
|
|
421
|
+
findings, err = load_findings(source_path)
|
|
422
|
+
if err:
|
|
423
|
+
op_findings.append(
|
|
424
|
+
_f(
|
|
425
|
+
Severity.CRITICAL,
|
|
426
|
+
f"cannot load findings: {source_path.name}",
|
|
427
|
+
str(source_path),
|
|
428
|
+
err,
|
|
429
|
+
"Resolve the source and re-run.",
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
report.emit(op_findings, args.output, args.format, scan_target=str(root))
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
roe = load_roe(roe_path)
|
|
436
|
+
if not roe:
|
|
437
|
+
op_findings.append(
|
|
438
|
+
_f(
|
|
439
|
+
Severity.MEDIUM,
|
|
440
|
+
"ROE not loaded",
|
|
441
|
+
str(roe_path),
|
|
442
|
+
f"No ROE at {roe_path}; scope/authorization section will be a placeholder.",
|
|
443
|
+
"Provide --roe FILE or place the ROE at the expected path.",
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
roe_clean = bool(roe.get("authorizer") and roe.get("time_window") and roe.get("signature_block"))
|
|
448
|
+
|
|
449
|
+
counts = severity_counts(findings)
|
|
450
|
+
risk = compute_risk_score(findings, roe_clean)
|
|
451
|
+
band = interpret_risk(risk)
|
|
452
|
+
if risk > 90:
|
|
453
|
+
op_findings.append(
|
|
454
|
+
_f(
|
|
455
|
+
Severity.CRITICAL,
|
|
456
|
+
f"risk score {risk} (Critical)",
|
|
457
|
+
str(root),
|
|
458
|
+
"Computed engagement risk is in the Critical band. Findings warrant "
|
|
459
|
+
"executive attention and urgent remediation planning.",
|
|
460
|
+
"Review the top remediation priorities and start remediation today.",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
elif risk > 75:
|
|
464
|
+
op_findings.append(
|
|
465
|
+
_f(
|
|
466
|
+
Severity.HIGH,
|
|
467
|
+
f"risk score {risk} (High)",
|
|
468
|
+
str(root),
|
|
469
|
+
"Computed engagement risk is in the High band.",
|
|
470
|
+
"Schedule executive review and a remediation plan within the next week.",
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
overrides = load_priority_overrides(Path(args.priority_overrides).resolve()) if args.priority_overrides else []
|
|
475
|
+
priorities = pick_top_priorities(findings, overrides)
|
|
476
|
+
|
|
477
|
+
coverage_excerpt = summarize_coverage(coverage_path)
|
|
478
|
+
if not coverage_path.exists():
|
|
479
|
+
op_findings.append(
|
|
480
|
+
_f(
|
|
481
|
+
Severity.MEDIUM,
|
|
482
|
+
"OWASP coverage report missing",
|
|
483
|
+
str(coverage_path),
|
|
484
|
+
"Coverage section will be a placeholder.",
|
|
485
|
+
"Run mapping-findings-to-owasp-top10 first.",
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
engagement_id = roe.get("engagement_id") or root.name
|
|
490
|
+
summary_md = render_summary(
|
|
491
|
+
findings,
|
|
492
|
+
risk,
|
|
493
|
+
band,
|
|
494
|
+
counts,
|
|
495
|
+
priorities,
|
|
496
|
+
summarize_roe(roe),
|
|
497
|
+
coverage_excerpt,
|
|
498
|
+
engagement_id,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
out_path = Path(args.summary_output).resolve() if args.summary_output else root / "reports" / "executive-summary.md"
|
|
502
|
+
try:
|
|
503
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
out_path.write_text(summary_md, encoding="utf-8")
|
|
505
|
+
op_findings.append(
|
|
506
|
+
_f(
|
|
507
|
+
Severity.INFO,
|
|
508
|
+
f"executive summary written: {out_path.name}",
|
|
509
|
+
str(out_path),
|
|
510
|
+
f"Risk: {risk}/100 ({band}); priorities: {len(priorities)}; findings: {len(findings)}.",
|
|
511
|
+
"Hand off to customer for the exec-readout meeting.",
|
|
512
|
+
evidence=(
|
|
513
|
+
("risk_score", risk),
|
|
514
|
+
("band", band),
|
|
515
|
+
("finding_count", len(findings)),
|
|
516
|
+
("priority_count", len(priorities)),
|
|
517
|
+
("output", str(out_path)),
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
except OSError as e:
|
|
522
|
+
op_findings.append(
|
|
523
|
+
_f(
|
|
524
|
+
Severity.HIGH,
|
|
525
|
+
f"cannot write summary: {out_path}",
|
|
526
|
+
str(out_path),
|
|
527
|
+
f"OSError: {e}",
|
|
528
|
+
"Resolve permissions and re-run.",
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
op_findings = _filter_min_severity(op_findings, args.min_severity)
|
|
533
|
+
report.emit(op_findings, args.output, args.format, scan_target=str(root))
|
|
534
|
+
return report.exit_code(op_findings)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
if __name__ == "__main__":
|
|
538
|
+
sys.exit(main())
|