aboutsummaryrefslogtreecommitdiffstats
path: root/src/nvd/cli/formatters.py
blob: fa95e9e49ad75a0866ce99650de260a13eac30cf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""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)