Add source code files
This commit is contained in:
99
src/codeguard/utils/output.py
Normal file
99
src/codeguard/utils/output.py
Normal file
@@ -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),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user