diff --git a/src/codeguard/utils/output.py b/src/codeguard/utils/output.py new file mode 100644 index 0000000..ffcfcf5 --- /dev/null +++ b/src/codeguard/utils/output.py @@ -0,0 +1,99 @@ +"""Output formatting for CodeGuard.""" + +from typing import Any +from codeguard.core.models import Finding, Severity +from rich.console import Console +from rich.table import Table +from rich.text import Text + + +class OutputFormatter: + SEVERITY_COLORS = { + Severity.CRITICAL: "red", + Severity.HIGH: "orange1", + Severity.MEDIUM: "yellow", + Severity.LOW: "green", + Severity.INFO: "blue", + } + + def __init__(self): + self.console = Console() + + @staticmethod + def print(findings: list[Finding], output_format: str = "text") -> None: + if output_format == "json": + print(OutputFormatter.to_json(findings)) + else: + OutputFormatter._print_text(findings) + + @staticmethod + def _print_text(findings: list[Finding]) -> None: + if not findings: + print("No security issues found.") + return + + console = Console() + + table = Table(title="CodeGuard Security Findings") + table.add_column("Severity", width=10) + table.add_column("File", width=30) + table.add_column("Line", width=6) + table.add_column("Issue", width=40) + table.add_column("Type", width=15) + + for finding in findings: + severity_text = Text(finding.severity.value.upper()) + severity_text.stylize(f"bold {OutputFormatter.SEVERITY_COLORS.get(finding.severity, 'white')}") + + file_short = finding.location.file + if len(file_short) > 28: + file_short = "..." + file_short[-28:] + + table.add_row( + severity_text, + file_short, + str(finding.location.line), + finding.title[:38], + finding.type.value, + ) + + console.print(table) + + summary = OutputFormatter._get_summary(findings) + console.print(f"\nSummary: {summary}") + + @staticmethod + def to_json(findings: list[Finding]) -> str: + import json + output = { + "findings": [f.model_dump(mode="json") for f in findings], + "summary": OutputFormatter._get_summary(findings), + } + return json.dumps(output, indent=2) + + @staticmethod + def to_yaml(findings: list[Finding]) -> str: + import yaml + output = { + "findings": [f.model_dump(mode="json") for f in findings], + "summary": OutputFormatter._get_summary(findings), + } + return yaml.dump(output) + + @staticmethod + def _get_summary(findings: list[Finding]) -> dict[str, Any]: + severity_counts: dict[str, int] = {} + type_counts: dict[str, int] = {} + files_affected = set() + + for f in findings: + severity_counts[f.severity.value] = severity_counts.get(f.severity.value, 0) + 1 + type_counts[f.type.value] = type_counts.get(f.type.value, 0) + 1 + files_affected.add(f.location.file) + + return { + "total": len(findings), + "by_severity": severity_counts, + "by_type": type_counts, + "files_affected": len(files_affected), + }