sec-report-kit 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ """sec_report_kit package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from sec_report_kit.cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
sec_report_kit/cli.py ADDED
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from sec_report_kit.parsers import detect_source_type
9
+ from sec_report_kit.parsers.pip_audit import parse_pip_audit_json
10
+ from sec_report_kit.parsers.trivy import parse_trivy_json
11
+ from sec_report_kit.report.html_renderer import render_html_report
12
+ from sec_report_kit.services.summarize import count_by_severity, sort_findings
13
+
14
+ app = typer.Typer(help="Security report kit CLI")
15
+ render_app = typer.Typer(help="Render HTML reports")
16
+ mcp_app = typer.Typer(help="MCP server commands")
17
+ app.add_typer(render_app, name="render")
18
+ app.add_typer(mcp_app, name="mcp")
19
+
20
+
21
+ def _load_json(path: Path) -> dict:
22
+ return json.loads(path.read_text(encoding="utf-8"))
23
+
24
+
25
+ def _write_report(source_label: str, target_ref: str, input_path: Path, output_path: Path, parser: str) -> None:
26
+ payload = _load_json(input_path)
27
+ if parser == "auto":
28
+ parser = detect_source_type(payload)
29
+ typer.echo(f"[INFO] Detected source type: {parser}")
30
+ if parser == "trivy":
31
+ findings = parse_trivy_json(payload)
32
+ elif parser == "pip-audit":
33
+ findings = parse_pip_audit_json(payload)
34
+ else:
35
+ raise typer.BadParameter(f"Unsupported parser: {parser}")
36
+
37
+ findings = sort_findings(findings)
38
+ counts = count_by_severity(findings)
39
+ report_html = render_html_report(target_ref=target_ref, source_label=source_label, findings=findings, counts=counts)
40
+ output_path.parent.mkdir(parents=True, exist_ok=True)
41
+ output_path.write_text(report_html, encoding="utf-8")
42
+
43
+ typer.echo(f"[OK] Report generated: {output_path}")
44
+ typer.echo(f"[INFO] Total findings: {len(findings)}")
45
+ typer.echo(f"[INFO] Severity counts: {counts}")
46
+
47
+
48
+ @render_app.command("trivy")
49
+ def render_trivy(
50
+ input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
51
+ output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
52
+ target: str = typer.Option(..., "--target", help="Scanned image or artifact reference"),
53
+ ) -> None:
54
+ """Render HTML report from Trivy JSON output."""
55
+ _write_report("trivy", target, input, output, parser="trivy")
56
+
57
+
58
+ @render_app.command("pip-audit")
59
+ def render_pip_audit(
60
+ input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
61
+ output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
62
+ target: str = typer.Option("python-environment", "--target", help="Python environment target label"),
63
+ ) -> None:
64
+ """Render HTML report from pip-audit JSON output."""
65
+ _write_report("pip-audit", target, input, output, parser="pip-audit")
66
+
67
+
68
+ @render_app.command("auto")
69
+ def render_auto(
70
+ input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
71
+ output: Path = typer.Option(..., "--output", dir_okay=False, file_okay=True),
72
+ target: str = typer.Option("unknown", "--target", help="Scanned image or artifact reference"),
73
+ ) -> None:
74
+ """Auto-detect input format (Trivy or pip-audit) and render HTML report."""
75
+ _write_report("auto", target, input, output, parser="auto")
76
+
77
+
78
+ @mcp_app.command("serve")
79
+ def serve_mcp(
80
+ transport: str = typer.Option("stdio", "--transport", help="MCP transport (currently stdio)")
81
+ ) -> None:
82
+ """Run the MCP server."""
83
+ from sec_report_kit.mcp.server import run_server
84
+
85
+ run_server(transport=transport)
86
+
87
+
88
+ def main() -> None:
89
+ app()
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1 @@
1
+ """MCP server integration."""
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from sec_report_kit.parsers import detect_source_type
7
+ from sec_report_kit.parsers.pip_audit import parse_pip_audit_json
8
+ from sec_report_kit.parsers.trivy import parse_trivy_json
9
+ from sec_report_kit.report.html_renderer import render_html_report
10
+ from sec_report_kit.services.summarize import count_by_severity, sort_findings
11
+
12
+
13
+ def _load_payload(source_type: str, input_path: str):
14
+ data = json.loads(Path(input_path).read_text(encoding="utf-8"))
15
+ if source_type == "auto":
16
+ source_type = detect_source_type(data)
17
+ if source_type == "trivy":
18
+ return sort_findings(parse_trivy_json(data))
19
+ if source_type == "pip-audit":
20
+ return sort_findings(parse_pip_audit_json(data))
21
+ raise ValueError("source_type must be 'trivy', 'pip-audit', or 'auto'")
22
+
23
+
24
+ def build_server():
25
+ try:
26
+ from mcp.server.fastmcp import FastMCP
27
+ except Exception as exc: # pragma: no cover
28
+ raise RuntimeError(
29
+ "MCP dependencies are not installed. Install with: pip install sec-report-kit[mcp]"
30
+ ) from exc
31
+
32
+ mcp = FastMCP("sec-report-kit")
33
+
34
+ @mcp.tool()
35
+ def summarize_json(source_type: str, input_path: str) -> dict:
36
+ findings = _load_payload(source_type=source_type, input_path=input_path)
37
+ counts = count_by_severity(findings)
38
+ return {"total": len(findings), "counts": counts}
39
+
40
+ @mcp.tool()
41
+ def render_report_from_json(source_type: str, input_path: str, output_path: str, target: str) -> dict:
42
+ findings = _load_payload(source_type=source_type, input_path=input_path)
43
+ counts = count_by_severity(findings)
44
+ html = render_html_report(target_ref=target, source_label=source_type, findings=findings, counts=counts)
45
+ out = Path(output_path)
46
+ out.parent.mkdir(parents=True, exist_ok=True)
47
+ out.write_text(html, encoding="utf-8")
48
+ return {"output_path": str(out), "total": len(findings), "counts": counts}
49
+
50
+ @mcp.tool()
51
+ def validate_input(source_type: str, input_path: str) -> dict:
52
+ findings = _load_payload(source_type=source_type, input_path=input_path)
53
+ return {"valid": True, "total_findings": len(findings)}
54
+
55
+ return mcp
56
+
57
+
58
+ def run_server(transport: str = "stdio") -> None:
59
+ mcp = build_server()
60
+ if transport != "stdio":
61
+ raise ValueError("Only stdio transport is currently supported")
62
+ mcp.run(transport="stdio")
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Finding:
8
+ source_type: str
9
+ target: str
10
+ severity: str
11
+ vulnerability_id: str
12
+ package: str
13
+ installed_version: str
14
+ fixed_version: str
15
+ title: str
16
+ primary_url: str
@@ -0,0 +1,18 @@
1
+ """Input format parsers."""
2
+
3
+
4
+ def detect_source_type(data: dict) -> str:
5
+ """Detect whether *data* came from Trivy or pip-audit.
6
+
7
+ Returns ``"trivy"`` or ``"pip-audit"``.
8
+ Raises ``ValueError`` if the format cannot be recognised.
9
+ """
10
+ if isinstance(data, dict):
11
+ if "Results" in data:
12
+ return "trivy"
13
+ if "dependencies" in data or "vulnerabilities" in data:
14
+ return "pip-audit"
15
+ raise ValueError(
16
+ "Cannot detect source type: JSON does not match any known format "
17
+ "(expected 'Results' for Trivy or 'dependencies'/'vulnerabilities' for pip-audit)."
18
+ )
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from sec_report_kit.models import Finding
4
+ from sec_report_kit.services.normalize import normalize_severity
5
+
6
+
7
+ def _best_url(vuln: dict) -> str:
8
+ if vuln.get("url"):
9
+ return str(vuln["url"])
10
+ if vuln.get("advisory"):
11
+ return str(vuln["advisory"])
12
+ if vuln.get("links") and isinstance(vuln["links"], list):
13
+ return str(vuln["links"][0]) if vuln["links"] else ""
14
+ return ""
15
+
16
+
17
+ def _fixed_version(vuln: dict) -> str:
18
+ fixes = vuln.get("fix_versions")
19
+ if isinstance(fixes, list) and fixes:
20
+ return ", ".join(str(value) for value in fixes)
21
+ fixed = vuln.get("fixed_version")
22
+ return str(fixed) if fixed else "-"
23
+
24
+
25
+ def parse_pip_audit_json(data: dict) -> list[Finding]:
26
+ findings: list[Finding] = []
27
+
28
+ dependencies = data.get("dependencies", []) if isinstance(data, dict) else []
29
+ for dep in dependencies:
30
+ package = dep.get("name", "-")
31
+ installed = dep.get("version", "-")
32
+ for vuln in dep.get("vulns", []) or []:
33
+ findings.append(
34
+ Finding(
35
+ source_type="python-pkg",
36
+ target="Python",
37
+ severity=normalize_severity(vuln.get("severity")),
38
+ vulnerability_id=str(vuln.get("id") or "-"),
39
+ package=str(package),
40
+ installed_version=str(installed),
41
+ fixed_version=_fixed_version(vuln),
42
+ title=str(vuln.get("description") or vuln.get("summary") or "-"),
43
+ primary_url=_best_url(vuln),
44
+ )
45
+ )
46
+
47
+ # Some tools may output a flat list under "vulnerabilities"; support that too.
48
+ if not findings and isinstance(data, dict) and isinstance(data.get("vulnerabilities"), list):
49
+ for vuln in data["vulnerabilities"]:
50
+ findings.append(
51
+ Finding(
52
+ source_type="python-pkg",
53
+ target="Python",
54
+ severity=normalize_severity(vuln.get("severity")),
55
+ vulnerability_id=str(vuln.get("id") or "-"),
56
+ package=str(vuln.get("package") or "-"),
57
+ installed_version=str(vuln.get("installed_version") or "-"),
58
+ fixed_version=_fixed_version(vuln),
59
+ title=str(vuln.get("description") or vuln.get("summary") or "-"),
60
+ primary_url=_best_url(vuln),
61
+ )
62
+ )
63
+
64
+ return findings
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from sec_report_kit.models import Finding
4
+ from sec_report_kit.services.normalize import normalize_severity
5
+
6
+
7
+ def parse_trivy_json(data: dict) -> list[Finding]:
8
+ findings: list[Finding] = []
9
+ for result in data.get("Results", []):
10
+ target = result.get("Target", "unknown")
11
+ source_type = result.get("Type", "unknown")
12
+ for vuln in result.get("Vulnerabilities", []) or []:
13
+ findings.append(
14
+ Finding(
15
+ source_type=source_type,
16
+ target=target,
17
+ severity=normalize_severity(vuln.get("Severity")),
18
+ vulnerability_id=vuln.get("VulnerabilityID", "-"),
19
+ package=vuln.get("PkgName", "-"),
20
+ installed_version=vuln.get("InstalledVersion", "-"),
21
+ fixed_version=vuln.get("FixedVersion") or "-",
22
+ title=vuln.get("Title") or "-",
23
+ primary_url=vuln.get("PrimaryURL") or "",
24
+ )
25
+ )
26
+ return findings
@@ -0,0 +1 @@
1
+ """Report renderers."""
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ import html
5
+
6
+ from sec_report_kit.models import Finding
7
+
8
+
9
+ def _esc(value: str) -> str:
10
+ return html.escape(value, quote=True)
11
+
12
+
13
+ def render_html_report(target_ref: str, source_label: str, findings: list[Finding], counts: dict[str, int]) -> str:
14
+ generated_at = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
15
+ total = len(findings)
16
+
17
+ rows = []
18
+ for item in findings:
19
+ vuln_id = _esc(item.vulnerability_id)
20
+ if item.primary_url:
21
+ vuln_cell = (
22
+ f'<a href="{_esc(item.primary_url)}" target="_blank" rel="noopener noreferrer">{vuln_id}</a>'
23
+ )
24
+ else:
25
+ vuln_cell = vuln_id
26
+
27
+ rows.append(
28
+ "<tr>"
29
+ f"<td class=\"sev {_esc(item.severity).lower()}\">{_esc(item.severity)}</td>"
30
+ f"<td>{vuln_cell}</td>"
31
+ f"<td>{_esc(item.package)}</td>"
32
+ f"<td>{_esc(item.installed_version)}</td>"
33
+ f"<td>{_esc(item.fixed_version)}</td>"
34
+ f"<td>{_esc(item.title)}</td>"
35
+ f"<td>{_esc(item.target)}</td>"
36
+ f"<td>{_esc(item.source_type)}</td>"
37
+ "</tr>"
38
+ )
39
+
40
+ rows_html = "\n".join(rows) if rows else "<tr><td colspan=\"8\">No vulnerabilities found.</td></tr>"
41
+
42
+ return f"""<!doctype html>
43
+ <html lang=\"en\">
44
+ <head>
45
+ <meta charset=\"utf-8\" />
46
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
47
+ <title>Security Report</title>
48
+ <style>
49
+ body {{ font-family: Segoe UI, Arial, sans-serif; margin: 24px; color: #1f2937; }}
50
+ h1 {{ margin: 0 0 8px 0; }}
51
+ .meta {{ margin-bottom: 18px; color: #4b5563; }}
52
+ .cards {{ display: grid; grid-template-columns: repeat(6, minmax(110px, 1fr)); gap: 10px; margin-bottom: 18px; }}
53
+ .card {{ border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #f9fafb; }}
54
+ .label {{ font-size: 12px; color: #6b7280; text-transform: uppercase; }}
55
+ .value {{ font-size: 24px; font-weight: 700; margin-top: 4px; }}
56
+ .filters {{ display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; align-items: center; }}
57
+ .filters label {{ font-size: 13px; color: #374151; display: flex; flex-direction: column; gap: 3px; }}
58
+ .filters select, .filters input[type=text] {{
59
+ font-size: 13px; padding: 5px 8px; border: 1px solid #d1d5db;
60
+ border-radius: 6px; background: #fff; color: #1f2937; min-width: 140px;
61
+ }}
62
+ .filters input[type=text] {{ min-width: 220px; }}
63
+ .filters button {{
64
+ font-size: 13px; padding: 6px 14px; border: 1px solid #d1d5db;
65
+ border-radius: 6px; background: #f3f4f6; cursor: pointer; color: #374151;
66
+ align-self: flex-end;
67
+ }}
68
+ .filters button:hover {{ background: #e5e7eb; }}
69
+ #result-count {{ font-size: 13px; color: #6b7280; align-self: flex-end; margin-left: auto; }}
70
+ table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
71
+ thead th {{ text-align: left; border-bottom: 2px solid #d1d5db; padding: 8px; background: #f3f4f6; position: sticky; top: 0; }}
72
+ tbody td {{ border-bottom: 1px solid #e5e7eb; padding: 8px; vertical-align: top; }}
73
+ tbody tr.hidden {{ display: none; }}
74
+ .sev {{ font-weight: 700; }}
75
+ .critical {{ color: #b91c1c; }}
76
+ .high {{ color: #c2410c; }}
77
+ .medium {{ color: #b45309; }}
78
+ .low {{ color: #1d4ed8; }}
79
+ .unknown {{ color: #6b7280; }}
80
+ a {{ color: #1d4ed8; text-decoration: none; }}
81
+ a:hover {{ text-decoration: underline; }}
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <h1>Security Vulnerability Report</h1>
86
+ <div class=\"meta\">Source: <strong>{_esc(source_label)}</strong> | Target: <strong>{_esc(target_ref)}</strong> | Generated: {_esc(generated_at)}</div>
87
+
88
+ <div class=\"cards\">
89
+ <div class=\"card\"><div class=\"label\">Total</div><div class=\"value\">{total}</div></div>
90
+ <div class=\"card\"><div class=\"label\">Critical</div><div class=\"value critical\">{counts['CRITICAL']}</div></div>
91
+ <div class=\"card\"><div class=\"label\">High</div><div class=\"value high\">{counts['HIGH']}</div></div>
92
+ <div class=\"card\"><div class=\"label\">Medium</div><div class=\"value medium\">{counts['MEDIUM']}</div></div>
93
+ <div class=\"card\"><div class=\"label\">Low</div><div class=\"value low\">{counts['LOW']}</div></div>
94
+ <div class=\"card\"><div class=\"label\">Unknown</div><div class=\"value unknown\">{counts['UNKNOWN']}</div></div>
95
+ </div>
96
+
97
+ <div class=\"filters\">
98
+ <label>Severity
99
+ <select id=\"f-severity\">
100
+ <option value=\"\">All</option>
101
+ <option value=\"critical\">Critical</option>
102
+ <option value=\"high\">High</option>
103
+ <option value=\"medium\">Medium</option>
104
+ <option value=\"low\">Low</option>
105
+ <option value=\"unknown\">Unknown</option>
106
+ </select>
107
+ </label>
108
+ <label>Vulnerability ID
109
+ <input type=\"text\" id=\"f-vuln\" placeholder=\"e.g. CVE-2024-\" />
110
+ </label>
111
+ <label>Package
112
+ <input type=\"text\" id=\"f-pkg\" placeholder=\"e.g. openssl\" />
113
+ </label>
114
+ <label>Target
115
+ <input type=\"text\" id=\"f-target\" placeholder=\"e.g. usr/lib\" />
116
+ </label>
117
+ <label>Type
118
+ <input type=\"text\" id=\"f-type\" placeholder=\"e.g. os-pkgs\" />
119
+ </label>
120
+ <label>Title / keyword
121
+ <input type=\"text\" id=\"f-title\" placeholder=\"e.g. buffer overflow\" />
122
+ </label>
123
+ <button onclick=\"clearFilters()\">Clear</button>
124
+ <span id=\"result-count\"></span>
125
+ </div>
126
+
127
+ <table id=\"vuln-table\">
128
+ <thead>
129
+ <tr>
130
+ <th>Severity</th>
131
+ <th>Vulnerability</th>
132
+ <th>Package</th>
133
+ <th>Installed</th>
134
+ <th>Fixed</th>
135
+ <th>Title</th>
136
+ <th>Target</th>
137
+ <th>Type</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody id=\"vuln-body\">
141
+ {rows_html}
142
+ </tbody>
143
+ </table>
144
+
145
+ <script>
146
+ const filterIds = ['f-severity', 'f-vuln', 'f-pkg', 'f-target', 'f-type', 'f-title'];
147
+ // col indices: severity=0, vuln=1, pkg=2, installed=3, fixed=4, title=5, target=6, type=7
148
+ const colMap = {{ 'f-severity': 0, 'f-vuln': 1, 'f-pkg': 2, 'f-target': 6, 'f-type': 7, 'f-title': 5 }};
149
+
150
+ function applyFilters() {{
151
+ const filters = {{}};
152
+ filterIds.forEach(id => {{
153
+ const el = document.getElementById(id);
154
+ filters[id] = el.value.trim().toLowerCase();
155
+ }});
156
+
157
+ const rows = document.querySelectorAll('#vuln-body tr');
158
+ let visible = 0;
159
+ rows.forEach(row => {{
160
+ const cells = row.querySelectorAll('td');
161
+ if (!cells.length) return;
162
+ let show = true;
163
+ for (const [id, col] of Object.entries(colMap)) {{
164
+ const val = filters[id];
165
+ if (!val) continue;
166
+ const cellText = (cells[col]?.textContent || '').trim().toLowerCase();
167
+ if (id === 'f-severity') {{
168
+ if (cellText !== val) {{ show = false; break; }}
169
+ }} else {{
170
+ if (!cellText.includes(val)) {{ show = false; break; }}
171
+ }}
172
+ }}
173
+ row.classList.toggle('hidden', !show);
174
+ if (show) visible++;
175
+ }});
176
+
177
+ const total = rows.length;
178
+ document.getElementById('result-count').textContent =
179
+ visible === total ? `${{total}} rows` : `${{visible}} of ${{total}} rows`;
180
+ }}
181
+
182
+ function clearFilters() {{
183
+ filterIds.forEach(id => {{ document.getElementById(id).value = ''; }});
184
+ applyFilters();
185
+ }}
186
+
187
+ filterIds.forEach(id => {{
188
+ const el = document.getElementById(id);
189
+ el.addEventListener('input', applyFilters);
190
+ el.addEventListener('change', applyFilters);
191
+ }});
192
+
193
+ applyFilters();
194
+ </script>
195
+
196
+ <footer style="margin-top: 40px; padding-top: 16px; border-top: 1px solid #e5e7eb; text-align: center; font-size: 12px; color: #9ca3af;">
197
+ Designed and developed by
198
+ <strong><a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer" style="color: #0a66c2; text-decoration: none;">Shan Konduru</a></strong>
199
+ &nbsp;|&nbsp;
200
+ <a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer" style="color: #0a66c2; text-decoration: none;">LinkedIn</a>
201
+ &nbsp;&bull;&nbsp;
202
+ <a href="https://github.com/ShanKonduru" target="_blank" rel="noopener noreferrer" style="color: #1f2937; text-decoration: none;">GitHub</a>
203
+ </footer>
204
+ </body>
205
+ </html>
206
+ """
@@ -0,0 +1 @@
1
+ """Normalization and summarization services."""
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ SEVERITY_ORDER = {
4
+ "CRITICAL": 0,
5
+ "HIGH": 1,
6
+ "MEDIUM": 2,
7
+ "LOW": 3,
8
+ "UNKNOWN": 4,
9
+ }
10
+
11
+
12
+ def normalize_severity(value: str | None) -> str:
13
+ if not value:
14
+ return "UNKNOWN"
15
+ upper = value.upper()
16
+ return upper if upper in SEVERITY_ORDER else "UNKNOWN"
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from sec_report_kit.models import Finding
6
+ from sec_report_kit.services.normalize import SEVERITY_ORDER
7
+
8
+
9
+ def sort_findings(findings: Iterable[Finding]) -> list[Finding]:
10
+ return sorted(
11
+ findings,
12
+ key=lambda item: (
13
+ SEVERITY_ORDER[item.severity],
14
+ item.package.lower(),
15
+ item.vulnerability_id.lower(),
16
+ ),
17
+ )
18
+
19
+
20
+ def count_by_severity(findings: Iterable[Finding]) -> dict[str, int]:
21
+ counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
22
+ for finding in findings:
23
+ counts[finding.severity] += 1
24
+ return counts
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: sec-report-kit
3
+ Version: 0.1.2
4
+ Summary: Generate HTML security reports from Trivy and pip-audit JSON
5
+ Author: Ravi Konduru
6
+ License: MIT
7
+ Keywords: cli,mcp,pip-audit,report,security,trivy
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: typer>=0.12.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: build>=1.2.0; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
22
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
23
+ Provides-Extra: mcp
24
+ Requires-Dist: mcp>=1.0.0; extra == 'mcp'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # sec-report-kit
28
+
29
+ Generate HTML vulnerability reports from Trivy and pip-audit JSON with a CLI and MCP server.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install -e .
35
+ ```
36
+
37
+ With MCP support:
38
+
39
+ ```bash
40
+ pip install -e .[mcp]
41
+ ```
42
+
43
+ ## CLI Usage
44
+
45
+ Both commands are available:
46
+
47
+ - `srk`
48
+ - `sec-report-kit`
49
+
50
+ Render Trivy JSON:
51
+
52
+ ```bash
53
+ srk render trivy --input security_reports/trivy-image-report-v1.0.21.json --output security_reports/report-trivy.html --target shankonduru/cpkc-poc:v1.0.21
54
+ ```
55
+
56
+ Render pip-audit JSON:
57
+
58
+ ```bash
59
+ srk render pip-audit --input pip-audit.json --output security_reports/report-pip-audit.html --target requirements.txt
60
+ ```
61
+
62
+ ## MCP Server
63
+
64
+ Run MCP server over stdio:
65
+
66
+ ```bash
67
+ srk mcp serve --transport stdio
68
+ ```
69
+
70
+ ### Available MCP Tools
71
+
72
+ | Tool | Description |
73
+ |---|---|
74
+ | `summarize_json` | Summarize vulnerabilities by severity from a JSON file |
75
+ | `render_report_from_json` | Parse JSON and render an HTML report to disk |
76
+ | `validate_input` | Validate that a JSON file is parseable and return finding count |
77
+
78
+ All tools accept `source_type` (`"trivy"` or `"pip-audit"`) and `input_path` (absolute path to JSON file).
79
+
80
+ ---
81
+
82
+ ### VS Code (GitHub Copilot Agent / MCP extension)
83
+
84
+ Add to your VS Code `settings.json` (or `.vscode/mcp.json` in the workspace):
85
+
86
+ ```json
87
+ {
88
+ "mcp": {
89
+ "servers": {
90
+ "sec-report-kit": {
91
+ "type": "stdio",
92
+ "command": "srk",
93
+ "args": ["mcp", "serve", "--transport", "stdio"]
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ > **Note:** If `srk` is not on the system PATH, replace `"command"` with the full path to the executable, e.g. `"C:/Users/you/.venv/Scripts/srk.exe"`.
101
+
102
+ ---
103
+
104
+ ### Claude Desktop
105
+
106
+ Edit `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "sec-report-kit": {
112
+ "command": "srk",
113
+ "args": ["mcp", "serve", "--transport", "stdio"]
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ### Cursor
122
+
123
+ Open **Cursor Settings → MCP** and add a new server entry:
124
+
125
+ ```json
126
+ {
127
+ "sec-report-kit": {
128
+ "command": "srk",
129
+ "args": ["mcp", "serve", "--transport", "stdio"]
130
+ }
131
+ }
132
+ ```
133
+
134
+ Or add it to `.cursor/mcp.json` in your project root:
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "sec-report-kit": {
140
+ "command": "srk",
141
+ "args": ["mcp", "serve", "--transport", "stdio"]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ### Windsurf (Codeium)
150
+
151
+ Edit `~/.codeium/windsurf/mcp_config.json`:
152
+
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "sec-report-kit": {
157
+ "command": "srk",
158
+ "args": ["mcp", "serve", "--transport", "stdio"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ---
165
+
166
+ ### Using a virtual environment
167
+
168
+ If the package is installed in a `.venv`, use the full path to avoid PATH issues:
169
+
170
+ **Windows:**
171
+ ```json
172
+ {
173
+ "command": "C:/MyProjects/sec-report-kit/.venv/Scripts/srk.exe",
174
+ "args": ["mcp", "serve", "--transport", "stdio"]
175
+ }
176
+ ```
177
+
178
+ **macOS / Linux:**
179
+ ```json
180
+ {
181
+ "command": "/home/user/sec-report-kit/.venv/bin/srk",
182
+ "args": ["mcp", "serve", "--transport", "stdio"]
183
+ }
184
+ ```
185
+
186
+ ## Package Publish
187
+
188
+ Build:
189
+
190
+ ```bash
191
+ python -m build
192
+ ```
193
+
194
+ Upload to TestPyPI:
195
+
196
+ ```bash
197
+ python -m twine upload --repository testpypi dist/*
198
+ ```
199
+
200
+ Upload to PyPI:
201
+
202
+ ```bash
203
+ python -m twine upload dist/*
204
+ ```
@@ -0,0 +1,18 @@
1
+ sec_report_kit/__init__.py,sha256=butvkt3RGg7a4NtMzx2wVfE2zTZ9NCMRso8hy2KjKrc,79
2
+ sec_report_kit/__main__.py,sha256=CNS9ncbqlLuigWe5YRQwvWy2pfhF00MaxMcgcGBBpz4,76
3
+ sec_report_kit/cli.py,sha256=EdKhx5IGg9zQUjPyHxftTY8h5fYUwxrNfq5wqAxDrhA,3524
4
+ sec_report_kit/models.py,sha256=v0okbfQyLv9ywZ3Gi2pFi4RYbIWcy2sNs8dLknjN_i4,295
5
+ sec_report_kit/mcp/__init__.py,sha256=xTYXpAoPsb-41eqtt7rvl-tcTSXIG9s9-3gp4pe_WL8,30
6
+ sec_report_kit/mcp/server.py,sha256=aqEELFVtdigJjJfXtbemuG17XPunE-L_ZQ-VqN42kVM,2459
7
+ sec_report_kit/parsers/__init__.py,sha256=lLUe3gagaQ0N4fU_wB7J0scGD2YSyTB9kihZMLzfN2c,625
8
+ sec_report_kit/parsers/pip_audit.py,sha256=bswVv83q957aNsC2vR-iqw4N5EnIdq4iGzcTGPlvrfs,2504
9
+ sec_report_kit/parsers/trivy.py,sha256=43G3pSDEa-xLJL8m6jo1q4s01Jwd_uBtPZDqzZGfrCc,1070
10
+ sec_report_kit/report/__init__.py,sha256=bmQ3dGBiHqSPwumamEebbTyZSdWgUDms_jPjo1MBB0Y,24
11
+ sec_report_kit/report/html_renderer.py,sha256=FTxEKFfPYB-qxIsK4ZAlnZ3TxBbTf67ucOjt2WURgpk,8348
12
+ sec_report_kit/services/__init__.py,sha256=4eRbOiuQROzQwRWauSbyfaZ6y9sstmw7j0rSzxB49Jk,48
13
+ sec_report_kit/services/normalize.py,sha256=jG1yG8eNueu2oEd5vqgU25guCS-y4Bf5TI6-EFt5l1w,320
14
+ sec_report_kit/services/summarize.py,sha256=N5U7qwzkLXtfMG4_J-BXr-2X8vYGo1ynZfuqwXCvSUQ,679
15
+ sec_report_kit-0.1.2.dist-info/METADATA,sha256=kAriVYobqZHbM-y0KpC53gZe1fFUDAAD-TPoacNZ8v0,4119
16
+ sec_report_kit-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ sec_report_kit-0.1.2.dist-info/entry_points.txt,sha256=FfuWDFya9VntP5bB6KOgXOKg6RvJQ0wo1CzvZBgfJsY,89
18
+ sec_report_kit-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ sec-report-kit = sec_report_kit.cli:main
3
+ srk = sec_report_kit.cli:main