sec-report-kit 0.2.6__tar.gz → 0.2.8__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 (97) hide show
  1. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/CHANGELOG.md +16 -0
  2. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/PKG-INFO +1 -1
  3. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/pyproject.toml +1 -1
  4. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/requirements.txt +1 -1
  5. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_consolidated_html.bat +1 -0
  6. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_consolidated_html.sh +1 -0
  7. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/__init__.py +1 -1
  8. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/cli.py +7 -0
  9. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/report/html_renderer.py +42 -14
  10. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/tests/test_cli.py +72 -1
  11. sec_report_kit-0.2.8/tests/test_html_renderer.py +240 -0
  12. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/tests/test_parsers.py +73 -0
  13. sec_report_kit-0.2.6/tests/test_html_renderer.py +0 -83
  14. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.gitattributes +0 -0
  15. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.github/workflows/pip-audit.yml +0 -0
  16. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.github/workflows/publish-pypi.yml +0 -0
  17. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.github/workflows/publish-testpypi.yml +0 -0
  18. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.github/workflows/trivy-fs-audit.yml +0 -0
  19. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.github/workflows/unit-test-coverage-gate.yml +0 -0
  20. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/.gitignore +0 -0
  21. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/README.md +0 -0
  22. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
  23. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/check_assets.py +0 -0
  24. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/download_safety_db.bat +0 -0
  25. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/download_safety_db.py +0 -0
  26. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/download_safety_db.sh +0 -0
  27. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/install_external_clis.py +0 -0
  28. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/install_tools.bat +0 -0
  29. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/install_tools.sh +0 -0
  30. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_bandit_html.bat +0 -0
  31. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_bandit_html.sh +0 -0
  32. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_checkov_html.bat +0 -0
  33. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_checkov_html.sh +0 -0
  34. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_codeql_html.bat +0 -0
  35. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_codeql_html.sh +0 -0
  36. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_gitleaks_html.bat +0 -0
  37. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_gitleaks_html.sh +0 -0
  38. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_osv_scanner_html.bat +0 -0
  39. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_osv_scanner_html.sh +0 -0
  40. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_pip_audit_html.bat +0 -0
  41. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_pip_audit_html.sh +0 -0
  42. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_safety_html.bat +0 -0
  43. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_safety_html.sh +0 -0
  44. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_semgrep_html.bat +0 -0
  45. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_semgrep_html.sh +0 -0
  46. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_tfsec_html.bat +0 -0
  47. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_tfsec_html.sh +0 -0
  48. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_trivy_html.bat +0 -0
  49. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_trivy_html.sh +0 -0
  50. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_trufflehog_html.bat +0 -0
  51. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/render_trufflehog_html.sh +0 -0
  52. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_bandit.bat +0 -0
  53. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_bandit.sh +0 -0
  54. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_checkov.bat +0 -0
  55. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_checkov.sh +0 -0
  56. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_codeql.bat +0 -0
  57. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_codeql.sh +0 -0
  58. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_gitleaks.bat +0 -0
  59. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_gitleaks.sh +0 -0
  60. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_osv_scanner.bat +0 -0
  61. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_osv_scanner.sh +0 -0
  62. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_pip_audit.bat +0 -0
  63. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_pip_audit.sh +0 -0
  64. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_safety.bat +0 -0
  65. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_safety.sh +0 -0
  66. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_semgrep.bat +0 -0
  67. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_semgrep.sh +0 -0
  68. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_tfsec.bat +0 -0
  69. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_tfsec.sh +0 -0
  70. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_trivy.bat +0 -0
  71. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_trivy.sh +0 -0
  72. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_trufflehog.bat +0 -0
  73. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_trufflehog.sh +0 -0
  74. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_unit_tests_with_coverage.bat +0 -0
  75. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/scripts/run_unit_tests_with_coverage.sh +0 -0
  76. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/__main__.py +0 -0
  77. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/generate_consolidated_security_report.py +0 -0
  78. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/mcp/__init__.py +0 -0
  79. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/mcp/server.py +0 -0
  80. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/models.py +0 -0
  81. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/__init__.py +0 -0
  82. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/bandit.py +0 -0
  83. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/checkov.py +0 -0
  84. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/codeql.py +0 -0
  85. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/gitleaks.py +0 -0
  86. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
  87. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/pip_audit.py +0 -0
  88. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/safety.py +0 -0
  89. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/semgrep.py +0 -0
  90. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/tfsec.py +0 -0
  91. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/trivy.py +0 -0
  92. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/parsers/trufflehog.py +0 -0
  93. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/report/__init__.py +0 -0
  94. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/services/__init__.py +0 -0
  95. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/services/normalize.py +0 -0
  96. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/src/sec_report_kit/services/summarize.py +0 -0
  97. {sec_report_kit-0.2.6 → sec_report_kit-0.2.8}/tests/test_mcp_server.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 0.2.8 - 2026-05-17
