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.
Files changed (96) hide show
  1. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/CHANGELOG.md +17 -0
  2. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/PKG-INFO +9 -1
  3. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/README.md +8 -0
  4. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/pyproject.toml +1 -1
  5. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_consolidated_html.bat +1 -0
  6. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_consolidated_html.sh +1 -0
  7. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/__init__.py +1 -1
  8. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/cli.py +52 -5
  9. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/__init__.py +16 -1
  10. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/report/html_renderer.py +42 -14
  11. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_cli.py +8 -0
  12. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_html_renderer.py +75 -1
  13. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.gitattributes +0 -0
  14. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/pip-audit.yml +0 -0
  15. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/publish-pypi.yml +0 -0
  16. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/publish-testpypi.yml +0 -0
  17. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/trivy-fs-audit.yml +0 -0
  18. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.github/workflows/unit-test-coverage-gate.yml +0 -0
  19. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/.gitignore +0 -0
  20. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
  21. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/check_assets.py +0 -0
  22. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/requirements.txt +0 -0
  23. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/download_safety_db.bat +0 -0
  24. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/download_safety_db.py +0 -0
  25. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/download_safety_db.sh +0 -0
  26. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/install_external_clis.py +0 -0
  27. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/install_tools.bat +0 -0
  28. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/install_tools.sh +0 -0
  29. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_bandit_html.bat +0 -0
  30. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_bandit_html.sh +0 -0
  31. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_checkov_html.bat +0 -0
  32. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_checkov_html.sh +0 -0
  33. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_codeql_html.bat +0 -0
  34. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_codeql_html.sh +0 -0
  35. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_gitleaks_html.bat +0 -0
  36. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_gitleaks_html.sh +0 -0
  37. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_osv_scanner_html.bat +0 -0
  38. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_osv_scanner_html.sh +0 -0
  39. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_pip_audit_html.bat +0 -0
  40. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_pip_audit_html.sh +0 -0
  41. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_safety_html.bat +0 -0
  42. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_safety_html.sh +0 -0
  43. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_semgrep_html.bat +0 -0
  44. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_semgrep_html.sh +0 -0
  45. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_tfsec_html.bat +0 -0
  46. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_tfsec_html.sh +0 -0
  47. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trivy_html.bat +0 -0
  48. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trivy_html.sh +0 -0
  49. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trufflehog_html.bat +0 -0
  50. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/render_trufflehog_html.sh +0 -0
  51. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_bandit.bat +0 -0
  52. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_bandit.sh +0 -0
  53. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_checkov.bat +0 -0
  54. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_checkov.sh +0 -0
  55. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_codeql.bat +0 -0
  56. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_codeql.sh +0 -0
  57. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_gitleaks.bat +0 -0
  58. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_gitleaks.sh +0 -0
  59. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_osv_scanner.bat +0 -0
  60. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_osv_scanner.sh +0 -0
  61. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_pip_audit.bat +0 -0
  62. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_pip_audit.sh +0 -0
  63. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_safety.bat +0 -0
  64. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_safety.sh +0 -0
  65. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_semgrep.bat +0 -0
  66. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_semgrep.sh +0 -0
  67. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_tfsec.bat +0 -0
  68. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_tfsec.sh +0 -0
  69. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trivy.bat +0 -0
  70. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trivy.sh +0 -0
  71. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trufflehog.bat +0 -0
  72. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_trufflehog.sh +0 -0
  73. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_unit_tests_with_coverage.bat +0 -0
  74. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/scripts/run_unit_tests_with_coverage.sh +0 -0
  75. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/__main__.py +0 -0
  76. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/generate_consolidated_security_report.py +0 -0
  77. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/mcp/__init__.py +0 -0
  78. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/mcp/server.py +0 -0
  79. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/models.py +0 -0
  80. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/bandit.py +0 -0
  81. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/checkov.py +0 -0
  82. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/codeql.py +0 -0
  83. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/gitleaks.py +0 -0
  84. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
  85. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/pip_audit.py +0 -0
  86. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/safety.py +0 -0
  87. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/semgrep.py +0 -0
  88. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/tfsec.py +0 -0
  89. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/trivy.py +0 -0
  90. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/parsers/trufflehog.py +0 -0
  91. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/report/__init__.py +0 -0
  92. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/services/__init__.py +0 -0
  93. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/services/normalize.py +0 -0
  94. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/src/sec_report_kit/services/summarize.py +0 -0
  95. {sec_report_kit-0.2.7 → sec_report_kit-0.2.9}/tests/test_mcp_server.py +0 -0
  96. {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.7
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"
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"
@@ -3,6 +3,7 @@ setlocal
3
3
 
4
4
  set "PYTHON_BIN=python"
5
5
  if exist "%~dp0..\.venv\Scripts\python.exe" set "PYTHON_BIN=%~dp0..\.venv\Scripts\python.exe"
6
+ set "PYTHONPATH=%~dp0..\src;%PYTHONPATH%"
6
7
 
7
8
  set "REPORT_DIR=%~1"
8
9
  if "%REPORT_DIR%"=="" set "REPORT_DIR=security_reports"
@@ -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)")"
@@ -1,4 +1,4 @@
1
1
  """sec_report_kit package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.2.7"
4
+ __version__ = "0.2.9"
@@ -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
- help="Output folder where consolidated HTML will be written",
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
- if "errors" in data or "paths" in data or "version" in data:
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(base_dirs: list[Path], file_names: list[str]) -> Path | None:
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
- return candidate
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(base_dirs, spec["report_files"])
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": matched_file is not None,
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 consolidated dashboard embeds tool-specific HTML reports when available.</p>
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> &nbsp;|&nbsp; 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
- &nbsp;|&nbsp;
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> &nbsp;|&nbsp; 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 ">sec-report-kit<" in html
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