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