6
+
7
+ ### Changed
8
+ - Consolidated report helpers now force the workspace `src/` directory onto `PYTHONPATH` so the generated HTML uses the current source tree.
9
+
10
+ ### Fixed
11
+ - The consolidated dashboard footer now shows the requested `sec-report-kit` repository link and developer LinkedIn link.
12
+
13
+ ## 0.2.7 - 2026-05-17
14
+
15
+ ### Added
16
+ - Test coverage now includes the consolidated date-filter helpers, Safety parser fallback branches, and consolidated HTML dashboard visibility paths, bringing enforced suite coverage to 100%.
17
+
18
+ ### Fixed
19
+ - `requirements.txt` now pins `urllib3==2.7.0` to resolve the Trivy-reported CVEs affecting `2.6.3`.
20
+
5
21
  ## 0.2.6 - 2026-05-17
6
22
 
7
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sec-report-kit
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sec-report-kit"
7
- version = "0.2.6"
7
+ version = "0.2.8"
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"
@@ -80,5 +80,5 @@ twine==6.2.0
80
80
  typer==0.25.1
81
81
  typing-inspection==0.4.2
82
82
  typing_extensions==4.15.0
83
- urllib3==2.6.3
83
+ urllib3==2.7.0
84
84
  uvicorn==0.46.0
@@ -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.6"
4
+ __version__ = "0.2.8"
@@ -338,6 +338,7 @@ def render_consolidated(
338
338
  )
339
339
 
340
340
  all_findings = []
341
+ included_sources: set[str] = set()
341
342
  included_files = 0
342
343
  skipped_files = 0
343
344
  for report_file in candidates:
