sec-report-kit 0.1.2__tar.gz

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,89 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ verify-version:
13
+ name: Verify tag matches package version
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ token: ${{ secrets.REPO_ACCESS_TOKEN || github.token }}
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Extract package version from pyproject.toml
25
+ id: pkg_version
26
+ run: |
27
+ VERSION=$(python -c "
28
+ import tomllib
29
+ with open('pyproject.toml', 'rb') as f:
30
+ data = tomllib.load(f)
31
+ print(data['project']['version'])
32
+ ")
33
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
34
+
35
+ - name: Check tag matches package version
36
+ run: |
37
+ TAG="${GITHUB_REF_NAME}" # e.g. v0.1.0
38
+ PKG="v${{ steps.pkg_version.outputs.version }}" # e.g. v0.1.0
39
+ echo "Git tag : $TAG"
40
+ echo "Package : $PKG"
41
+ if [ "$TAG" != "$PKG" ]; then
42
+ echo "ERROR: git tag '$TAG' does not match package version '$PKG'"
43
+ exit 1
44
+ fi
45
+
46
+ test:
47
+ name: Run tests
48
+ needs: verify-version
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ with:
53
+ token: ${{ secrets.REPO_ACCESS_TOKEN || github.token }}
54
+
55
+ - uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.12"
58
+
59
+ - name: Install dev dependencies
60
+ run: pip install -e ".[mcp,dev]"
61
+
62
+ - name: Run tests
63
+ run: pytest
64
+
65
+ publish-pypi:
66
+ name: Build & publish to PyPI
67
+ needs: test
68
+ runs-on: ubuntu-latest
69
+ environment: pypi
70
+ permissions:
71
+ id-token: write # required for OIDC trusted publishing
72
+
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ with:
76
+ token: ${{ secrets.REPO_ACCESS_TOKEN || github.token }}
77
+
78
+ - uses: actions/setup-python@v5
79
+ with:
80
+ python-version: "3.12"
81
+
82
+ - name: Install build tools
83
+ run: pip install build
84
+
85
+ - name: Build distribution
86
+ run: python -m build
87
+
88
+ - name: Publish to PyPI
89
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,57 @@
1
+ name: Publish to TestPyPI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "**"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: Run tests
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ token: ${{ secrets.REPO_ACCESS_TOKEN || github.token }}
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install dev dependencies
25
+ run: pip install -e ".[mcp,dev]"
26
+
27
+ - name: Run tests
28
+ run: pytest
29
+
30
+ publish-testpypi:
31
+ name: Build & publish to TestPyPI
32
+ needs: test
33
+ runs-on: ubuntu-latest
34
+ environment: testpypi
35
+ permissions:
36
+ id-token: write # required for OIDC trusted publishing
37
+
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ with:
41
+ token: ${{ secrets.REPO_ACCESS_TOKEN || github.token }}
42
+
43
+ - uses: actions/setup-python@v5
44
+ with:
45
+ python-version: "3.12"
46
+
47
+ - name: Install build tools
48
+ run: pip install build
49
+
50
+ - name: Build distribution
51
+ run: python -m build
52
+
53
+ - name: Publish to TestPyPI
54
+ uses: pypa/gh-action-pypi-publish@release/v1
55
+ with:
56
+ repository-url: https://test.pypi.org/legacy/
57
+ skip-existing: true
@@ -0,0 +1,85 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .nox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ *.py,cover
48
+ .pytest_cache/
49
+
50
+ # Type checkers / analyzers
51
+ .mypy_cache/
52
+ .dmypy.json
53
+ dmypy.json
54
+ .pyre/
55
+ .pytype/
56
+ .ruff_cache/
57
+
58
+ # Environments
59
+ .env
60
+ .env.*
61
+ !.env.sample
62
+ .venv/
63
+ venv/
64
+ env/
65
+ ENV/
66
+ env.bak/
67
+ venv.bak/
68
+
69
+ # Notebook artifacts
70
+ .ipynb_checkpoints/
71
+
72
+ # IDE / editor
73
+ .vscode/
74
+ .idea/
75
+ *.swp
76
+ *.swo
77
+
78
+ # OS files
79
+ .DS_Store
80
+ Thumbs.db
81
+
82
+ # Project-local runtime data
83
+ data/
84
+ logs/
85
+ security_reports/
@@ -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,178 @@
1
+ # sec-report-kit
2
+
3
+ Generate HTML vulnerability reports from Trivy and pip-audit JSON with a CLI and MCP server.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ With MCP support:
12
+
13
+ ```bash
14
+ pip install -e .[mcp]
15
+ ```
16
+
17
+ ## CLI Usage
18
+
19
+ Both commands are available:
20
+
21
+ - `srk`
22
+ - `sec-report-kit`
23
+
24
+ Render Trivy JSON:
25
+
26
+ ```bash
27
+ 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
28
+ ```
29
+
30
+ Render pip-audit JSON:
31
+
32
+ ```bash
33
+ srk render pip-audit --input pip-audit.json --output security_reports/report-pip-audit.html --target requirements.txt
34
+ ```
35
+
36
+ ## MCP Server
37
+
38
+ Run MCP server over stdio:
39
+
40
+ ```bash
41
+ srk mcp serve --transport stdio
42
+ ```
43
+
44
+ ### Available MCP Tools
45
+
46
+ | Tool | Description |
47
+ |---|---|
48
+ | `summarize_json` | Summarize vulnerabilities by severity from a JSON file |
49
+ | `render_report_from_json` | Parse JSON and render an HTML report to disk |
50
+ | `validate_input` | Validate that a JSON file is parseable and return finding count |
51
+
52
+ All tools accept `source_type` (`"trivy"` or `"pip-audit"`) and `input_path` (absolute path to JSON file).
53
+
54
+ ---
55
+
56
+ ### VS Code (GitHub Copilot Agent / MCP extension)
57
+
58
+ Add to your VS Code `settings.json` (or `.vscode/mcp.json` in the workspace):
59
+
60
+ ```json
61
+ {
62
+ "mcp": {
63
+ "servers": {
64
+ "sec-report-kit": {
65
+ "type": "stdio",
66
+ "command": "srk",
67
+ "args": ["mcp", "serve", "--transport", "stdio"]
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ > **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"`.
75
+
76
+ ---
77
+
78
+ ### Claude Desktop
79
+
80
+ Edit `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "sec-report-kit": {
86
+ "command": "srk",
87
+ "args": ["mcp", "serve", "--transport", "stdio"]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ### Cursor
96
+
97
+ Open **Cursor Settings → MCP** and add a new server entry:
98
+
99
+ ```json
100
+ {
101
+ "sec-report-kit": {
102
+ "command": "srk",
103
+ "args": ["mcp", "serve", "--transport", "stdio"]
104
+ }
105
+ }
106
+ ```
107
+
108
+ Or add it to `.cursor/mcp.json` in your project root:
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "sec-report-kit": {
114
+ "command": "srk",
115
+ "args": ["mcp", "serve", "--transport", "stdio"]
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ### Windsurf (Codeium)
124
+
125
+ Edit `~/.codeium/windsurf/mcp_config.json`:
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "sec-report-kit": {
131
+ "command": "srk",
132
+ "args": ["mcp", "serve", "--transport", "stdio"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ### Using a virtual environment
141
+
142
+ If the package is installed in a `.venv`, use the full path to avoid PATH issues:
143
+
144
+ **Windows:**
145
+ ```json
146
+ {
147
+ "command": "C:/MyProjects/sec-report-kit/.venv/Scripts/srk.exe",
148
+ "args": ["mcp", "serve", "--transport", "stdio"]
149
+ }
150
+ ```
151
+
152
+ **macOS / Linux:**
153
+ ```json
154
+ {
155
+ "command": "/home/user/sec-report-kit/.venv/bin/srk",
156
+ "args": ["mcp", "serve", "--transport", "stdio"]
157
+ }
158
+ ```
159
+
160
+ ## Package Publish
161
+
162
+ Build:
163
+
164
+ ```bash
165
+ python -m build
166
+ ```
167
+
168
+ Upload to TestPyPI:
169
+
170
+ ```bash
171
+ python -m twine upload --repository testpypi dist/*
172
+ ```
173
+
174
+ Upload to PyPI:
175
+
176
+ ```bash
177
+ python -m twine upload dist/*
178
+ ```
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sec-report-kit"
7
+ version = "0.1.2"
8
+ description = "Generate HTML security reports from Trivy and pip-audit JSON"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Ravi Konduru" }]
13
+ keywords = ["security", "trivy", "pip-audit", "mcp", "cli", "report"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Security",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "typer>=0.12.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ mcp = [
31
+ "mcp>=1.0.0",
32
+ ]
33
+ dev = [
34
+ "pytest>=8.0.0",
35
+ "build>=1.2.0",
36
+ "twine>=5.0.0",
37
+ ]
38
+
39
+ [project.scripts]
40
+ srk = "sec_report_kit.cli:main"
41
+ "sec-report-kit" = "sec_report_kit.cli:main"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/sec_report_kit"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -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()
@@ -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,70 @@
1
+ from sec_report_kit.parsers.pip_audit import parse_pip_audit_json
2
+ from sec_report_kit.parsers.trivy import parse_trivy_json
3
+ from sec_report_kit.services.summarize import count_by_severity
4
+
5
+
6
+ def test_parse_trivy_json_basic():
7
+ payload = {
8
+ "Results": [
9
+ {
10
+ "Target": "myimage:1",
11
+ "Type": "debian",
12
+ "Vulnerabilities": [
13
+ {
14
+ "Severity": "HIGH",
15
+ "VulnerabilityID": "CVE-1",
16
+ "PkgName": "openssl",
17
+ "InstalledVersion": "1.0",
18
+ "FixedVersion": "1.1",
19
+ "Title": "Example",
20
+ "PrimaryURL": "https://example.com",
21
+ }
22
+ ],
23
+ }
24
+ ]
25
+ }
26
+
27
+ findings = parse_trivy_json(payload)
28
+ assert len(findings) == 1
29
+ assert findings[0].severity == "HIGH"
30
+ assert findings[0].package == "openssl"
31
+
32
+
33
+ def test_parse_pip_audit_json_basic():
34
+ payload = {
35
+ "dependencies": [
36
+ {
37
+ "name": "requests",
38
+ "version": "2.31.0",
39
+ "vulns": [
40
+ {
41
+ "id": "GHSA-xxxx",
42
+ "fix_versions": ["2.32.0"],
43
+ "description": "Example vuln",
44
+ "url": "https://example.com/advisory",
45
+ }
46
+ ],
47
+ }
48
+ ]
49
+ }
50
+
51
+ findings = parse_pip_audit_json(payload)
52
+ assert len(findings) == 1
53
+ assert findings[0].package == "requests"
54
+ assert findings[0].fixed_version == "2.32.0"
55
+
56
+
57
+ def test_count_by_severity_unknown_when_missing():
58
+ payload = {
59
+ "dependencies": [
60
+ {
61
+ "name": "pkg",
62
+ "version": "1.0.0",
63
+ "vulns": [{"id": "PYSEC-1", "description": "no severity"}],
64
+ }
65
+ ]
66
+ }
67
+
68
+ findings = parse_pip_audit_json(payload)
69
+ counts = count_by_severity(findings)
70
+ assert counts["UNKNOWN"] == 1