"""Output formatters for CLI.""" import json import sys from typing import Any, List import yaml from rich.console import Console from rich.table import Table from ..models import CPEData, CPEMatchString, CVEData, SourceData # Console for data output - explicitly use stdout console = Console(file=sys.stdout, stderr=False) def format_json(data: Any) -> None: """Format output as JSON.""" if hasattr(data, "model_dump"): output = data.model_dump(mode="json", exclude_none=True) else: output = data print(json.dumps(output, indent=2, default=str, ensure_ascii=False)) def format_json_lines(data: List[Any]) -> None: """Format multiple items as JSON lines (one per line).""" for item in data: if hasattr(item, "model_dump"): output = item.model_dump(mode="json", exclude_none=True) else: output = item print(json.dumps(output, default=str)) def format_yaml(data: Any) -> None: """Format output as YAML.""" if hasattr(data, "model_dump"): output = data.model_dump(mode="json", exclude_none=True) else: output = data print(yaml.dump(output, default_flow_style=False, sort_keys=False, allow_unicode=True), end="") def format_cve_table(cves: List[CVEData]) -> None: """Format CVEs as a table.""" table = Table(title="CVE Results", show_header=True, header_style="bold magenta") table.add_column("CVE ID", style="cyan", no_wrap=True) table.add_column("Published", style="green") table.add_column("CVSS v3", justify="right", style="yellow") table.add_column("Status", style="blue") table.add_column("Description", style="white", max_width=60) for cve in cves: score = cve.cvss_v3_score or cve.cvss_v2_score score_str = f"{score:.1f}" if score else "N/A" description = cve.description[:100] + "..." if len(cve.description) > 100 else cve.description table.add_row( cve.id, cve.published.strftime("%Y-%m-%d"), score_str, cve.vulnStatus, description, ) console.print(table) def format_cpe_table(cpes: List[CPEData]) -> None: """Format CPEs as a table.""" table = Table(title="CPE Results", show_header=True, header_style="bold magenta") table.add_column("CPE Name", style="cyan", no_wrap=True, max_width=50) table.add_column("Title", style="green", max_width=40) table.add_column("Deprecated", style="yellow") table.add_column("Last Modified", style="blue") for cpe in cpes: table.add_row( cpe.cpeName, cpe.title, "Yes" if cpe.deprecated else "No", cpe.lastModified.strftime("%Y-%m-%d"), ) console.print(table) def format_match_criteria_table(matches: List[CPEMatchString]) -> None: """Format CPE match criteria as a table.""" table = Table(title="CPE Match Criteria", show_header=True, header_style="bold magenta") table.add_column("Criteria", style="cyan", max_width=50) table.add_column("Status", style="green") table.add_column("Version Range", style="yellow", max_width=30) for match in matches: version_range = "" if match.versionStartIncluding: version_range += f">={match.versionStartIncluding} " if match.versionStartExcluding: version_range += f">{match.versionStartExcluding} " if match.versionEndIncluding: version_range += f"<={match.versionEndIncluding} " if match.versionEndExcluding: version_range += f"<{match.versionEndExcluding} " table.add_row( match.criteria, match.status, version_range.strip() or "All versions", ) console.print(table) def format_source_table(sources: List[SourceData]) -> None: """Format sources as a table.""" table = Table(title="Data Sources", show_header=True, header_style="bold magenta") table.add_column("Name", style="cyan") table.add_column("Contact Email", style="green") table.add_column("Identifiers", style="yellow", max_width=40) table.add_column("Created", style="blue") for source in sources: identifiers = ", ".join(source.sourceIdentifiers[:3]) if len(source.sourceIdentifiers) > 3: identifiers += f" (+{len(source.sourceIdentifiers) - 3} more)" table.add_row( source.name, source.contactEmail, identifiers, source.created.strftime("%Y-%m-%d"), ) console.print(table)