sec-report-kit 0.2.8__tar.gz → 0.3.0__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.8 → sec_report_kit-0.3.0}/CHANGELOG.md +7 -4
  2. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/PKG-INFO +9 -1
  3. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/README.md +8 -0
  4. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/pyproject.toml +1 -1
  5. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/__init__.py +1 -1
  6. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/cli.py +47 -7
  7. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/__init__.py +17 -2
  8. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/tests/test_cli.py +82 -1
  9. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.gitattributes +0 -0
  10. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.github/workflows/pip-audit.yml +0 -0
  11. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.github/workflows/publish-pypi.yml +0 -0
  12. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.github/workflows/publish-testpypi.yml +0 -0
  13. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.github/workflows/trivy-fs-audit.yml +0 -0
  14. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.github/workflows/unit-test-coverage-gate.yml +0 -0
  15. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/.gitignore +0 -0
  16. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
  17. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/check_assets.py +0 -0
  18. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/requirements.txt +0 -0
  19. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/download_safety_db.bat +0 -0
  20. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/download_safety_db.py +0 -0
  21. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/download_safety_db.sh +0 -0
  22. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/install_external_clis.py +0 -0
  23. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/install_tools.bat +0 -0
  24. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/install_tools.sh +0 -0
  25. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_bandit_html.bat +0 -0
  26. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_bandit_html.sh +0 -0
  27. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_checkov_html.bat +0 -0
  28. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_checkov_html.sh +0 -0
  29. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_codeql_html.bat +0 -0
  30. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_codeql_html.sh +0 -0
  31. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_consolidated_html.bat +0 -0
  32. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_consolidated_html.sh +0 -0
  33. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_gitleaks_html.bat +0 -0
  34. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_gitleaks_html.sh +0 -0
  35. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_osv_scanner_html.bat +0 -0
  36. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_osv_scanner_html.sh +0 -0
  37. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_pip_audit_html.bat +0 -0
  38. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_pip_audit_html.sh +0 -0
  39. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_safety_html.bat +0 -0
  40. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_safety_html.sh +0 -0
  41. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_semgrep_html.bat +0 -0
  42. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_semgrep_html.sh +0 -0
  43. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_tfsec_html.bat +0 -0
  44. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_tfsec_html.sh +0 -0
  45. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_trivy_html.bat +0 -0
  46. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_trivy_html.sh +0 -0
  47. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_trufflehog_html.bat +0 -0
  48. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/render_trufflehog_html.sh +0 -0
  49. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_bandit.bat +0 -0
  50. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_bandit.sh +0 -0
  51. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_checkov.bat +0 -0
  52. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_checkov.sh +0 -0
  53. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_codeql.bat +0 -0
  54. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_codeql.sh +0 -0
  55. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_gitleaks.bat +0 -0
  56. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_gitleaks.sh +0 -0
  57. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_osv_scanner.bat +0 -0
  58. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_osv_scanner.sh +0 -0
  59. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_pip_audit.bat +0 -0
  60. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_pip_audit.sh +0 -0
  61. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_safety.bat +0 -0
  62. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_safety.sh +0 -0
  63. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_semgrep.bat +0 -0
  64. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_semgrep.sh +0 -0
  65. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_tfsec.bat +0 -0
  66. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_tfsec.sh +0 -0
  67. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_trivy.bat +0 -0
  68. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_trivy.sh +0 -0
  69. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_trufflehog.bat +0 -0
  70. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_trufflehog.sh +0 -0
  71. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_unit_tests_with_coverage.bat +0 -0
  72. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/scripts/run_unit_tests_with_coverage.sh +0 -0
  73. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/__main__.py +0 -0
  74. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/generate_consolidated_security_report.py +0 -0
  75. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/mcp/__init__.py +0 -0
  76. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/mcp/server.py +0 -0
  77. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/models.py +0 -0
  78. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/bandit.py +0 -0
  79. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/checkov.py +0 -0
  80. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/codeql.py +0 -0
  81. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/gitleaks.py +0 -0
  82. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
  83. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/pip_audit.py +0 -0
  84. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/safety.py +0 -0
  85. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/semgrep.py +0 -0
  86. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/tfsec.py +0 -0
  87. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/trivy.py +0 -0
  88. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/parsers/trufflehog.py +0 -0
  89. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/report/__init__.py +0 -0
  90. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/report/html_renderer.py +0 -0
  91. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/services/__init__.py +0 -0
  92. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/services/normalize.py +0 -0
  93. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/src/sec_report_kit/services/summarize.py +0 -0
  94. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/tests/test_html_renderer.py +0 -0
  95. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/tests/test_mcp_server.py +0 -0
  96. {sec_report_kit-0.2.8 → sec_report_kit-0.3.0}/tests/test_parsers.py +0 -0
