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.
- sec_report_kit/__init__.py +4 -0
- sec_report_kit/__main__.py +5 -0
- sec_report_kit/cli.py +93 -0
- sec_report_kit/mcp/__init__.py +1 -0
- sec_report_kit/mcp/server.py +62 -0
- sec_report_kit/models.py +16 -0
- sec_report_kit/parsers/__init__.py +18 -0
- sec_report_kit/parsers/pip_audit.py +64 -0
- sec_report_kit/parsers/trivy.py +26 -0
- sec_report_kit/report/__init__.py +1 -0
- sec_report_kit/report/html_renderer.py +206 -0
- sec_report_kit/services/__init__.py +1 -0
- sec_report_kit/services/normalize.py +16 -0
- sec_report_kit/services/summarize.py +24 -0
- sec_report_kit-0.1.2.dist-info/METADATA +204 -0
- sec_report_kit-0.1.2.dist-info/RECORD +18 -0
- sec_report_kit-0.1.2.dist-info/WHEEL +4 -0
- sec_report_kit-0.1.2.dist-info/entry_points.txt +3 -0
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")
|
sec_report_kit/models.py
ADDED
|
@@ -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
|
+
|
|
|
200
|
+
<a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer" style="color: #0a66c2; text-decoration: none;">LinkedIn</a>
|
|
201
|
+
•
|
|
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,,
|