Coverage for src \ sec_report_kit \ cli.py: 100%
110 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 08:06 +0530
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 08:06 +0530
1from __future__ import annotations
3import json
4from pathlib import Path
5from typing import Any
7import typer
9from sec_report_kit.parsers import detect_source_type
10from sec_report_kit.parsers.bandit import parse_bandit_json
11from sec_report_kit.parsers.checkov import parse_checkov_json
12from sec_report_kit.parsers.codeql import parse_codeql_json
13from sec_report_kit.parsers.gitleaks import parse_gitleaks_json
14from sec_report_kit.parsers.osv_scanner import parse_osv_scanner_json
15from sec_report_kit.parsers.pip_audit import parse_pip_audit_json
16from sec_report_kit.parsers.semgrep import parse_semgrep_json
17from sec_report_kit.parsers.tfsec import parse_tfsec_json
18from sec_report_kit.parsers.trivy import parse_trivy_json
19from sec_report_kit.parsers.trufflehog import parse_trufflehog_json
20from sec_report_kit.report.html_renderer import render_html_report
21from sec_report_kit.services.summarize import count_by_severity, sort_findings
23app = typer.Typer(help="Security report kit CLI")
24render_app = typer.Typer(help="Render HTML reports")
25mcp_app = typer.Typer(help="MCP server commands")
26app.add_typer(render_app, name="render")
27app.add_typer(mcp_app, name="mcp")
30def _load_json(path: Path) -> Any:
31 # Accept both UTF-8 and UTF-8 BOM encoded JSON files.
32 raw = path.read_text(encoding="utf-8-sig")
33 try:
34 return json.loads(raw)
35 except json.JSONDecodeError:
36 # Some scanners emit NDJSON (one JSON object per line).
37 items: list[dict[str, Any]] = []
38 for line in raw.splitlines():
39 line = line.strip()
40 if not line:
41 continue
42 items.append(json.loads(line))
43 return items
46def _write_report(source_label: str, target_ref: str, input_path: Path, output_path: Path, parser: str) -> None:
47 payload = _load_json(input_path)
48 if parser == "auto":
49 parser = detect_source_type(payload)
50 typer.echo(f"[INFO] Detected source type: {parser}")
51 if parser == "trivy":
52 findings = parse_trivy_json(payload)
53 elif parser == "pip-audit":
54 findings = parse_pip_audit_json(payload)
55 elif parser == "bandit":
56 findings = parse_bandit_json(payload)
57 elif parser == "gitleaks":
58 findings = parse_gitleaks_json(payload)
59 elif parser == "semgrep":
60 findings = parse_semgrep_json(payload)
61 elif parser == "codeql":
62 findings = parse_codeql_json(payload)
63 elif parser == "osv-scanner":
64 findings = parse_osv_scanner_json(payload)
65 elif parser == "checkov":
66 findings = parse_checkov_json(payload)
67 elif parser == "tfsec":
68 findings = parse_tfsec_json(payload)
69 elif parser == "trufflehog":
70 findings = parse_trufflehog_json(payload)
71 else:
72 raise typer.BadParameter(f"Unsupported parser: {parser}")
74 findings = sort_findings(findings)
75 counts = count_by_severity(findings)
76 report_html = render_html_report(target_ref=target_ref, source_label=source_label, findings=findings, counts=counts)
77 output_path.parent.mkdir(parents=True, exist_ok=True)
78 output_path.write_text(report_html, encoding="utf-8")
80 typer.echo(f"[OK] Report generated: {output_path}")
81 typer.echo(f"[INFO] Total findings: {len(findings)}")
82 typer.echo(f"[INFO] Severity counts: {counts}")
85@render_app.command("trivy")
86def render_trivy(
87 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
88 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
89 target: str = typer.Option(..., "--target", help="Scanned image or artifact reference"),
90) -> None:
91 """Render HTML report from Trivy JSON output."""
92 _write_report("trivy", target, input, output, parser="trivy")
95@render_app.command("pip-audit")
96def render_pip_audit(
97 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
98 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
99 target: str = typer.Option("python-environment", "--target", help="Python environment target label"),
100) -> None:
101 """Render HTML report from pip-audit JSON output."""
102 _write_report("pip-audit", target, input, output, parser="pip-audit")
105@render_app.command("auto")
106def render_auto(
107 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
108 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
109 target: str = typer.Option("unknown", "--target", help="Scanned image or artifact reference"),
110) -> None:
111 """Auto-detect input format (Trivy, pip-audit, Bandit, or Gitleaks) and render HTML report."""
112 _write_report("auto", target, input, output, parser="auto")
115@render_app.command("bandit")
116def render_bandit(
117 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
118 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
119 target: str = typer.Option("python-codebase", "--target", help="Codebase target label"),
120) -> None:
121 """Render HTML report from Bandit JSON output."""
122 _write_report("bandit", target, input, output, parser="bandit")
125@render_app.command("gitleaks")
126def render_gitleaks(
127 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
128 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
129 target: str = typer.Option("repository", "--target", help="Repository or scan target label"),
130) -> None:
131 """Render HTML report from Gitleaks JSON output."""
132 _write_report("gitleaks", target, input, output, parser="gitleaks")
135@render_app.command("semgrep")
136def render_semgrep(
137 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
138 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
139 target: str = typer.Option("repository", "--target", help="Repository or scan target label"),
140) -> None:
141 """Render HTML report from Semgrep JSON output."""
142 _write_report("semgrep", target, input, output, parser="semgrep")
145@render_app.command("codeql")
146def render_codeql(
147 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
148 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
149 target: str = typer.Option("repository", "--target", help="Repository or scan target label"),
150) -> None:
151 """Render HTML report from CodeQL SARIF JSON output."""
152 _write_report("codeql", target, input, output, parser="codeql")
155@render_app.command("osv-scanner")
156def render_osv_scanner(
157 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
158 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
159 target: str = typer.Option("dependency-manifest", "--target", help="Dependency manifest target label"),
160) -> None:
161 """Render HTML report from OSV-Scanner JSON output."""
162 _write_report("osv-scanner", target, input, output, parser="osv-scanner")
165@render_app.command("checkov")
166def render_checkov(
167 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
168 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
169 target: str = typer.Option("infrastructure-code", "--target", help="IaC scan target label"),
170) -> None:
171 """Render HTML report from Checkov JSON output."""
172 _write_report("checkov", target, input, output, parser="checkov")
175@render_app.command("tfsec")
176def render_tfsec(
177 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
178 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
179 target: str = typer.Option("infrastructure-code", "--target", help="IaC scan target label"),
180) -> None:
181 """Render HTML report from tfsec JSON output."""
182 _write_report("tfsec", target, input, output, parser="tfsec")
185@render_app.command("trufflehog")
186def render_trufflehog(
187 input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
188 output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
189 target: str = typer.Option("repository", "--target", help="Repository or scan target label"),
190) -> None:
191 """Render HTML report from TruffleHog JSON output."""
192 _write_report("trufflehog", target, input, output, parser="trufflehog")
195@mcp_app.command("serve")
196def serve_mcp(
197 transport: str = typer.Option("stdio", "--transport", help="MCP transport (currently stdio)")
198) -> None:
199 """Run the MCP server."""
200 from sec_report_kit.mcp.server import run_server
202 run_server(transport=transport)
205ASCII_BANNER = """
206 ____ _____ ____ ____ _____ ____ ___ ____ _____ _ _____ _____
207/ ___|| ____/ ___| | _ \\| ____| _ \\ / _ \\| _ \\_ _| | |/ /_ _|_ _|
208\\___ \\| _|| | | |_) | _| | |_) | | | | |_) || | | ' / | | | |
209 ___) | |__| |___ | _ <| |___| __/| |_| | _ < | | | . \\ | | | |
210|____/|_____|\\____| |_| \\_\\_____|_| \\___/|_| \\_\\|_| |_|\\_\\___| |_|
211"""
214def main() -> None:
215 typer.echo(ASCII_BANNER)
216 app()
219if __name__ == "__main__": # pragma: no cover
220 main()