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

1from __future__ import annotations 

2 

3import json 

4from pathlib import Path 

5from typing import Any 

6 

7import typer 

8 

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 

22 

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") 

28 

29 

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 

44 

45 

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}") 

73 

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") 

79 

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}") 

83 

84 

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") 

93 

94 

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") 

103 

104 

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") 

113 

114 

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") 

123 

124 

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") 

133 

134 

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") 

143 

144 

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") 

153 

154 

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") 

163 

164 

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") 

173 

174 

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") 

183 

184 

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") 

193 

194 

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 

201 

202 run_server(transport=transport) 

203 

204 

205ASCII_BANNER = """ 

206 ____ _____ ____ ____ _____ ____ ___ ____ _____ _ _____ _____  

207/ ___|| ____/ ___| | _ \\| ____| _ \\ / _ \\| _ \\_ _| | |/ /_ _|_ _| 

208\\___ \\| _|| | | |_) | _| | |_) | | | | |_) || | | ' / | | | |  

209 ___) | |__| |___ | _ <| |___| __/| |_| | _ < | | | . \\ | | | |  

210|____/|_____|\\____| |_| \\_\\_____|_| \\___/|_| \\_\\|_| |_|\\_\\___| |_|  

211""" 

212 

213 

214def main() -> None: 

215 typer.echo(ASCII_BANNER) 

216 app() 

217 

218 

219if __name__ == "__main__": # pragma: no cover 

220 main()