src/pqc_lint/reporters/text.py
2.9 KB · 86 lines · python Raw
1 """Plain-text reporter for terminal output."""
2
3 from __future__ import annotations
4
5 from io import StringIO
6
7 from rich.console import Console
8 from rich.table import Table
9
10 from pqc_lint.findings import ScanReport, Severity
11 from pqc_lint.reporters.base import Reporter
12
13 _SEVERITY_STYLE = {
14 Severity.CRITICAL: "bold red",
15 Severity.HIGH: "red",
16 Severity.MEDIUM: "yellow",
17 Severity.LOW: "cyan",
18 Severity.INFO: "dim",
19 }
20
21
22 class TextReporter(Reporter):
23 format_name = "text"
24
25 def render(self, report: ScanReport) -> str:
26 buf = StringIO()
27 console = Console(file=buf, force_terminal=False, width=120)
28
29 counts = report.counts_by_severity()
30 total = len(report.findings)
31
32 console.print()
33 console.print("[bold]PQC Lint Scan Report[/bold]")
34 console.print(f"Root: {report.scan_root}")
35 console.print(
36 f"Files scanned: {report.files_scanned} "
37 f"skipped: {report.files_skipped} "
38 f"duration: {report.duration_ms}ms"
39 )
40 console.print()
41 console.print(
42 f"[bold]Summary[/bold]: {total} findings "
43 f"[bold red]{counts['critical']} critical[/bold red] "
44 f"[red]{counts['high']} high[/red] "
45 f"[yellow]{counts['medium']} medium[/yellow] "
46 f"[cyan]{counts['low']} low[/cyan]"
47 )
48 console.print()
49
50 if not report.findings:
51 console.print("[green]No classical crypto detected. Looks PQC-clean.[/green]")
52 return buf.getvalue()
53
54 # Group findings by file
55 by_file: dict[str, list] = {}
56 for f in report.findings:
57 by_file.setdefault(f.file, []).append(f)
58
59 for path in sorted(by_file):
60 console.print(f"[bold]{path}[/bold]")
61 table = Table(show_header=True, header_style="bold dim", box=None, padding=(0, 1))
62 table.add_column("Line", justify="right", style="dim")
63 table.add_column("Severity")
64 table.add_column("Rule")
65 table.add_column("Message")
66 for finding in sorted(by_file[path], key=lambda x: x.line):
67 style = _SEVERITY_STYLE[finding.severity]
68 table.add_row(
69 str(finding.line),
70 f"[{style}]{finding.severity.value.upper()}[/{style}]",
71 finding.rule_id,
72 finding.message.split(":")[0],
73 )
74 console.print(table)
75 for finding in sorted(by_file[path], key=lambda x: x.line):
76 if finding.snippet:
77 console.print(
78 f" [dim]{finding.line}[/dim] "
79 f"[dim cyan]{finding.snippet}[/dim cyan]"
80 )
81 if finding.suggestion:
82 console.print(f" [dim]->[/dim] {finding.suggestion}")
83 console.print()
84
85 return buf.getvalue()
86