@@ -346,6 +347,7 @@ def render_consolidated(
346
347
  parser = detect_source_type(payload)
347
348
  findings = _parse_findings(payload, parser)
348
349
  all_findings.extend(findings)
350
+ included_sources.add(parser)
349
351
  included_files += 1
350
352
  typer.echo(f"[INFO] Included {report_file.name} as {parser} ({len(findings)} findings)")
351
353
  except Exception as exc:
@@ -354,12 +356,17 @@ def render_consolidated(
354
356
 
355
357
  all_findings = sort_findings(all_findings)
356
358
  counts = count_by_severity(all_findings)
359
+ since_dt = _parse_modified_since(modified_since) if modified_since else None
360
+ until_dt = _parse_modified_until(modified_until) if modified_until else None
357
361
  report_html = render_consolidated_dashboard_report(
358
362
  target_ref=target,
359
363
  findings=all_findings,
360
364
  counts=counts,
361
365
  reports_dir=input,
362
366
  output_dir=output,
367
+ modified_since=since_dt,
368
+ modified_until=until_dt,
369
+ included_sources=included_sources,
363
370
  )
364
371
 
365
372
  output.mkdir(parents=True, exist_ok=True)
@@ -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>
@@ -8,7 +8,7 @@ from unittest.mock import patch
8
8
  import pytest
9
9
  from typer.testing import CliRunner
10
10
 
11
- from sec_report_kit.cli import _write_report, app
11
+ from sec_report_kit.cli import _parse_modified_since, _parse_modified_until, _write_report, app
12
12
 
13
13
  runner = CliRunner()
14
14
 
@@ -666,6 +666,77 @@ def test_generate_consolidated_wrapper_invokes_cli(monkeypatch, tmp_path):
666
666
  )
667
667
 
668
668
 
669
+ def test_parse_modified_since_supports_named_ranges():
670
+ today = _parse_modified_since("today")
671
+ yesterday = _parse_modified_since("yesterday")
672
+ last_week = _parse_modified_since("last-week")
673
+
674
+ assert today.hour == 0
675
+ assert today.minute == 0
676
+ assert yesterday.hour == 0
677
+ assert yesterday.minute == 0
678
+ assert yesterday < today
679
+ assert last_week < today
680
+
681
+
682
+ def test_parse_modified_since_supports_iso_inputs_and_raises_for_invalid():
683
+ naive = _parse_modified_since("2026-05-10")
684
+ aware = _parse_modified_since("2026-05-10T09:00:00Z")
685
+
686
+ assert naive.year == 2026
687
+ assert naive.month == 5
688
+ assert naive.day == 10
689
+ assert naive.tzinfo is not None
690
+ assert aware.tzinfo is not None
691
+
692
+ import typer
693
+
694
+ with pytest.raises(typer.BadParameter):
695
+ _parse_modified_since("not-a-date")
696
+
697
+
698
+ def test_parse_modified_until_supports_named_and_date_only_inputs():
699
+ today = _parse_modified_until("today")
700
+ yesterday = _parse_modified_until("yesterday")
701
+ date_only = _parse_modified_until("2026-05-10")
702
+ datetime_value = _parse_modified_until("2026-05-10T09:15:00")
703
+
704
+ assert today.hour == 23
705
+ assert today.minute == 59
706
+ assert today.second == 59
707
+ assert today.microsecond == 999999
708
+ assert yesterday.hour == 23
709
+ assert yesterday.minute == 59
710
+ assert date_only.hour == 23
711
+ assert date_only.minute == 59
712
+ assert datetime_value.hour == 9
713
+ assert datetime_value.minute == 15
714
+
715
+
716
+ def test_generate_consolidated_wrapper_module_main(monkeypatch, tmp_path):
717
+ input_dir = tmp_path / "reports"
718
+ output_dir = tmp_path / "out"
719
+ input_dir.mkdir()
720
+
721
+ monkeypatch.setattr(
722
+ "sys.argv",
723
+ [
724
+ "generate_consolidated_security_report.py",
725
+ "--input",
726
+ str(input_dir),
727
+ "--output",
728
+ str(output_dir),
729
+ ],
730
+ )
731
+
732
+ with patch("sec_report_kit.cli.render_consolidated") as mock_render:
733
+ with pytest.raises(SystemExit) as exc:
734
+ runpy.run_module("sec_report_kit.generate_consolidated_security_report", run_name="__main__")
735
+
736
+ assert exc.value.code == 0
737
+ mock_render.assert_called_once()
738
+
739
+
669
740
  def test_consolidated_helper_scripts_exist():
670
741
  assert Path("scripts/render_consolidated_html.bat").exists()
671
742
  assert Path("scripts/render_consolidated_html.sh").exists()
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+
5
+ from sec_report_kit.models import Finding
6
+ from sec_report_kit.report.html_renderer import render_consolidated_dashboard_report, render_html_report
7
+
8
+
9
+ EMPTY_COUNTS = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
10
+
11
+
12
+ def _finding(severity="HIGH", primary_url="https://example.com"):
13
+ return Finding(
14
+ source_type="test",
15
+ target="myimage:1",
16
+ severity=severity,
17
+ vulnerability_id="CVE-2024-0001",
18
+ package="openssl",
19
+ installed_version="1.0",
20
+ fixed_version="1.1",
21
+ title="Test vuln",
22
+ primary_url=primary_url,
23
+ )
24
+
25
+
26
+ def test_render_html_with_finding_with_url():
27
+ counts = {**EMPTY_COUNTS, "HIGH": 1}
28
+ html = render_html_report(
29
+ target_ref="myimage:1",
30
+ source_label="trivy",
31
+ findings=[_finding(primary_url="https://example.com")],
32
+ counts=counts,
33
+ )
34
+ assert "CVE-2024-0001" in html
35
+ assert "https://example.com" in html
36
+ assert "openssl" in html
37
+ assert "This report is generated by" in html
38
+ assert "sec-report-kit utility" in html
39
+ assert "Connect with the developer:" in html
40
+ assert ">Shan Konduru<" in html
41
+ assert "https://github.com/ShanKonduru/sec-report-kit" in html
42
+ assert "https://www.linkedin.com/in/shankonduru/" in html
43
+
44
+
45
+ def test_render_html_with_finding_no_url():
46
+ counts = {**EMPTY_COUNTS, "HIGH": 1}
47
+ html = render_html_report(
48
+ target_ref="myimage:1",
49
+ source_label="trivy",
50
+ findings=[_finding(primary_url="")],
51
+ counts=counts,
52
+ )
53
+ assert "CVE-2024-0001" in html
54
+ # no anchor tag when url is empty
55
+ assert 'href=""' not in html
56
+
57
+
58
+ def test_render_html_no_findings():
59
+ html = render_html_report(
60
+ target_ref="myimage:1",
61
+ source_label="trivy",
62
+ findings=[],
63
+ counts=EMPTY_COUNTS,
64
+ )
65
+ assert "No vulnerabilities found" in html
66
+
67
+
68
+ def test_render_html_escapes_special_chars():
69
+ finding = Finding(
70
+ source_type="test",
71
+ target='target<script>alert(1)</script>',
72
+ severity="LOW",
73
+ vulnerability_id="CVE-X",
74
+ package='pkg"quoted"',
75
+ installed_version="1.0",
76
+ fixed_version="-",
77
+ title="<b>bold</b>",
78
+ primary_url="",
79
+ )
80
+ counts = {**EMPTY_COUNTS, "LOW": 1}
81
+ html = render_html_report(
82
+ target_ref="ref",
83
+ source_label="test",
84
+ findings=[finding],
85
+ counts=counts,
86
+ )
87
+ assert "<script>alert(1)</script>" not in html
88
+ assert "&lt;script&gt;alert(1)&lt;/script&gt;" in html
89
+
90
+
91
+ def test_render_consolidated_dashboard_with_available_and_missing_reports(tmp_path):
92
+ reports_dir = tmp_path / "reports"
93
+ output_dir = tmp_path / "out"
94
+ reports_dir.mkdir()
95
+ output_dir.mkdir()
96
+
97
+ (reports_dir / "bandit-report.html").write_text("<html>bandit</html>", encoding="utf-8")
98
+ (output_dir / "trivy-report.html").write_text("<html>trivy</html>", encoding="utf-8")
99
+
100
+ findings = [
101
+ Finding(
102
+ source_type="bandit",
103
+ target="src/app.py",
104
+ severity="HIGH",
105
+ vulnerability_id="B101",
106
+ package="-",
107
+ installed_version="-",
108
+ fixed_version="-",
109
+ title="Assert used",
110
+ primary_url="",
111
+ ),
112
+ Finding(
113
+ source_type="trivy",
114
+ target="requirements.txt",
115
+ severity="CRITICAL",
116
+ vulnerability_id="CVE-1",
117
+ package="urllib3",
118
+ installed_version="2.6.3",
119
+ fixed_version="2.7.0",
120
+ title="Critical vuln",
121
+ primary_url="",
122
+ ),
123
+ Finding(
124
+ source_type="pip-audit",
125
+ target="requirements.txt",
126
+ severity="MEDIUM",
127
+ vulnerability_id="GHSA-1",
128
+ package="requests",
129
+ installed_version="2.0.0",
130
+ fixed_version="2.1.0",
131
+ title="Dependency finding",
132
+ primary_url="",
133
+ ),
134
+ ]
135
+
136
+ html = render_consolidated_dashboard_report(
137
+ target_ref="repo-root",
138
+ findings=findings,
139
+ counts={"CRITICAL": 1, "HIGH": 1, "MEDIUM": 1, "LOW": 0, "UNKNOWN": 0},
140
+ reports_dir=reports_dir,
141
+ output_dir=output_dir,
142
+ )
143
+
144
+ assert 'href="../reports/bandit-report.html"' in html
145
+ assert 'src="trivy-report.html"' in html
146
+ assert "Open Full Report" in html
147
+ assert "Generate this report first using scripts/render_pip_audit_html.sh or scripts/render_pip_audit_html.bat." in html
148
+ assert "scripts/render_pip_audit_html.sh" in html
149
+ assert "Bandit Report" in html
150
+ assert "Trivy Report" in html
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
155
+
156
+
157
+ def test_render_consolidated_dashboard_shows_all_tools_when_no_visible_tools(tmp_path):
158
+ reports_dir = tmp_path / "reports"
159
+ output_dir = tmp_path / "out"
160
+ reports_dir.mkdir()
161
+ output_dir.mkdir()
162
+
163
+ html = render_consolidated_dashboard_report(
164
+ target_ref="repo-root",
165
+ findings=[],
166
+ counts={"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0},
167
+ reports_dir=reports_dir,
168
+ output_dir=output_dir,
169
+ )
170
+
171
+ assert "pip-audit Report" in html
172
+ assert "GitLeaks Report" in html
173
+ assert "TruffleHog Report" in html
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
@@ -88,6 +88,79 @@ def test_parse_safety_json_basic():
88
88
  assert findings[0].fixed_version == "1.26.19"
89
89
 
90
90
 
91
+ def test_parse_safety_json_list_payload_and_helper_fallbacks():
92
+ payload = [
93
+ {
94
+ "id": "CVE-2026-1",
95
+ "package": "requests",
96
+ "installed_version": "2.0.0",
97
+ "fixed_version": "2.1.0",
98
+ "description": "Fallback description",
99
+ "url": "https://example.com/safety/url",
100
+ },
101
+ {
102
+ "CVE": "CVE-2026-2",
103
+ "package_name": "urllib3",
104
+ "analyzed_version": "1.0.0",
105
+ "fix_versions": ["1.1.0", "1.2.0"],
106
+ "summary": "Summary title",
107
+ "references": [{"url": "https://example.com/safety/ref"}],
108
+ "severity": {"name": "medium"},
109
+ },
110
+ {
111
+ "vulnerability_id": "CVE-2026-3",
112
+ "package_name": "idna",
113
+ "analyzed_version": "3.0",
114
+ "fixed_versions": [],
115
+ "references": ["https://example.com/safety/list-ref"],
116
+ "severity": {"value": "low"},
117
+ },
118
+ ]
119
+
120
+ findings = parse_safety_json(payload)
121
+
122
+ assert len(findings) == 3
123
+ assert findings[0].severity == "UNKNOWN"
124
+ assert findings[0].fixed_version == "2.1.0"
125
+ assert findings[0].title == "Fallback description"
126
+ assert findings[0].primary_url == "https://example.com/safety/url"
127
+ assert findings[1].severity == "MEDIUM"
128
+ assert findings[1].vulnerability_id == "CVE-2026-2"
129
+ assert findings[1].fixed_version == "1.1.0, 1.2.0"
130
+ assert findings[1].title == "Summary title"
131
+ assert findings[1].primary_url == "https://example.com/safety/ref"
132
+ assert findings[2].severity == "LOW"
133
+ assert findings[2].fixed_version == "-"
134
+ assert findings[2].title == "Safety finding"
135
+ assert findings[2].primary_url == "https://example.com/safety/list-ref"
136
+
137
+
138
+ def test_parse_safety_json_dict_filters_non_dict_items_and_handles_unknown_severity():
139
+ payload = {
140
+ "vulnerabilities": [
141
+ "skip-me",
142
+ {"package_name": "pkg", "severity": {"other": "none"}},
143
+ ]
144
+ }
145
+
146
+ findings = parse_safety_json(payload)
147
+
148
+ assert len(findings) == 1
149
+ assert findings[0].severity == "UNKNOWN"
150
+ assert findings[0].vulnerability_id == "-"
151
+ assert findings[0].package == "pkg"
152
+
153
+
154
+ def test_detect_source_type_safety_from_scanned_packages_metadata():
155
+ payload = {"scanned_packages": [], "vulnerabilities": []}
156
+ assert detect_source_type(payload) == "safety"
157
+
158
+
159
+ def test_detect_source_type_safety_from_cve_sample():
160
+ payload = {"vulnerabilities": [{"CVE": "CVE-2026-1000"}]}
161
+ assert detect_source_type(payload) == "safety"
162
+
163
+
91
164
  def test_count_by_severity_unknown_when_missing():
92
165
  payload = {
93
166
  "dependencies": [
@@ -1,83 +0,0 @@
1
- from sec_report_kit.models import Finding
2
- from sec_report_kit.report.html_renderer import render_html_report
3
-
4
-
5
- EMPTY_COUNTS = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
6
-
7
-
8
- def _finding(severity="HIGH", primary_url="https://example.com"):
9
- return Finding(
10
- source_type="test",
11
- target="myimage:1",
12
- severity=severity,
13
- vulnerability_id="CVE-2024-0001",
14
- package="openssl",
15
- installed_version="1.0",
16
- fixed_version="1.1",
17
- title="Test vuln",
18
- primary_url=primary_url,
19
- )
20
-
21
-
22
- def test_render_html_with_finding_with_url():
23
- counts = {**EMPTY_COUNTS, "HIGH": 1}
24
- html = render_html_report(
25
- target_ref="myimage:1",
26
- source_label="trivy",
27
- findings=[_finding(primary_url="https://example.com")],
28
- counts=counts,
29
- )
30
- assert "CVE-2024-0001" in html
31
- assert "https://example.com" in html
32
- assert "openssl" in html
33
- assert "This report is generated by" in html
34
- assert ">sec-report-kit<" in html
35
- assert "Connect with the developer:" in html
36
- assert ">Shan Konduru<" in html
37
- assert "https://www.linkedin.com/in/shankonduru/" in html
38
-
39
-
40
- def test_render_html_with_finding_no_url():
41
- counts = {**EMPTY_COUNTS, "HIGH": 1}
42
- html = render_html_report(
43
- target_ref="myimage:1",
44
- source_label="trivy",
45
- findings=[_finding(primary_url="")],
46
- counts=counts,
47
- )
48
- assert "CVE-2024-0001" in html
49
- # no anchor tag when url is empty
50
- assert 'href=""' not in html
51
-
52
-
53
- def test_render_html_no_findings():
54
- html = render_html_report(
55
- target_ref="myimage:1",
56
- source_label="trivy",
57
- findings=[],
58
- counts=EMPTY_COUNTS,
59
- )
60
- assert "No vulnerabilities found" in html
61
-
62
-
63
- def test_render_html_escapes_special_chars():
64
- finding = Finding(
65
- source_type="test",
66
- target='target<script>alert(1)</script>',
67
- severity="LOW",
68
- vulnerability_id="CVE-X",
69
- package='pkg"quoted"',
70
- installed_version="1.0",
71
- fixed_version="-",
72
- title="<b>bold</b>",
73
- primary_url="",
74
- )
75
- counts = {**EMPTY_COUNTS, "LOW": 1}
76
- html = render_html_report(
77
- target_ref="ref",
78
- source_label="test",
79
- findings=[finding],
80
- counts=counts,
81
- )
82
- assert "<script>alert(1)</script>" not in html
83
- assert "&lt;script&gt;alert(1)&lt;/script&gt;" in html
File without changes