Skip to content

Commit e657fae

Browse files
committed
Refactor ConsoleFormatter for improved findings display and summary output
1 parent d168e0f commit e657fae

File tree

1 file changed

+101
-47
lines changed

1 file changed

+101
-47
lines changed
Lines changed: 101 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
"""Rich-based colored terminal output."""
1+
"""Semgrep-style colored terminal output."""
22

33
from __future__ import annotations
44

5+
from collections import defaultdict
6+
57
from rich.console import Console
68
from rich.panel import Panel
7-
from rich.table import Table
89
from rich.text import Text
910

1011
from llm_authz_audit.core.engine import ScanResult
11-
from llm_authz_audit.core.finding import Severity
12+
from llm_authz_audit.core.finding import Finding, Severity
1213
from llm_authz_audit.output.formatter import BaseFormatter, FormatterFactory
1314

1415
_SEVERITY_COLORS = {
@@ -18,70 +19,123 @@
1819
Severity.LOW: "blue",
1920
}
2021

21-
_SEVERITY_ICONS = {
22-
Severity.CRITICAL: "[!]",
23-
Severity.HIGH: "[H]",
24-
Severity.MEDIUM: "[M]",
25-
Severity.LOW: "[L]",
22+
_SEVERITY_CHEVRONS = {
23+
Severity.CRITICAL: "\u276f\u276f\u276f\u2771", # ❯❯❯❱
24+
Severity.HIGH: "\u276f\u276f\u2771", # ❯❯❱
25+
Severity.MEDIUM: "\u276f\u2771", # ❯❱
26+
Severity.LOW: "\u2771", # ❱
2627
}
2728

2829

30+
def _section_box(title: str) -> Panel:
31+
"""Semgrep-style section header: ┌──────┐ │ Title │ └──────┘"""
32+
return Panel(
33+
Text(title, style="bold"),
34+
expand=False,
35+
border_style="dim",
36+
padding=(0, 1),
37+
)
38+
39+
2940
class ConsoleFormatter(BaseFormatter):
3041
def format(self, result: ScanResult) -> str:
3142
console = Console(record=True, width=120)
3243

44+
# ── Findings ──
3345
if not result.findings:
34-
console.print(Panel(
35-
"[bold green]No security findings detected.[/bold green]",
36-
title="llm-authz-audit",
37-
border_style="green",
38-
))
46+
console.print(_section_box("Scan Complete"))
47+
console.print(" [bold green]\u2705 No security findings detected.[/bold green]")
48+
console.print()
3949
else:
40-
console.print(Panel(
41-
f"[bold]{len(result.findings)} finding(s) detected[/bold]",
42-
title="llm-authz-audit",
43-
border_style="red" if result.exit_code else "yellow",
44-
))
50+
count = len(result.findings)
51+
blocking = sum(
52+
1 for f in result.findings
53+
if f.severity >= Severity.HIGH
54+
)
55+
label = f"{count} Code Finding{'s' if count != 1 else ''}"
56+
console.print(_section_box(label))
57+
console.print()
4558

46-
for finding in result.findings:
47-
sev_color = _SEVERITY_COLORS.get(finding.severity, "white")
48-
icon = _SEVERITY_ICONS.get(finding.severity, "[-]")
59+
self._print_findings(console, result.findings)
4960

50-
header = Text()
51-
header.append(f"{icon} ", style=sev_color)
52-
header.append(f"{finding.rule_id}: ", style="bold")
53-
header.append(finding.title)
61+
# ── Scan Summary ──
62+
console.print(_section_box("Scan Summary"))
63+
self._print_summary(console, result)
64+
65+
return console.export_text()
5466

55-
console.print(header)
56-
location = f" {finding.file_path}"
57-
if finding.line_number:
58-
location += f":{finding.line_number}"
59-
console.print(location, style="dim")
67+
def _print_findings(self, console: Console, findings: list[Finding]) -> None:
68+
# Group findings by file path (preserving severity order within each file)
69+
by_file: dict[str, list[Finding]] = defaultdict(list)
70+
for f in findings:
71+
by_file[f.file_path].append(f)
6072

61-
if finding.snippet:
62-
console.print(f" > {finding.snippet}", style="dim italic")
73+
for file_path, file_findings in by_file.items():
74+
console.print(f" {file_path}", highlight=False)
6375

64-
console.print(f" {finding.description}")
65-
console.print(f" Fix: {finding.remediation}", style="green")
76+
for finding in file_findings:
77+
sev_color = _SEVERITY_COLORS.get(finding.severity, "white")
78+
chevron = _SEVERITY_CHEVRONS.get(finding.severity, "\u2771")
6679

80+
# ❯❯❱ rule_id
81+
header = Text()
82+
header.append(f" {chevron} ", style=sev_color)
83+
header.append(finding.rule_id, style="bold")
6784
if finding.owasp_llm:
68-
console.print(f" OWASP LLM: {finding.owasp_llm}", style="dim")
85+
header.append(f" [{finding.owasp_llm}]", style="dim")
86+
console.print(header, highlight=False)
87+
88+
# Indented description
89+
console.print(f" {finding.title}", highlight=False)
90+
91+
# Code snippet with line number: 5┆ code here
92+
if finding.snippet and finding.line_number:
93+
console.print(
94+
f" [dim]{finding.line_number}\u2506[/dim] {finding.snippet}",
95+
highlight=False,
96+
)
97+
elif finding.snippet:
98+
console.print(
99+
f" [dim]\u2506[/dim] {finding.snippet}",
100+
highlight=False,
101+
)
102+
103+
# Remediation
104+
console.print(
105+
f" [green]fix:[/green] [dim]{finding.remediation}[/dim]",
106+
highlight=False,
107+
)
69108
console.print()
70109

71-
# Summary table
72-
table = Table(title="Summary", show_header=True, header_style="bold")
73-
table.add_column("Metric", style="bold")
74-
table.add_column("Value", justify="right")
75-
table.add_row("Files scanned", str(result.files_scanned))
76-
table.add_row("Analyzers run", str(len(result.analyzers_run)))
77-
table.add_row("Total findings", str(len(result.findings)))
110+
def _print_summary(self, console: Console, result: ScanResult) -> None:
111+
if result.findings:
112+
blocking = sum(1 for f in result.findings if f.severity >= Severity.HIGH)
113+
console.print(
114+
f" [bold]\u26a0 Findings:[/bold] {len(result.findings)}"
115+
f" ({blocking} blocking)",
116+
highlight=False,
117+
)
118+
else:
119+
console.print(
120+
" [bold green]\u2705 Scan completed successfully.[/bold green]",
121+
highlight=False,
122+
)
123+
124+
console.print(f" [dim]\u2022[/dim] Analyzers run: {len(result.analyzers_run)}", highlight=False)
125+
console.print(f" [dim]\u2022[/dim] Files scanned: {result.files_scanned}", highlight=False)
126+
127+
# Severity breakdown
78128
for sev in [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW]:
79129
count = result.summary.get(sev.value, 0)
80-
style = _SEVERITY_COLORS.get(sev, "white") if count > 0 else "dim"
81-
table.add_row(sev.value.capitalize(), Text(str(count), style=style))
82-
console.print(table)
83-
84-
return console.export_text()
130+
if count > 0:
131+
color = _SEVERITY_COLORS.get(sev, "white")
132+
chevron = _SEVERITY_CHEVRONS.get(sev, "\u2771")
133+
console.print(
134+
f" [dim]\u2022[/dim] [{color}]{chevron} {sev.value.capitalize()}: {count}[/{color}]",
135+
highlight=False,
136+
)
137+
138+
console.print()
85139

86140

87141
FormatterFactory.register("console", ConsoleFormatter)

0 commit comments

Comments
 (0)