sec-report-kit 0.2.7__tar.gz → 0.2.9__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.
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/CHANGELOG.md +17 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/PKG-INFO +9 -1
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/README.md +8 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/pyproject.toml +1 -1
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_consolidated_html.bat +1 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_consolidated_html.sh +1 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/__init__.py +1 -1
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/cli.py +52 -5
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/__init__.py +16 -1
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/report/html_renderer.py +42 -14
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_cli.py +8 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_html_renderer.py +75 -1
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.gitattributes +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/pip-audit.yml +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/publish-pypi.yml +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/publish-testpypi.yml +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/trivy-fs-audit.yml +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/unit-test-coverage-gate.yml +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.gitignore +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/check_assets.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/requirements.txt +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/download_safety_db.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/download_safety_db.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/download_safety_db.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/install_external_clis.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/install_tools.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/install_tools.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_bandit_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_bandit_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_checkov_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_checkov_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_codeql_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_codeql_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_gitleaks_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_gitleaks_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_osv_scanner_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_osv_scanner_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_pip_audit_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_pip_audit_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_safety_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_safety_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_semgrep_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_semgrep_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_tfsec_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_tfsec_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trivy_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trivy_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trufflehog_html.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trufflehog_html.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_bandit.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_bandit.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_checkov.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_checkov.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_codeql.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_codeql.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_gitleaks.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_gitleaks.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_osv_scanner.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_osv_scanner.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_pip_audit.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_pip_audit.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_safety.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_safety.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_semgrep.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_semgrep.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_tfsec.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_tfsec.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trivy.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trivy.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trufflehog.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trufflehog.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_unit_tests_with_coverage.bat +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_unit_tests_with_coverage.sh +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/__main__.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/generate_consolidated_security_report.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/mcp/__init__.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/mcp/server.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/models.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/bandit.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/checkov.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/codeql.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/gitleaks.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/pip_audit.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/safety.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/semgrep.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/tfsec.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/trivy.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/trufflehog.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/report/__init__.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/services/__init__.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/services/normalize.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/services/summarize.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_mcp_server.py +0 -0
- {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_parsers.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.9 - 2026-05-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- `srk render consolidated` now works from raw JSON/SARIF inputs alone and automatically generates the per-tool HTML reports it embeds in the dashboard.
|
|
9
|
+
- The consolidated renderer now defaults `--output` to the input folder when the option is omitted.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Consolidated auto-detection no longer misclassifies Bandit JSON as Semgrep, restoring correct per-tool totals and severity counts.
|
|
13
|
+
|
|
14
|
+
## 0.2.8 - 2026-05-17
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Consolidated report helpers now force the workspace `src/` directory onto `PYTHONPATH` so the generated HTML uses the current source tree.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- The consolidated dashboard footer now shows the requested `sec-report-kit` repository link and developer LinkedIn link.
|
|
21
|
+
|
|
5
22
|
## 0.2.7 - 2026-05-17
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sec-report-kit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Generate HTML security reports from Trivy, pip-audit, Safety, Bandit, and Gitleaks JSON
|
|
5
5
|
Author: Ravi Konduru
|
|
6
6
|
License: MIT
|
|
@@ -89,6 +89,14 @@ srk render consolidated --input security_reports --output security_reports --tar
|
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
This command writes `consolidated-security-report.html` to the output folder.
|
|
92
|
+
If `--output` is omitted, it defaults to the same folder passed to `--input`.
|
|
93
|
+
The consolidated renderer reads raw JSON/SARIF inputs, generates the per-tool HTML reports automatically, and then links/embeds those generated HTML files in the dashboard. You do not need to pre-render individual HTML reports first.
|
|
94
|
+
|
|
95
|
+
Example with implicit output folder:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
srk render consolidated --input security_reports --target sec-report-kit
|
|
99
|
+
```
|
|
92
100
|
|
|
93
101
|
## Helper Scripts (bat/sh)
|
|
94
102
|
|
|
@@ -62,6 +62,14 @@ srk render consolidated --input security_reports --output security_reports --tar
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
This command writes `consolidated-security-report.html` to the output folder.
|
|
65
|
+
If `--output` is omitted, it defaults to the same folder passed to `--input`.
|
|
66
|
+
The consolidated renderer reads raw JSON/SARIF inputs, generates the per-tool HTML reports automatically, and then links/embeds those generated HTML files in the dashboard. You do not need to pre-render individual HTML reports first.
|
|
67
|
+
|
|
68
|
+
Example with implicit output folder:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
srk render consolidated --input security_reports --target sec-report-kit
|
|
72
|
+
```
|
|
65
73
|
|
|
66
74
|
## Helper Scripts (bat/sh)
|
|
67
75
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sec-report-kit"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "Generate HTML security reports from Trivy, pip-audit, Safety, Bandit, and Gitleaks JSON"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -5,6 +5,7 @@ PYTHON_BIN="${PYTHON_BIN:-python}"
|
|
|
5
5
|
if [[ -x "$(dirname "$0")/../.venv/bin/python" ]]; then
|
|
6
6
|
PYTHON_BIN="$(dirname "$0")/../.venv/bin/python"
|
|
7
7
|
fi
|
|
8
|
+
export PYTHONPATH="$(dirname "$0")/../src${PYTHONPATH:+:$PYTHONPATH}"
|
|
8
9
|
|
|
9
10
|
REPORT_DIR="${1:-security_reports}"
|
|
10
11
|
ROOT_NAME="$(basename "$(cd "$(dirname "$0")/.." && pwd)")"
|
|
@@ -29,6 +29,21 @@ app.add_typer(render_app, name="render")
|
|
|
29
29
|
app.add_typer(mcp_app, name="mcp")
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
_CONSOLIDATED_TOOL_OUTPUT_FILES = {
|
|
33
|
+
"pip-audit": "pip-audit-report.html",
|
|
34
|
+
"trivy": "trivy-report.html",
|
|
35
|
+
"gitleaks": "gitleaks-report.html",
|
|
36
|
+
"safety": "safety-report.html",
|
|
37
|
+
"bandit": "bandit-report.html",
|
|
38
|
+
"semgrep": "semgrep-report.html",
|
|
39
|
+
"codeql": "codeql-report.html",
|
|
40
|
+
"osv-scanner": "osv-scanner-report.html",
|
|
41
|
+
"checkov": "checkov-report.html",
|
|
42
|
+
"tfsec": "tfsec-report.html",
|
|
43
|
+
"trufflehog": "trufflehog-report.html",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
32
47
|
def _parse_modified_since(value: str) -> dt.datetime:
|
|
33
48
|
local_tz = dt.datetime.now().astimezone().tzinfo or dt.timezone.utc
|
|
34
49
|
now = dt.datetime.now(local_tz)
|
|
@@ -173,6 +188,23 @@ def _write_report(source_label: str, target_ref: str, input_path: Path, output_p
|
|
|
173
188
|
typer.echo(f"[INFO] Severity counts: {counts}")
|
|
174
189
|
|
|
175
190
|
|
|
191
|
+
def _write_consolidated_tool_reports(output_dir: Path, target_ref: str, findings_by_parser: dict[str, list[Any]]) -> None:
|
|
192
|
+
for parser, findings in findings_by_parser.items():
|
|
193
|
+
output_name = _CONSOLIDATED_TOOL_OUTPUT_FILES.get(parser)
|
|
194
|
+
if output_name is None:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
sorted_findings = sort_findings(findings)
|
|
198
|
+
counts = count_by_severity(sorted_findings)
|
|
199
|
+
report_html = render_html_report(
|
|
200
|
+
target_ref=target_ref,
|
|
201
|
+
source_label=parser,
|
|
202
|
+
findings=sorted_findings,
|
|
203
|
+
counts=counts,
|
|
204
|
+
)
|
|
205
|
+
(output_dir / output_name).write_text(report_html, encoding="utf-8")
|
|
206
|
+
|
|
207
|
+
|
|
176
208
|
@render_app.command("trivy")
|
|
177
209
|
def render_trivy(
|
|
178
210
|
input: Path = typer.Option(..., "--input", exists=True, dir_okay=False, file_okay=True, readable=True),
|
|
@@ -304,12 +336,14 @@ def render_consolidated(
|
|
|
304
336
|
readable=True,
|
|
305
337
|
help="Folder containing scanner JSON/SARIF report files",
|
|
306
338
|
),
|
|
307
|
-
output: Path = typer.Option(
|
|
308
|
-
|
|
339
|
+
output: Path | None = typer.Option(
|
|
340
|
+
None,
|
|
309
341
|
"--output",
|
|
310
342
|
dir_okay=True,
|
|
311
343
|
file_okay=False,
|
|
312
|
-
|
|
344
|
+
prompt="Output folder for consolidated report",
|
|
345
|
+
prompt_required=False,
|
|
346
|
+
help="Output folder where consolidated HTML will be written (defaults to --input when omitted)",
|
|
313
347
|
),
|
|
314
348
|
target: str = typer.Option("consolidated-scan", "--target", help="Consolidated target label"),
|
|
315
349
|
modified_since: str | None = typer.Option(
|
|
@@ -330,6 +364,9 @@ def render_consolidated(
|
|
|
330
364
|
),
|
|
331
365
|
) -> None:
|
|
332
366
|
"""Render a consolidated HTML report from all supported report files in a directory."""
|
|
367
|
+
if output is None:
|
|
368
|
+
output = input
|
|
369
|
+
|
|
333
370
|
candidates = _collect_consolidated_candidates(
|
|
334
371
|
input_dir=input,
|
|
335
372
|
modified_since=modified_since,
|
|
@@ -338,6 +375,8 @@ def render_consolidated(
|
|
|
338
375
|
)
|
|
339
376
|
|
|
340
377
|
all_findings = []
|
|
378
|
+
findings_by_parser: dict[str, list[Any]] = {}
|
|
379
|
+
included_sources: set[str] = set()
|
|
341
380
|
included_files = 0
|
|
342
381
|
skipped_files = 0
|
|
343
382
|
for report_file in candidates:
|
|
@@ -346,23 +385,31 @@ def render_consolidated(
|
|
|
346
385
|
parser = detect_source_type(payload)
|
|
347
386
|
findings = _parse_findings(payload, parser)
|
|
348
387
|
all_findings.extend(findings)
|
|
388
|
+
findings_by_parser.setdefault(parser, []).extend(findings)
|
|
389
|
+
included_sources.add(parser)
|
|
349
390
|
included_files += 1
|
|
350
391
|
typer.echo(f"[INFO] Included {report_file.name} as {parser} ({len(findings)} findings)")
|
|
351
392
|
except Exception as exc:
|
|
352
393
|
skipped_files += 1
|
|
353
394
|
typer.echo(f"[WARN] Skipping {report_file.name}: {exc}")
|
|
354
395
|
|
|
396
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
397
|
+
_write_consolidated_tool_reports(output, target, findings_by_parser)
|
|
398
|
+
|
|
355
399
|
all_findings = sort_findings(all_findings)
|
|
356
400
|
counts = count_by_severity(all_findings)
|
|
401
|
+
since_dt = _parse_modified_since(modified_since) if modified_since else None
|
|
402
|
+
until_dt = _parse_modified_until(modified_until) if modified_until else None
|
|
357
403
|
report_html = render_consolidated_dashboard_report(
|
|
358
404
|
target_ref=target,
|
|
359
405
|
findings=all_findings,
|
|
360
406
|
counts=counts,
|
|
361
407
|
reports_dir=input,
|
|
362
408
|
output_dir=output,
|
|
409
|
+
modified_since=since_dt,
|
|
410
|
+
modified_until=until_dt,
|
|
411
|
+
included_sources=included_sources,
|
|
363
412
|
)
|
|
364
|
-
|
|
365
|
-
output.mkdir(parents=True, exist_ok=True)
|
|
366
413
|
output_file = output / "consolidated-security-report.html"
|
|
367
414
|
output_file.write_text(report_html, encoding="utf-8")
|
|
368
415
|
|
|
@@ -61,10 +61,25 @@ def detect_source_type(data: dict | list) -> str:
|
|
|
61
61
|
):
|
|
62
62
|
return "trufflehog"
|
|
63
63
|
if "results" in data and isinstance(data.get("results"), list):
|
|
64
|
-
|
|
64
|
+
sample = data["results"][0] if data["results"] else {}
|
|
65
|
+
if isinstance(sample, dict) and (
|
|
66
|
+
"check_id" in sample
|
|
67
|
+
or (
|
|
68
|
+
"extra" in sample
|
|
69
|
+
and isinstance(sample.get("extra"), dict)
|
|
70
|
+
and ("severity" in sample["extra"] or "message" in sample["extra"])
|
|
71
|
+
)
|
|
72
|
+
):
|
|
65
73
|
return "semgrep"
|
|
66
74
|
if all(isinstance(item, dict) and "rule_id" in item for item in data.get("results", [])):
|
|
67
75
|
return "tfsec"
|
|
76
|
+
if isinstance(sample, dict) and (
|
|
77
|
+
"issue_severity" in sample
|
|
78
|
+
or "issue_text" in sample
|
|
79
|
+
or "test_id" in sample
|
|
80
|
+
or "test_name" in sample
|
|
81
|
+
):
|
|
82
|
+
return "bandit"
|
|
68
83
|
return "bandit"
|
|
69
84
|
if "findings" in data and isinstance(data.get("findings"), list):
|
|
70
85
|
return "gitleaks"
|
|
@@ -24,12 +24,27 @@ def _count_severities(findings: list[Finding]) -> dict[str, int]:
|
|
|
24
24
|
return counted
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def _find_first_existing(
|
|
27
|
+
def _find_first_existing(
|
|
28
|
+
base_dirs: list[Path],
|
|
29
|
+
file_names: list[str],
|
|
30
|
+
modified_since: dt.datetime | None = None,
|
|
31
|
+
modified_until: dt.datetime | None = None,
|
|
32
|
+
) -> Path | None:
|
|
28
33
|
for file_name in file_names:
|
|
29
34
|
for base in base_dirs:
|
|
30
35
|
candidate = base / file_name
|
|
31
|
-
if candidate.exists() and candidate.is_file():
|
|
32
|
-
|
|
36
|
+
if not (candidate.exists() and candidate.is_file()):
|
|
37
|
+
continue
|
|
38
|
+
if modified_since is not None or modified_until is not None:
|
|
39
|
+
mtime = dt.datetime.fromtimestamp(
|
|
40
|
+
candidate.stat().st_mtime,
|
|
41
|
+
tz=(modified_since or modified_until).tzinfo, # type: ignore[union-attr]
|
|
42
|
+
)
|
|
43
|
+
if modified_since is not None and mtime < modified_since:
|
|
44
|
+
continue
|
|
45
|
+
if modified_until is not None and mtime > modified_until:
|
|
46
|
+
continue
|
|
47
|
+
return candidate
|
|
33
48
|
return None
|
|
34
49
|
|
|
35
50
|
|
|
@@ -44,6 +59,9 @@ def render_consolidated_dashboard_report(
|
|
|
44
59
|
counts: dict[str, int],
|
|
45
60
|
reports_dir: Path,
|
|
46
61
|
output_dir: Path,
|
|
62
|
+
modified_since: dt.datetime | None = None,
|
|
63
|
+
modified_until: dt.datetime | None = None,
|
|
64
|
+
included_sources: set[str] | None = None,
|
|
47
65
|
) -> str:
|
|
48
66
|
generated_at = dt.datetime.now(dt.timezone.utc).isoformat()
|
|
49
67
|
|
|
@@ -132,22 +150,38 @@ def render_consolidated_dashboard_report(
|
|
|
132
150
|
normalized_key = _normalize_tool_key(item.source_type)
|
|
133
151
|
normalized_findings.setdefault(normalized_key, []).append(item)
|
|
134
152
|
|
|
153
|
+
normalized_included_sources = {
|
|
154
|
+
_normalize_tool_key(source)
|
|
155
|
+
for source in (included_sources or set())
|
|
156
|
+
}
|
|
157
|
+
filtered_mode = modified_since is not None or modified_until is not None
|
|
158
|
+
|
|
135
159
|
base_dirs = [reports_dir]
|
|
136
160
|
if output_dir != reports_dir:
|
|
137
161
|
base_dirs.append(output_dir)
|
|
138
162
|
|
|
139
163
|
tool_rows: list[dict[str, object]] = []
|
|
140
164
|
for spec in tool_specs:
|
|
141
|
-
matched_file = _find_first_existing(
|
|
165
|
+
matched_file = _find_first_existing(
|
|
166
|
+
base_dirs,
|
|
167
|
+
spec["report_files"],
|
|
168
|
+
modified_since=modified_since,
|
|
169
|
+
modified_until=modified_until,
|
|
170
|
+
)
|
|
142
171
|
source_findings = normalized_findings.get(spec["key"], [])
|
|
143
172
|
source_counts = _count_severities(source_findings)
|
|
173
|
+
available = matched_file is not None
|
|
174
|
+
if filtered_mode:
|
|
175
|
+
# In filtered runs, only show embedded tool HTML if that tool had at least one
|
|
176
|
+
# source file included in the filtered candidate set.
|
|
177
|
+
available = available and spec["key"] in normalized_included_sources
|
|
144
178
|
tool_rows.append(
|
|
145
179
|
{
|
|
146
180
|
"key": spec["key"],
|
|
147
181
|
"name": spec["name"],
|
|
148
182
|
"subtitle": spec["subtitle"],
|
|
149
183
|
"run_hint": spec["run_hint"],
|
|
150
|
-
"available":
|
|
184
|
+
"available": available,
|
|
151
185
|
"href": _to_href(output_dir, matched_file) if matched_file else "",
|
|
152
186
|
"total": len(source_findings),
|
|
153
187
|
"critical": source_counts["CRITICAL"],
|
|
@@ -336,7 +370,7 @@ footer {{
|
|
|
336
370
|
<main class="page">
|
|
337
371
|
<section class="hero">
|
|
338
372
|
<h1>Security Audit Consolidated Dashboard</h1>
|
|
339
|
-
<div class="hero-meta">Generated: {_esc(generated_at)} | Reports available: {reports_available}/{reports_total} | Target: {_esc(target_ref)}</div>
|
|
373
|
+
<div class="hero-meta">Generated: {_esc(generated_at)} | Reports available: {reports_available}/{reports_total} | Target: {_esc(target_ref)}{(' | Since: ' + _esc(modified_since.strftime('%Y-%m-%d %H:%M'))) if modified_since else ''}{(' | Until: ' + _esc(modified_until.strftime('%Y-%m-%d %H:%M'))) if modified_until else ''}</div>
|
|
340
374
|
<div class="summary">
|
|
341
375
|
<div class="summary-card"><span class="label">Total Findings</span><span class="value">{len(findings)}</span></div>
|
|
342
376
|
<div class="summary-card"><span class="label">Critical</span><span class="value">{counts['CRITICAL']}</span></div>
|
|
@@ -360,8 +394,7 @@ footer {{
|
|
|
360
394
|
</section>
|
|
361
395
|
|
|
362
396
|
<footer>
|
|
363
|
-
<p>This
|
|
364
|
-
<p>Generate missing tool HTML files using the corresponding scripts in the scripts folder.</p>
|
|
397
|
+
<p>This report is generated by <a href="https://github.com/ShanKonduru/sec-report-kit" target="_blank" rel="noopener noreferrer">sec-report-kit utility</a> | Connect with the developer: <a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer">Shan Konduru</a></p>
|
|
365
398
|
</footer>
|
|
366
399
|
</main>
|
|
367
400
|
</body>
|
|
@@ -561,12 +594,7 @@ def render_html_report(target_ref: str, source_label: str, findings: list[Findin
|
|
|
561
594
|
</script>
|
|
562
595
|
|
|
563
596
|
<footer class="report-footer">
|
|
564
|
-
This report is generated by
|
|
565
|
-
<a href="https://github.com/ShanKonduru/sec-report-kit" target="_blank" rel="noopener noreferrer">sec-report-kit</a>
|
|
566
|
-
utility
|
|
567
|
-
|
|
|
568
|
-
Connect with the developer:
|
|
569
|
-
<a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer">Shan Konduru</a>
|
|
597
|
+
This report is generated by <a href="https://github.com/ShanKonduru/sec-report-kit" target="_blank" rel="noopener noreferrer">sec-report-kit utility</a> | Connect with the developer: <a href="https://www.linkedin.com/in/shankonduru/" target="_blank" rel="noopener noreferrer">Shan Konduru</a>
|
|
570
598
|
</footer>
|
|
571
599
|
</body>
|
|
572
600
|
</html>
|
|
@@ -438,14 +438,20 @@ def test_render_consolidated_command_from_directory(tmp_path):
|
|
|
438
438
|
)
|
|
439
439
|
|
|
440
440
|
output_file = output_dir / "consolidated-security-report.html"
|
|
441
|
+
bandit_report = output_dir / "bandit-report.html"
|
|
442
|
+
trivy_report = output_dir / "trivy-report.html"
|
|
441
443
|
assert result.exit_code == 0
|
|
442
444
|
assert output_file.exists()
|
|
445
|
+
assert bandit_report.exists()
|
|
446
|
+
assert trivy_report.exists()
|
|
443
447
|
html = output_file.read_text()
|
|
444
448
|
assert "Security Audit Consolidated Dashboard" in html
|
|
445
449
|
assert "Reports available:" in html
|
|
446
450
|
assert "Target: repo-root" in html
|
|
447
451
|
assert "Bandit" in html
|
|
448
452
|
assert "Trivy" in html
|
|
453
|
+
assert 'src="bandit-report.html"' in html
|
|
454
|
+
assert 'src="trivy-report.html"' in html
|
|
449
455
|
|
|
450
456
|
|
|
451
457
|
def test_render_consolidated_command_empty_or_unsupported_directory(tmp_path):
|
|
@@ -503,6 +509,8 @@ def test_render_consolidated_command_modified_since_filters_candidates(tmp_path)
|
|
|
503
509
|
assert "Included trivy.json" in result.output
|
|
504
510
|
assert "Included bandit.json" not in result.output
|
|
505
511
|
assert "Files included: 1" in result.output
|
|
512
|
+
assert (output_dir / "trivy-report.html").exists()
|
|
513
|
+
assert not (output_dir / "bandit-report.html").exists()
|
|
506
514
|
|
|
507
515
|
|
|
508
516
|
def test_render_consolidated_command_modified_until_filters_candidates(tmp_path):
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
|
|
1
5
|
from sec_report_kit.models import Finding
|
|
2
6
|
from sec_report_kit.report.html_renderer import render_consolidated_dashboard_report, render_html_report
|
|
3
7
|
|
|
@@ -31,9 +35,10 @@ def test_render_html_with_finding_with_url():
|
|
|
31
35
|
assert "https://example.com" in html
|
|
32
36
|
assert "openssl" in html
|
|
33
37
|
assert "This report is generated by" in html
|
|
34
|
-
assert "
|
|
38
|
+
assert "sec-report-kit utility" in html
|
|
35
39
|
assert "Connect with the developer:" in html
|
|
36
40
|
assert ">Shan Konduru<" in html
|
|
41
|
+
assert "https://github.com/ShanKonduru/sec-report-kit" in html
|
|
37
42
|
assert "https://www.linkedin.com/in/shankonduru/" in html
|
|
38
43
|
|
|
39
44
|
|
|
@@ -144,6 +149,9 @@ def test_render_consolidated_dashboard_with_available_and_missing_reports(tmp_pa
|
|
|
144
149
|
assert "Bandit Report" in html
|
|
145
150
|
assert "Trivy Report" in html
|
|
146
151
|
assert "Reports available:" in html
|
|
152
|
+
assert "This report is generated by" in html
|
|
153
|
+
assert "sec-report-kit utility" in html
|
|
154
|
+
assert "Connect with the developer:" in html
|
|
147
155
|
|
|
148
156
|
|
|
149
157
|
def test_render_consolidated_dashboard_shows_all_tools_when_no_visible_tools(tmp_path):
|
|
@@ -164,3 +172,69 @@ def test_render_consolidated_dashboard_shows_all_tools_when_no_visible_tools(tmp
|
|
|
164
172
|
assert "GitLeaks Report" in html
|
|
165
173
|
assert "TruffleHog Report" in html
|
|
166
174
|
assert "Generate this report first using scripts/render_trivy_html.sh or scripts/render_trivy_html.bat." in html
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_render_consolidated_dashboard_date_filter_excludes_old_html_files(tmp_path):
|
|
178
|
+
"""HTML report files outside the date range must not appear as 'available'."""
|
|
179
|
+
reports_dir = tmp_path / "reports"
|
|
180
|
+
output_dir = reports_dir
|
|
181
|
+
reports_dir.mkdir()
|
|
182
|
+
|
|
183
|
+
# Write a bandit HTML file and backdate its mtime to 2000-01-01
|
|
184
|
+
bandit_html = reports_dir / "bandit-report.html"
|
|
185
|
+
bandit_html.write_text("<html>bandit</html>", encoding="utf-8")
|
|
186
|
+
old_mtime = dt.datetime(2000, 1, 1, tzinfo=dt.timezone.utc).timestamp()
|
|
187
|
+
import os
|
|
188
|
+
os.utime(bandit_html, (old_mtime, old_mtime))
|
|
189
|
+
|
|
190
|
+
# Write a trivy HTML file with a very-future mtime (3000-01-01)
|
|
191
|
+
trivy_html = reports_dir / "trivy-report.html"
|
|
192
|
+
trivy_html.write_text("<html>trivy</html>", encoding="utf-8")
|
|
193
|
+
future_mtime = dt.datetime(3000, 1, 1, tzinfo=dt.timezone.utc).timestamp()
|
|
194
|
+
os.utime(trivy_html, (future_mtime, future_mtime))
|
|
195
|
+
|
|
196
|
+
tz = dt.timezone.utc
|
|
197
|
+
since = dt.datetime(2026, 1, 1, tzinfo=tz)
|
|
198
|
+
until = dt.datetime(2026, 12, 31, 23, 59, 59, tzinfo=tz)
|
|
199
|
+
|
|
200
|
+
html = render_consolidated_dashboard_report(
|
|
201
|
+
target_ref="repo",
|
|
202
|
+
findings=[],
|
|
203
|
+
counts={"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0},
|
|
204
|
+
reports_dir=reports_dir,
|
|
205
|
+
output_dir=output_dir,
|
|
206
|
+
modified_since=since,
|
|
207
|
+
modified_until=until,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Bandit HTML is from 2000 (before since) — must NOT be shown as available
|
|
211
|
+
assert 'src="../reports/bandit-report.html"' not in html
|
|
212
|
+
assert 'src="bandit-report.html"' not in html
|
|
213
|
+
# Trivy HTML is from 3000 (after until) — must NOT be shown as available
|
|
214
|
+
assert 'src="trivy-report.html"' not in html
|
|
215
|
+
# Since and until labels should appear in the hero
|
|
216
|
+
assert "Since: 2026-01-01" in html
|
|
217
|
+
assert "Until: 2026-12-31" in html
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_render_consolidated_dashboard_filtered_mode_requires_included_source(tmp_path):
|
|
221
|
+
reports_dir = tmp_path / "reports"
|
|
222
|
+
reports_dir.mkdir()
|
|
223
|
+
|
|
224
|
+
bandit_html = reports_dir / "bandit-report.html"
|
|
225
|
+
bandit_html.write_text("<html>bandit</html>", encoding="utf-8")
|
|
226
|
+
|
|
227
|
+
html = render_consolidated_dashboard_report(
|
|
228
|
+
target_ref="repo",
|
|
229
|
+
findings=[],
|
|
230
|
+
counts={"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0},
|
|
231
|
+
reports_dir=reports_dir,
|
|
232
|
+
output_dir=reports_dir,
|
|
233
|
+
modified_since=dt.datetime(2026, 5, 17, tzinfo=dt.timezone.utc),
|
|
234
|
+
modified_until=dt.datetime(2026, 5, 20, 23, 59, 59, tzinfo=dt.timezone.utc),
|
|
235
|
+
included_sources=set(),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
assert "Reports available: 0/11" in html
|
|
239
|
+
assert 'src="bandit-report.html"' not in html
|
|
240
|
+
assert "Tool report not found" in html
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|