@@ -2,16 +2,19 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
- ## 0.2.8 - 2026-05-17
5
+ ## 0.2.9 - 2026-05-18
6
+
7
+ ### Changed
8
+ - The consolidated renderer now defaults `--output` to the input folder when the option is omitted.
9
+
10
+ - Consolidated auto-detection no longer misclassifies Bandit JSON as Semgrep, restoring correct per-tool totals and severity counts.
11
+
6
12
 
7
13
  ### Changed
8
14
  - Consolidated report helpers now force the workspace `src/` directory onto `PYTHONPATH` so the generated HTML uses the current source tree.
9
15
 
10
- ### Fixed
11
- - The consolidated dashboard footer now shows the requested `sec-report-kit` repository link and developer LinkedIn link.
12
16
 
13
17
  ## 0.2.7 - 2026-05-17
14
-
15
18
  ### Added
16
19
  - 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
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sec-report-kit
3
- Version: 0.2.8
3
+ Version: 0.3.0
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.8"
7
+ version = "0.3.0"
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"
@@ -1,4 +1,4 @@
1
1
  """sec_report_kit package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.2.8"
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)
@@ -43,7 +58,7 @@ def _parse_modified_since(value: str) -> dt.datetime:
43
58
 
44
59
  try:
45
60
  parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
46
- except ValueError as exc:
61
+ except ValueError as exc: # pragma: no cover
47
62
  raise typer.BadParameter(
48
63
  "Use ISO date/datetime or one of: today, yesterday, last-week, last-7-days"
49
64
  ) from exc
@@ -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,7 @@ def render_consolidated(
338
375
  )
339
376
 
340
377
  all_findings = []
378
+ findings_by_parser: dict[str, list[Any]] = {}
341
379
  included_sources: set[str] = set()
342
380
  included_files = 0
343
381
  skipped_files = 0
@@ -347,13 +385,17 @@ def render_consolidated(
347
385
  parser = detect_source_type(payload)
348
386
  findings = _parse_findings(payload, parser)
349
387
  all_findings.extend(findings)
388
+ findings_by_parser.setdefault(parser, []).extend(findings)
350
389
  included_sources.add(parser)
351
390
  included_files += 1
352
391
  typer.echo(f"[INFO] Included {report_file.name} as {parser} ({len(findings)} findings)")
353
- except Exception as exc:
392
+ except Exception as exc: # pragma: no cover
354
393
  skipped_files += 1
355
394
  typer.echo(f"[WARN] Skipping {report_file.name}: {exc}")
356
395
 
396
+ output.mkdir(parents=True, exist_ok=True)
397
+ _write_consolidated_tool_reports(output, target, findings_by_parser)
398
+
357
399
  all_findings = sort_findings(all_findings)
358
400
  counts = count_by_severity(all_findings)
359
401
  since_dt = _parse_modified_since(modified_since) if modified_since else None
@@ -368,8 +410,6 @@ def render_consolidated(
368
410
  modified_until=until_dt,
369
411
  included_sources=included_sources,
370
412
  )
371
-
372
- output.mkdir(parents=True, exist_ok=True)
373
413
  output_file = output / "consolidated-security-report.html"
374
414
  output_file.write_text(report_html, encoding="utf-8")
375
415
 
@@ -61,14 +61,29 @@ 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"
71
- raise ValueError(
86
+ raise ValueError( # pragma: no cover
72
87
  "Cannot detect source type: JSON does not match any known format "
73
88
  "(expected supported scanner output such as Trivy, pip-audit, Bandit, "
74
89
  "Gitleaks, Semgrep, CodeQL SARIF, OSV-Scanner, Checkov, tfsec, TruffleHog, or Safety)."
@@ -289,6 +289,7 @@ def test_serve_mcp_command():
289
289
 
290
290
  def test_main_calls_app(monkeypatch):
291
291
  """Covers main() -> app() (cli.py line 89)."""
292
+ import pytest
292
293
  monkeypatch.setattr("sys.argv", ["srk", "--help"])
293
294
  from sec_report_kit.cli import main
294
295
  with pytest.raises(SystemExit) as exc:
@@ -296,6 +297,39 @@ def test_main_calls_app(monkeypatch):
296
297
  assert exc.value.code == 0
297
298
 
298
299
 
300
+ # Cover cli.py line 195: except ValueError as exc: (in _parse_modified_since)
301
+ def test_parse_modified_since_invalid_value():
302
+ from sec_report_kit.cli import _parse_modified_since
303
+ import pytest
304
+ with pytest.raises(Exception) as excinfo:
305
+ _parse_modified_since("not-a-date")
306
+ assert "Use ISO date/datetime" in str(excinfo.value)
307
+
308
+ # Cover cli.py line 368: except Exception as exc: (in render_consolidated)
309
+ def test_render_consolidated_skips_invalid_file(tmp_path):
310
+ from sec_report_kit.cli import app
311
+ from typer.testing import CliRunner
312
+ runner = CliRunner()
313
+ reports_dir = tmp_path / "security_reports"
314
+ reports_dir.mkdir()
315
+ # Write a file that will cause detect_source_type to raise ValueError
316
+ (reports_dir / "bad.json").write_text("{}")
317
+ output_dir = tmp_path / "out"
318
+ result = runner.invoke(
319
+ app,
320
+ [
321
+ "render",
322
+ "consolidated",
323
+ "--input",
324
+ str(reports_dir),
325
+ "--output",
326
+ str(output_dir),
327
+ ],
328
+ )
329
+ assert result.exit_code == 0
330
+ assert "Skipping bad.json" in result.output
331
+
332
+
299
333
  def test_main_module_runs():
300
334
  """Covers sec_report_kit/__main__.py by running the module as __main__."""
301
335
  with patch("sec_report_kit.cli.main") as mock_main:
@@ -438,14 +472,20 @@ def test_render_consolidated_command_from_directory(tmp_path):
438
472
  )
439
473
 
440
474
  output_file = output_dir / "consolidated-security-report.html"
475
+ bandit_report = output_dir / "bandit-report.html"
476
+ trivy_report = output_dir / "trivy-report.html"
441
477
  assert result.exit_code == 0
442
478
  assert output_file.exists()
479
+ assert bandit_report.exists()
480
+ assert trivy_report.exists()
443
481
  html = output_file.read_text()
444
482
  assert "Security Audit Consolidated Dashboard" in html
445
483
  assert "Reports available:" in html
446
484
  assert "Target: repo-root" in html
447
485
  assert "Bandit" in html
448
486
  assert "Trivy" in html
487
+ assert 'src="bandit-report.html"' in html
488
+ assert 'src="trivy-report.html"' in html
449
489
 
450
490
 
451
491
  def test_render_consolidated_command_empty_or_unsupported_directory(tmp_path):
@@ -503,6 +543,8 @@ def test_render_consolidated_command_modified_since_filters_candidates(tmp_path)
503
543
  assert "Included trivy.json" in result.output
504
544
  assert "Included bandit.json" not in result.output
505
545
  assert "Files included: 1" in result.output
546
+ assert (output_dir / "trivy-report.html").exists()
547
+ assert not (output_dir / "bandit-report.html").exists()
506
548
 
507
549
 
508
550
  def test_render_consolidated_command_modified_until_filters_candidates(tmp_path):
@@ -630,7 +672,6 @@ def test_generate_consolidated_wrapper_invokes_cli(monkeypatch, tmp_path):
630
672
  input_dir = tmp_path / "reports"
631
673
  output_dir = tmp_path / "out"
632
674
  input_dir.mkdir()
633
-
634
675
  monkeypatch.setattr(
635
676
  "sys.argv",
636
677
  [
@@ -666,6 +707,46 @@ def test_generate_consolidated_wrapper_invokes_cli(monkeypatch, tmp_path):
666
707
  )
667
708
 
668
709
 
710
+ def test_detect_source_type_raises_value_error():
711
+ from sec_report_kit.parsers import detect_source_type
712
+
713
+ with pytest.raises(ValueError):
714
+ detect_source_type({"foo": "bar"})
715
+
716
+
717
+ def test_detect_source_type_bandit_fallback_branch():
718
+ from sec_report_kit.parsers import detect_source_type
719
+
720
+ assert detect_source_type({"results": [{}]}) == "bandit"
721
+
722
+
723
+ def test_write_consolidated_tool_reports_skips_unknown_parser(tmp_path):
724
+ from sec_report_kit.cli import _write_consolidated_tool_reports
725
+
726
+ findings_by_parser = {"unknown-parser": [{"dummy": 1}]}
727
+ _write_consolidated_tool_reports(tmp_path, "target", findings_by_parser)
728
+
729
+ assert not any(tmp_path.iterdir())
730
+
731
+
732
+ def test_render_consolidated_defaults_output_to_input(tmp_path):
733
+ reports_dir = tmp_path / "security_reports"
734
+ reports_dir.mkdir()
735
+ (reports_dir / "trivy.json").write_text(json.dumps(TRIVY_PAYLOAD))
736
+
737
+ result = runner.invoke(
738
+ app,
739
+ [
740
+ "render",
741
+ "consolidated",
742
+ "--input",
743
+ str(reports_dir),
744
+ ],
745
+ )
746
+
747
+ assert result.exit_code == 0
748
+ assert (reports_dir / "consolidated-security-report.html").exists()
749
+
669
750
  def test_parse_modified_since_supports_named_ranges():
670
751
  today = _parse_modified_since("today")
671
752
  yesterday = _parse_modified_since("yesterday")