pyGdbToolkit 0.1.0__py3-none-any.whl
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.
- pyGdbToolkit/__init__.py +55 -0
- pyGdbToolkit/arch/__init__.py +79 -0
- pyGdbToolkit/arch/arm/__init__.py +17 -0
- pyGdbToolkit/arch/arm/coresight.py +427 -0
- pyGdbToolkit/arch/arm/cortex_m.py +504 -0
- pyGdbToolkit/arch/arm/fault.py +629 -0
- pyGdbToolkit/arch/arm/models.py +60 -0
- pyGdbToolkit/arch/arm/mpu.py +556 -0
- pyGdbToolkit/arch/arm/probe.py +10 -0
- pyGdbToolkit/arch/arm/providers/__init__.py +16 -0
- pyGdbToolkit/arch/arm/providers/base.py +77 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/__init__.py +4 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/stm32.py +680 -0
- pyGdbToolkit/arch/arm/sau.py +253 -0
- pyGdbToolkit/arch/arm/security.py +816 -0
- pyGdbToolkit/arch/arm/session_state.py +96 -0
- pyGdbToolkit/arch/arm/target.py +32 -0
- pyGdbToolkit/arch/base.py +143 -0
- pyGdbToolkit/arch/diagnostics.py +300 -0
- pyGdbToolkit/arch/registry.py +43 -0
- pyGdbToolkit/cmd_faultinfo.py +99 -0
- pyGdbToolkit/cmd_lscpu.py +116 -0
- pyGdbToolkit/cmd_profile.py +1402 -0
- pyGdbToolkit/cmd_rtos.py +298 -0
- pyGdbToolkit/cmd_secscan.py +466 -0
- pyGdbToolkit/cmd_svd.py +1208 -0
- pyGdbToolkit/diagnostic_runtime.py +52 -0
- pyGdbToolkit/rtos/__init__.py +8 -0
- pyGdbToolkit/rtos/camelot.py +513 -0
- pyGdbToolkit/session.py +233 -0
- pyGdbToolkit/svd.py +1194 -0
- pyGdbToolkit/target_memory.py +190 -0
- pygdbtoolkit-0.1.0.dist-info/METADATA +586 -0
- pygdbtoolkit-0.1.0.dist-info/RECORD +36 -0
- pygdbtoolkit-0.1.0.dist-info/WHEEL +5 -0
- pygdbtoolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The architecture-neutral ``secscan`` GDB command entry point."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import html
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import gdb
|
|
16
|
+
from rich import box
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
from .arch import (
|
|
23
|
+
DiagnosticReport,
|
|
24
|
+
DiagnosticServiceName,
|
|
25
|
+
DiagnosticSeverity,
|
|
26
|
+
)
|
|
27
|
+
from .diagnostic_runtime import gdb_diagnostic_access
|
|
28
|
+
from .session import SESSION, ToolkitSession
|
|
29
|
+
from .target_memory import TargetReadError
|
|
30
|
+
|
|
31
|
+
CONSOLE = Console(force_terminal=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class SecscanFinding:
|
|
36
|
+
"""One security-audit observation."""
|
|
37
|
+
|
|
38
|
+
category: str
|
|
39
|
+
severity: str
|
|
40
|
+
title: str
|
|
41
|
+
detail: str
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
"""Convert this finding to a plain dictionary."""
|
|
45
|
+
return {
|
|
46
|
+
"category": self.category,
|
|
47
|
+
"severity": self.severity,
|
|
48
|
+
"title": self.title,
|
|
49
|
+
"detail": self.detail,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: dict[str, Any]) -> SecscanFinding:
|
|
54
|
+
"""Rebuild a finding from its dictionary representation."""
|
|
55
|
+
return cls(
|
|
56
|
+
category=str(data["category"]),
|
|
57
|
+
severity=str(data["severity"]),
|
|
58
|
+
title=str(data["title"]),
|
|
59
|
+
detail=str(data["detail"]),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class SecscanReport:
|
|
65
|
+
"""A complete security-posture audit report rendered by this command."""
|
|
66
|
+
|
|
67
|
+
core: str
|
|
68
|
+
vendor: str | None
|
|
69
|
+
device_name: str | None
|
|
70
|
+
generated_at: str
|
|
71
|
+
findings: tuple[SecscanFinding, ...]
|
|
72
|
+
|
|
73
|
+
def counts(self) -> dict[str, int]:
|
|
74
|
+
"""Return the number of findings per established output severity."""
|
|
75
|
+
counts = {"FAIL": 0, "WARN": 0, "INFO": 0, "PASS": 0}
|
|
76
|
+
for finding in self.findings:
|
|
77
|
+
counts[finding.severity] = counts.get(finding.severity, 0) + 1
|
|
78
|
+
return counts
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, Any]:
|
|
81
|
+
"""Convert the complete report to the stable JSON schema."""
|
|
82
|
+
return {
|
|
83
|
+
"core": self.core,
|
|
84
|
+
"vendor": self.vendor,
|
|
85
|
+
"device_name": self.device_name,
|
|
86
|
+
"generated_at": self.generated_at,
|
|
87
|
+
"summary": self.counts(),
|
|
88
|
+
"findings": [finding.to_dict() for finding in self.findings],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dict(cls, data: dict[str, Any]) -> SecscanReport:
|
|
93
|
+
"""Rebuild a report from its stable JSON schema."""
|
|
94
|
+
return cls(
|
|
95
|
+
core=str(data["core"]),
|
|
96
|
+
vendor=data.get("vendor"),
|
|
97
|
+
device_name=data.get("device_name"),
|
|
98
|
+
generated_at=str(data["generated_at"]),
|
|
99
|
+
findings=tuple(SecscanFinding.from_dict(finding) for finding in data["findings"]),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_SEVERITY_NAMES = {
|
|
104
|
+
DiagnosticSeverity.PASS: "PASS",
|
|
105
|
+
DiagnosticSeverity.INFO: "INFO",
|
|
106
|
+
DiagnosticSeverity.WARNING: "WARN",
|
|
107
|
+
DiagnosticSeverity.ERROR: "FAIL",
|
|
108
|
+
DiagnosticSeverity.CRITICAL: "FAIL",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run_audit(session: ToolkitSession = SESSION) -> SecscanReport:
|
|
113
|
+
"""Run the registered security-audit service against the selected target.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
session : ToolkitSession
|
|
118
|
+
Unified session providing target access and the portable diagnostic
|
|
119
|
+
runtime. Dependency injection keeps command tests independent of a
|
|
120
|
+
live GDB target.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
SecscanReport
|
|
125
|
+
The output-compatible report model.
|
|
126
|
+
|
|
127
|
+
Raises
|
|
128
|
+
------
|
|
129
|
+
gdb.GdbError
|
|
130
|
+
If no registered architecture can perform the requested audit.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
result = session.diagnose(
|
|
134
|
+
DiagnosticServiceName.SECURITY_AUDIT,
|
|
135
|
+
gdb_diagnostic_access(),
|
|
136
|
+
)
|
|
137
|
+
except TargetReadError as error:
|
|
138
|
+
raise gdb.GdbError(str(error)) from error
|
|
139
|
+
if result.report is None:
|
|
140
|
+
if result.access_error:
|
|
141
|
+
raise gdb.GdbError(result.unavailable_reason or "target unavailable")
|
|
142
|
+
raise gdb.GdbError("secscan only supports ARM Cortex-M targets")
|
|
143
|
+
return _render_model(result.report)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _render_model(report: DiagnosticReport) -> SecscanReport:
|
|
147
|
+
"""Adapt a portable diagnostic report to the stable secscan file schema."""
|
|
148
|
+
return SecscanReport(
|
|
149
|
+
core=report.target.core_name,
|
|
150
|
+
vendor=_target_field(report, "vendor"),
|
|
151
|
+
device_name=_target_field(report, "device_name"),
|
|
152
|
+
generated_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
153
|
+
findings=tuple(
|
|
154
|
+
SecscanFinding(
|
|
155
|
+
finding.category,
|
|
156
|
+
_SEVERITY_NAMES.get(finding.severity, finding.severity.value.upper()),
|
|
157
|
+
finding.title,
|
|
158
|
+
finding.detail,
|
|
159
|
+
)
|
|
160
|
+
for finding in report.findings
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _target_field(report: DiagnosticReport, name: str) -> str | None:
|
|
166
|
+
"""Return a string target metadata field supplied by a diagnostic service."""
|
|
167
|
+
for section in report.sections:
|
|
168
|
+
for field in section.fields:
|
|
169
|
+
if field.name == name and isinstance(field.value, str):
|
|
170
|
+
return field.value
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
_SEVERITY_STYLE = {
|
|
175
|
+
"FAIL": "bold red",
|
|
176
|
+
"WARN": "bold yellow",
|
|
177
|
+
"INFO": "cyan",
|
|
178
|
+
"PASS": "bold green",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def render_report(report: SecscanReport) -> None:
|
|
183
|
+
"""Render a security-audit report through the shared Rich console."""
|
|
184
|
+
counts = report.counts()
|
|
185
|
+
summary = (
|
|
186
|
+
f"Core: {report.core} | Vendor: {report.vendor or 'Unknown'} | "
|
|
187
|
+
f"Device: {report.device_name or 'Unknown'}\n"
|
|
188
|
+
f"Generated: {report.generated_at}\n"
|
|
189
|
+
f"[bold red]FAIL: {counts['FAIL']}[/bold red] "
|
|
190
|
+
f"[bold yellow]WARN: {counts['WARN']}[/bold yellow] "
|
|
191
|
+
f"[cyan]INFO: {counts['INFO']}[/cyan] "
|
|
192
|
+
f"[bold green]PASS: {counts['PASS']}[/bold green]"
|
|
193
|
+
)
|
|
194
|
+
CONSOLE.print(Panel(summary, title="secscan audit summary", box=box.SIMPLE_HEAVY))
|
|
195
|
+
|
|
196
|
+
categories: list[str] = []
|
|
197
|
+
for finding in report.findings:
|
|
198
|
+
if finding.category not in categories:
|
|
199
|
+
categories.append(finding.category)
|
|
200
|
+
for category in categories:
|
|
201
|
+
table = Table(
|
|
202
|
+
title=category,
|
|
203
|
+
box=box.SIMPLE_HEAVY,
|
|
204
|
+
header_style="bold cyan",
|
|
205
|
+
show_header=True,
|
|
206
|
+
)
|
|
207
|
+
table.add_column("Severity", no_wrap=True)
|
|
208
|
+
table.add_column("Finding", style="bold")
|
|
209
|
+
table.add_column("Detail")
|
|
210
|
+
for finding in report.findings:
|
|
211
|
+
if finding.category == category:
|
|
212
|
+
table.add_row(
|
|
213
|
+
Text(finding.severity, style=_SEVERITY_STYLE.get(finding.severity, "")),
|
|
214
|
+
finding.title,
|
|
215
|
+
finding.detail,
|
|
216
|
+
)
|
|
217
|
+
CONSOLE.print(table)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def dump_report_to_json(report: SecscanReport, output_path: Path) -> int:
|
|
221
|
+
"""Serialize an audit report to a JSON file and return its byte size."""
|
|
222
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
with output_path.open("w", encoding="utf-8") as json_file:
|
|
224
|
+
json.dump(report.to_dict(), json_file, indent=2)
|
|
225
|
+
return output_path.stat().st_size
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
_HTML_STYLE = """
|
|
229
|
+
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 2rem; color: #1a1a1a; }
|
|
230
|
+
h1 { margin-bottom: 0.25rem; }
|
|
231
|
+
.meta { color: #555; margin-bottom: 1.5rem; }
|
|
232
|
+
.summary { display: flex; gap: 1rem; margin-bottom: 2rem; }
|
|
233
|
+
.badge { padding: 0.5rem 1rem; border-radius: 6px; font-weight: 600; color: #fff; }
|
|
234
|
+
.badge.fail { background: #c0392b; }
|
|
235
|
+
.badge.warn { background: #b9770e; }
|
|
236
|
+
.badge.info { background: #2874a6; }
|
|
237
|
+
.badge.pass { background: #1e8449; }
|
|
238
|
+
section { margin-bottom: 2rem; }
|
|
239
|
+
table { border-collapse: collapse; width: 100%; }
|
|
240
|
+
th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; }
|
|
241
|
+
th { background: #f4f4f4; }
|
|
242
|
+
td.sev { font-weight: 700; white-space: nowrap; }
|
|
243
|
+
tr.sev-fail td.sev { color: #c0392b; }
|
|
244
|
+
tr.sev-warn td.sev { color: #b9770e; }
|
|
245
|
+
tr.sev-info td.sev { color: #2874a6; }
|
|
246
|
+
tr.sev-pass td.sev { color: #1e8449; }
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
_HTML_SEVERITY_CLASS = {
|
|
250
|
+
"FAIL": "sev-fail",
|
|
251
|
+
"WARN": "sev-warn",
|
|
252
|
+
"INFO": "sev-info",
|
|
253
|
+
"PASS": "sev-pass",
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _html_finding_row(finding: SecscanFinding) -> str:
|
|
258
|
+
"""Render one finding as an HTML table row, escaping all user-facing text."""
|
|
259
|
+
row_class = _HTML_SEVERITY_CLASS.get(finding.severity, "")
|
|
260
|
+
severity = html.escape(finding.severity)
|
|
261
|
+
title = html.escape(finding.title)
|
|
262
|
+
detail = html.escape(finding.detail)
|
|
263
|
+
return (
|
|
264
|
+
f'<tr class="{row_class}"><td class="sev">{severity}</td>'
|
|
265
|
+
f"<td>{title}</td><td>{detail}</td></tr>"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def generate_html_report(report: SecscanReport) -> str:
|
|
270
|
+
"""Render a security-audit report as a standalone HTML document."""
|
|
271
|
+
counts = report.counts()
|
|
272
|
+
categories: list[str] = []
|
|
273
|
+
for finding in report.findings:
|
|
274
|
+
if finding.category not in categories:
|
|
275
|
+
categories.append(finding.category)
|
|
276
|
+
sections: list[str] = []
|
|
277
|
+
for category in categories:
|
|
278
|
+
rows = "\n".join(
|
|
279
|
+
_html_finding_row(finding)
|
|
280
|
+
for finding in report.findings
|
|
281
|
+
if finding.category == category
|
|
282
|
+
)
|
|
283
|
+
sections.append(
|
|
284
|
+
f"<section><h2>{html.escape(category)}</h2>"
|
|
285
|
+
"<table><thead><tr><th>Severity</th><th>Finding</th><th>Detail</th></tr></thead>"
|
|
286
|
+
f"<tbody>{rows}</tbody></table></section>"
|
|
287
|
+
)
|
|
288
|
+
head = (
|
|
289
|
+
'<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n'
|
|
290
|
+
f"<title>secscan report - {html.escape(report.core)}</title>\n"
|
|
291
|
+
f"<style>{_HTML_STYLE}</style>\n</head>\n<body>\n"
|
|
292
|
+
)
|
|
293
|
+
meta = (
|
|
294
|
+
"<h1>secscan audit report</h1>\n"
|
|
295
|
+
f'<p class="meta">Core: {html.escape(report.core)} · '
|
|
296
|
+
f"Vendor: {html.escape(report.vendor or 'Unknown')} · "
|
|
297
|
+
f"Device: {html.escape(report.device_name or 'Unknown')} · "
|
|
298
|
+
f"Generated: {html.escape(report.generated_at)}</p>\n"
|
|
299
|
+
)
|
|
300
|
+
summary = (
|
|
301
|
+
'<div class="summary">'
|
|
302
|
+
f'<span class="badge fail">FAIL {counts["FAIL"]}</span>'
|
|
303
|
+
f'<span class="badge warn">WARN {counts["WARN"]}</span>'
|
|
304
|
+
f'<span class="badge info">INFO {counts["INFO"]}</span>'
|
|
305
|
+
f'<span class="badge pass">PASS {counts["PASS"]}</span>'
|
|
306
|
+
"</div>\n"
|
|
307
|
+
)
|
|
308
|
+
return head + meta + summary + "\n".join(sections) + "\n</body>\n</html>\n"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def dump_report_to_html(report: SecscanReport, output_path: Path) -> int:
|
|
312
|
+
"""Write a standalone HTML report and return its byte size."""
|
|
313
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
with output_path.open("w", encoding="utf-8") as html_file:
|
|
315
|
+
html_file.write(generate_html_report(report))
|
|
316
|
+
return output_path.stat().st_size
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def load_report_from_json(input_path: Path) -> SecscanReport:
|
|
320
|
+
"""Load a previously dumped audit report from a JSON file."""
|
|
321
|
+
with input_path.open("r", encoding="utf-8") as json_file:
|
|
322
|
+
data = json.load(json_file)
|
|
323
|
+
try:
|
|
324
|
+
return SecscanReport.from_dict(data)
|
|
325
|
+
except (KeyError, TypeError) as error:
|
|
326
|
+
raise ValueError(f"'{input_path}' is not a valid secscan report: {error}") from error
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def render_help() -> None:
|
|
330
|
+
"""Render the secscan command overview and help table."""
|
|
331
|
+
table = Table(
|
|
332
|
+
title="Cortex-M Security Posture Audit",
|
|
333
|
+
box=box.SIMPLE_HEAVY,
|
|
334
|
+
header_style="bold cyan",
|
|
335
|
+
show_header=True,
|
|
336
|
+
)
|
|
337
|
+
table.add_column("Command Syntax", style="bold yellow", no_wrap=True)
|
|
338
|
+
table.add_column("Description")
|
|
339
|
+
table.add_row(
|
|
340
|
+
"secscan audit [<output.json>]",
|
|
341
|
+
"Run the full security audit against the live target and optionally dump it to JSON",
|
|
342
|
+
)
|
|
343
|
+
table.add_row(
|
|
344
|
+
"secscan report <report.json> [--html <output.html>]",
|
|
345
|
+
"Render a clean report from a previously dumped audit file (console by default, "
|
|
346
|
+
"or a standalone HTML file with --html)",
|
|
347
|
+
)
|
|
348
|
+
table.add_row("secscan help", "Show this command reference")
|
|
349
|
+
CONSOLE.print(table)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class SecscanCmd(gdb.Command):
|
|
353
|
+
"""Run a registered security audit and render its portable report."""
|
|
354
|
+
|
|
355
|
+
def __init__(self) -> None:
|
|
356
|
+
"""Register the command with GDB."""
|
|
357
|
+
super().__init__("secscan", gdb.COMMAND_USER, gdb.COMPLETE_NONE, True)
|
|
358
|
+
|
|
359
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
360
|
+
"""Display the help overview."""
|
|
361
|
+
del arg
|
|
362
|
+
del from_tty
|
|
363
|
+
render_help()
|
|
364
|
+
|
|
365
|
+
def _invoke_audit(self, args: list[str]) -> None:
|
|
366
|
+
"""Handle ``secscan audit [<output.json>]``."""
|
|
367
|
+
if len(args) > 1:
|
|
368
|
+
raise gdb.GdbError("Usage: secscan audit [<output.json>]")
|
|
369
|
+
report = run_audit()
|
|
370
|
+
render_report(report)
|
|
371
|
+
if args:
|
|
372
|
+
out_path = Path(args[0]).expanduser().resolve()
|
|
373
|
+
try:
|
|
374
|
+
dump_report_to_json(report, out_path)
|
|
375
|
+
except OSError as error:
|
|
376
|
+
raise gdb.GdbError(f"Failed to write dump to '{out_path}': {error}") from error
|
|
377
|
+
CONSOLE.print(f"[green]Report written to {out_path}[/green]")
|
|
378
|
+
|
|
379
|
+
def _invoke_report(self, args: list[str]) -> None:
|
|
380
|
+
"""Handle ``secscan report <report.json> [--html <output.html>]``."""
|
|
381
|
+
usage = "Usage: secscan report <report.json> [--html <output.html>]"
|
|
382
|
+
html_arg: str | None = None
|
|
383
|
+
positional: list[str] = []
|
|
384
|
+
index = 0
|
|
385
|
+
while index < len(args):
|
|
386
|
+
token = args[index]
|
|
387
|
+
if token == "--html":
|
|
388
|
+
if index + 1 >= len(args):
|
|
389
|
+
raise gdb.GdbError(f"--html requires an output file path. {usage}")
|
|
390
|
+
html_arg = args[index + 1]
|
|
391
|
+
index += 2
|
|
392
|
+
continue
|
|
393
|
+
positional.append(token)
|
|
394
|
+
index += 1
|
|
395
|
+
if len(positional) != 1:
|
|
396
|
+
raise gdb.GdbError(usage)
|
|
397
|
+
input_path = Path(positional[0]).expanduser().resolve()
|
|
398
|
+
try:
|
|
399
|
+
report = load_report_from_json(input_path)
|
|
400
|
+
except (OSError, ValueError) as error:
|
|
401
|
+
raise gdb.GdbError(f"Failed to read report from '{input_path}': {error}") from error
|
|
402
|
+
if html_arg is not None:
|
|
403
|
+
html_path = Path(html_arg).expanduser().resolve()
|
|
404
|
+
try:
|
|
405
|
+
file_size = dump_report_to_html(report, html_path)
|
|
406
|
+
except OSError as error:
|
|
407
|
+
raise gdb.GdbError(
|
|
408
|
+
f"Failed to write HTML report to '{html_path}': {error}"
|
|
409
|
+
) from error
|
|
410
|
+
CONSOLE.print(f"[green]HTML report written to {html_path} ({file_size} bytes)[/green]")
|
|
411
|
+
return
|
|
412
|
+
render_report(report)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class _SecscanSubcommand(gdb.Command):
|
|
416
|
+
"""Base class for concrete ``secscan`` subcommands."""
|
|
417
|
+
|
|
418
|
+
def __init__(self, parent: SecscanCmd, name: str) -> None:
|
|
419
|
+
"""Register one ``secscan <name>`` subcommand."""
|
|
420
|
+
self.parent = parent
|
|
421
|
+
self.name = name
|
|
422
|
+
super().__init__(f"secscan {name}", gdb.COMMAND_USER)
|
|
423
|
+
|
|
424
|
+
def _argv(self, arg: str) -> list[str]:
|
|
425
|
+
"""Split command arguments using GDB's CLI lexer."""
|
|
426
|
+
return list(gdb.string_to_argv(arg)) if arg.strip() else []
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class SecscanAuditCmd(_SecscanSubcommand):
|
|
430
|
+
"""Run the full security audit against the live target."""
|
|
431
|
+
|
|
432
|
+
def __init__(self, parent: SecscanCmd) -> None:
|
|
433
|
+
"""Register the ``secscan audit`` subcommand."""
|
|
434
|
+
super().__init__(parent, "audit")
|
|
435
|
+
|
|
436
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
437
|
+
"""Execute ``secscan audit [<output.json>]``."""
|
|
438
|
+
del from_tty
|
|
439
|
+
self.parent._invoke_audit(self._argv(arg))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class SecscanReportCmd(_SecscanSubcommand):
|
|
443
|
+
"""Render a previously dumped audit report from a JSON file."""
|
|
444
|
+
|
|
445
|
+
def __init__(self, parent: SecscanCmd) -> None:
|
|
446
|
+
"""Register the ``secscan report`` subcommand."""
|
|
447
|
+
super().__init__(parent, "report")
|
|
448
|
+
|
|
449
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
450
|
+
"""Execute ``secscan report <report.json>``."""
|
|
451
|
+
del from_tty
|
|
452
|
+
self.parent._invoke_report(self._argv(arg))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class SecscanHelpCmd(_SecscanSubcommand):
|
|
456
|
+
"""Show the secscan command reference."""
|
|
457
|
+
|
|
458
|
+
def __init__(self, parent: SecscanCmd) -> None:
|
|
459
|
+
"""Register the ``secscan help`` subcommand."""
|
|
460
|
+
super().__init__(parent, "help")
|
|
461
|
+
|
|
462
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
463
|
+
"""Execute ``secscan help``."""
|
|
464
|
+
del arg
|
|
465
|
+
del from_tty
|
|
466
|
+
render_help()
|