sec-report-kit 0.2.0__tar.gz → 0.2.1__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.0 → sec_report_kit-0.2.1}/PKG-INFO +1 -1
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/pyproject.toml +1 -1
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/tests/test_cli.py +104 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/tests/test_mcp_server.py +100 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/tests/test_parsers.py +357 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.gitattributes +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.github/workflows/pip-audit.yml +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.github/workflows/publish-pypi.yml +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.github/workflows/publish-testpypi.yml +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.github/workflows/trivy-fs-audit.yml +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.github/workflows/unit-test-coverage-gate.yml +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/.gitignore +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/README.md +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/bandit-gaps-vs-sast-open-source-tools.md +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/install_tools.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/install_tools.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_bandit_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_bandit_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_checkov_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_checkov_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_codeql_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_codeql_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_gitleaks_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_gitleaks_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_osv_scanner_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_osv_scanner_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_pip_audit_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_pip_audit_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_semgrep_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_semgrep_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_tfsec_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_tfsec_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_trivy_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_trivy_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_trufflehog_html.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/render_trufflehog_html.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_checkov.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_checkov.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_codeql.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_codeql.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_osv_scanner.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_osv_scanner.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_pip_audit.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_pip_audit.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_semgrep.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_semgrep.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_tfsec.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_tfsec.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_trufflehog.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_trufflehog.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_unit_tests_with_coverage.bat +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/scripts/run_unit_tests_with_coverage.sh +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/__init__.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/__main__.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/cli.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/mcp/__init__.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/mcp/server.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/models.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/__init__.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/bandit.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/checkov.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/codeql.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/gitleaks.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/osv_scanner.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/pip_audit.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/semgrep.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/tfsec.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/trivy.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/parsers/trufflehog.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/report/__init__.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/report/html_renderer.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/services/__init__.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/services/normalize.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/src/sec_report_kit/services/summarize.py +0 -0
- {sec_report_kit-0.2.0 → sec_report_kit-0.2.1}/tests/test_html_renderer.py +0 -0
|
@@ -257,3 +257,107 @@ def test_main_module_runs():
|
|
|
257
257
|
with patch("sec_report_kit.cli.main") as mock_main:
|
|
258
258
|
runpy.run_module("sec_report_kit.__main__", run_name="__main__")
|
|
259
259
|
mock_main.assert_called_once()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
OSV_SCANNER_PAYLOAD = {
|
|
263
|
+
"results": [
|
|
264
|
+
{
|
|
265
|
+
"source": {"path": "requirements.txt"},
|
|
266
|
+
"packages": [
|
|
267
|
+
{
|
|
268
|
+
"package": {"name": "requests", "version": "2.0.0"},
|
|
269
|
+
"vulnerabilities": [{"id": "GHSA-1", "summary": "Issue"}],
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
CHECKOV_PAYLOAD = {
|
|
277
|
+
"results": {
|
|
278
|
+
"failed_checks": [
|
|
279
|
+
{
|
|
280
|
+
"check_id": "CKV_AWS_1",
|
|
281
|
+
"check_name": "Ensure no public bucket",
|
|
282
|
+
"severity": "HIGH",
|
|
283
|
+
"file_path": "main.tf",
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
TFSEC_PAYLOAD = {
|
|
290
|
+
"results": [
|
|
291
|
+
{
|
|
292
|
+
"rule_id": "AWS001",
|
|
293
|
+
"description": "S3 bucket publicly accessible",
|
|
294
|
+
"severity": "HIGH",
|
|
295
|
+
"location": {"filename": "main.tf"},
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_load_json_ndjson_branch(tmp_path):
|
|
302
|
+
"""_load_json falls back to NDJSON parsing when content is not valid JSON (lines 35-43).
|
|
303
|
+
|
|
304
|
+
Uses two JSON objects on separate lines (true NDJSON, invalid as a single JSON document)
|
|
305
|
+
with a blank line in between to exercise the 'continue' on blank lines.
|
|
306
|
+
"""
|
|
307
|
+
ndjson_content = (
|
|
308
|
+
'{"DetectorName":"AWS","SourceName":"repo","Verified":true}\n'
|
|
309
|
+
'\n'
|
|
310
|
+
'{"DetectorName":"GitHub","SourceName":"repo","Verified":false}\n'
|
|
311
|
+
)
|
|
312
|
+
input_file = tmp_path / "trufflehog.ndjson"
|
|
313
|
+
input_file.write_text(ndjson_content)
|
|
314
|
+
output_file = tmp_path / "report.html"
|
|
315
|
+
|
|
316
|
+
result = runner.invoke(
|
|
317
|
+
app,
|
|
318
|
+
["render", "trufflehog", "--input", str(input_file), "--output", str(output_file), "--target", "repo"],
|
|
319
|
+
)
|
|
320
|
+
assert result.exit_code == 0
|
|
321
|
+
assert output_file.exists()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_render_osv_scanner_command(tmp_path):
|
|
325
|
+
input_file = tmp_path / "osv.json"
|
|
326
|
+
input_file.write_text(json.dumps(OSV_SCANNER_PAYLOAD))
|
|
327
|
+
output_file = tmp_path / "osv.html"
|
|
328
|
+
|
|
329
|
+
result = runner.invoke(
|
|
330
|
+
app,
|
|
331
|
+
["render", "osv-scanner", "--input", str(input_file), "--output", str(output_file), "--target", "requirements.txt"],
|
|
332
|
+
)
|
|
333
|
+
assert result.exit_code == 0
|
|
334
|
+
assert output_file.exists()
|
|
335
|
+
assert "requests" in output_file.read_text()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_render_checkov_command(tmp_path):
|
|
339
|
+
input_file = tmp_path / "checkov.json"
|
|
340
|
+
input_file.write_text(json.dumps(CHECKOV_PAYLOAD))
|
|
341
|
+
output_file = tmp_path / "checkov.html"
|
|
342
|
+
|
|
343
|
+
result = runner.invoke(
|
|
344
|
+
app,
|
|
345
|
+
["render", "checkov", "--input", str(input_file), "--output", str(output_file), "--target", "terraform"],
|
|
346
|
+
)
|
|
347
|
+
assert result.exit_code == 0
|
|
348
|
+
assert output_file.exists()
|
|
349
|
+
assert "CKV_AWS_1" in output_file.read_text()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_render_tfsec_command(tmp_path):
|
|
353
|
+
input_file = tmp_path / "tfsec.json"
|
|
354
|
+
input_file.write_text(json.dumps(TFSEC_PAYLOAD))
|
|
355
|
+
output_file = tmp_path / "tfsec.html"
|
|
356
|
+
|
|
357
|
+
result = runner.invoke(
|
|
358
|
+
app,
|
|
359
|
+
["render", "tfsec", "--input", str(input_file), "--output", str(output_file), "--target", "terraform"],
|
|
360
|
+
)
|
|
361
|
+
assert result.exit_code == 0
|
|
362
|
+
assert output_file.exists()
|
|
363
|
+
assert "AWS001" in output_file.read_text()
|
|
@@ -214,3 +214,103 @@ def test_run_server_stdio_calls_run():
|
|
|
214
214
|
with patch("sec_report_kit.mcp.server.build_server", return_value=fake_mcp):
|
|
215
215
|
srv.run_server(transport="stdio")
|
|
216
216
|
fake_mcp.run.assert_called_once_with(transport="stdio")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------- _load_payload additional source types ----------
|
|
220
|
+
|
|
221
|
+
SEMGREP_PAYLOAD = {
|
|
222
|
+
"version": "1.0",
|
|
223
|
+
"results": [
|
|
224
|
+
{
|
|
225
|
+
"check_id": "python.lang.security.audit",
|
|
226
|
+
"path": "app.py",
|
|
227
|
+
"extra": {"severity": "HIGH", "message": "Issue"},
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
OSV_SCANNER_PAYLOAD = {
|
|
233
|
+
"results": [
|
|
234
|
+
{
|
|
235
|
+
"source": {"path": "requirements.txt"},
|
|
236
|
+
"packages": [
|
|
237
|
+
{
|
|
238
|
+
"package": {"name": "requests", "version": "2.0.0"},
|
|
239
|
+
"vulnerabilities": [{"id": "GHSA-1", "summary": "Issue"}],
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
CHECKOV_PAYLOAD = {
|
|
247
|
+
"results": {
|
|
248
|
+
"failed_checks": [
|
|
249
|
+
{
|
|
250
|
+
"check_id": "CKV_AWS_1",
|
|
251
|
+
"check_name": "Ensure no public bucket",
|
|
252
|
+
"severity": "HIGH",
|
|
253
|
+
"file_path": "main.tf",
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
TFSEC_PAYLOAD_SRV = {
|
|
260
|
+
"results": [
|
|
261
|
+
{"rule_id": "AWS001", "description": "Issue", "severity": "HIGH"}
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_load_payload_ndjson_branch(tmp_path):
|
|
267
|
+
"""_load_payload falls back to NDJSON parsing (lines 26-27).
|
|
268
|
+
|
|
269
|
+
Two objects on separate lines make the content invalid as a single JSON document,
|
|
270
|
+
forcing the json.JSONDecodeError fallback branch.
|
|
271
|
+
"""
|
|
272
|
+
ndjson = (
|
|
273
|
+
'{"DetectorName":"AWS","SourceName":"repo","Verified":true}\n'
|
|
274
|
+
'{"DetectorName":"GitHub","SourceName":"repo","Verified":false}\n'
|
|
275
|
+
)
|
|
276
|
+
p = tmp_path / "trufflehog.ndjson"
|
|
277
|
+
p.write_text(ndjson)
|
|
278
|
+
findings = srv._load_payload("trufflehog", str(p))
|
|
279
|
+
assert len(findings) == 2
|
|
280
|
+
assert findings[0].vulnerability_id == "AWS"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_load_payload_semgrep(tmp_path):
|
|
284
|
+
"""Covers semgrep branch in _load_payload (line 39)."""
|
|
285
|
+
p = tmp_path / "semgrep.json"
|
|
286
|
+
p.write_text(json.dumps(SEMGREP_PAYLOAD))
|
|
287
|
+
findings = srv._load_payload("semgrep", str(p))
|
|
288
|
+
assert len(findings) == 1
|
|
289
|
+
assert findings[0].vulnerability_id == "python.lang.security.audit"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_load_payload_osv_scanner(tmp_path):
|
|
293
|
+
"""Covers osv-scanner branch in _load_payload (line 43)."""
|
|
294
|
+
p = tmp_path / "osv.json"
|
|
295
|
+
p.write_text(json.dumps(OSV_SCANNER_PAYLOAD))
|
|
296
|
+
findings = srv._load_payload("osv-scanner", str(p))
|
|
297
|
+
assert len(findings) == 1
|
|
298
|
+
assert findings[0].package == "requests"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_load_payload_checkov(tmp_path):
|
|
302
|
+
"""Covers checkov branch in _load_payload (line 45)."""
|
|
303
|
+
p = tmp_path / "checkov.json"
|
|
304
|
+
p.write_text(json.dumps(CHECKOV_PAYLOAD))
|
|
305
|
+
findings = srv._load_payload("checkov", str(p))
|
|
306
|
+
assert len(findings) == 1
|
|
307
|
+
assert findings[0].vulnerability_id == "CKV_AWS_1"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_load_payload_tfsec(tmp_path):
|
|
311
|
+
"""Covers tfsec branch in _load_payload (line 47)."""
|
|
312
|
+
p = tmp_path / "tfsec.json"
|
|
313
|
+
p.write_text(json.dumps(TFSEC_PAYLOAD_SRV))
|
|
314
|
+
findings = srv._load_payload("tfsec", str(p))
|
|
315
|
+
assert len(findings) == 1
|
|
316
|
+
assert findings[0].vulnerability_id == "AWS001"
|
|
@@ -454,3 +454,360 @@ def test_parse_trufflehog_json_basic():
|
|
|
454
454
|
findings = parse_trufflehog_json(payload)
|
|
455
455
|
assert len(findings) == 1
|
|
456
456
|
assert findings[0].severity == "HIGH"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# ---------- trufflehog _extract_findings / _target branches ----------
|
|
460
|
+
|
|
461
|
+
def test_trufflehog_dict_with_results_list():
|
|
462
|
+
"""_extract_findings: dict with a top-level 'results' list (lines 16-17)."""
|
|
463
|
+
payload = {"results": [{"DetectorName": "GitHub", "Verified": False}]}
|
|
464
|
+
findings = parse_trufflehog_json(payload)
|
|
465
|
+
assert len(findings) == 1
|
|
466
|
+
assert findings[0].vulnerability_id == "GitHub"
|
|
467
|
+
assert findings[0].severity == "MEDIUM"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_trufflehog_extract_findings_returns_empty_for_non_matching_dict():
|
|
471
|
+
"""_extract_findings: dict with no matching keys → empty list (line 18)."""
|
|
472
|
+
payload = {"other_key": "value"}
|
|
473
|
+
findings = parse_trufflehog_json(payload)
|
|
474
|
+
assert findings == []
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_trufflehog_target_filesystem_path():
|
|
478
|
+
"""_target: SourceMetadata.Data.Filesystem.file branch (lines 24-28)."""
|
|
479
|
+
payload = [
|
|
480
|
+
{
|
|
481
|
+
"DetectorName": "AWS",
|
|
482
|
+
"Verified": True,
|
|
483
|
+
"SourceMetadata": {"Data": {"Filesystem": {"file": "config/secrets.env"}}},
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
findings = parse_trufflehog_json(payload)
|
|
487
|
+
assert findings[0].target == "config/secrets.env"
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def test_trufflehog_target_git_path():
|
|
491
|
+
"""_target: SourceMetadata.Data.Git.file branch (lines 29-31)."""
|
|
492
|
+
payload = [
|
|
493
|
+
{
|
|
494
|
+
"DetectorName": "AWS",
|
|
495
|
+
"Verified": True,
|
|
496
|
+
"SourceMetadata": {"Data": {"Git": {"file": "src/config.py"}}},
|
|
497
|
+
}
|
|
498
|
+
]
|
|
499
|
+
findings = parse_trufflehog_json(payload)
|
|
500
|
+
assert findings[0].target == "src/config.py"
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def test_trufflehog_target_falls_back_to_source_name():
|
|
504
|
+
"""_target: no SourceMetadata → fallback to SourceName."""
|
|
505
|
+
payload = [{"DetectorName": "AWS", "Verified": False, "SourceName": "my-repo"}]
|
|
506
|
+
findings = parse_trufflehog_json(payload)
|
|
507
|
+
assert findings[0].target == "my-repo"
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ---------- detect_source_type: trufflehog in results list (parsers/__init__ line 50) ----------
|
|
511
|
+
|
|
512
|
+
def test_detect_source_type_trufflehog_in_results_list():
|
|
513
|
+
"""detect_source_type: dict with results list containing DetectorName items."""
|
|
514
|
+
payload = {"results": [{"DetectorName": "GitHub", "Verified": True}]}
|
|
515
|
+
assert detect_source_type(payload) == "trufflehog"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ---------- codeql branches ----------
|
|
519
|
+
|
|
520
|
+
def test_codeql_rule_index_fallback():
|
|
521
|
+
"""_extract_rule_index + index fallback in _rule_info (lines 8-15, 31-38)."""
|
|
522
|
+
payload = {
|
|
523
|
+
"runs": [
|
|
524
|
+
{
|
|
525
|
+
"tool": {
|
|
526
|
+
"driver": {
|
|
527
|
+
"rules": [
|
|
528
|
+
{
|
|
529
|
+
"id": "py/path-injection",
|
|
530
|
+
"shortDescription": {"text": "Path injection"},
|
|
531
|
+
"helpUri": "https://codeql.github.com/path",
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
"results": [
|
|
537
|
+
{
|
|
538
|
+
"ruleId": "no-match-rule",
|
|
539
|
+
"level": "warning",
|
|
540
|
+
"message": {"text": "Some issue"},
|
|
541
|
+
"locations": [
|
|
542
|
+
{
|
|
543
|
+
"logicalLocations": [{"index": 0}]
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
findings = parse_codeql_json(payload)
|
|
552
|
+
assert len(findings) == 1
|
|
553
|
+
assert findings[0].vulnerability_id == "py/path-injection"
|
|
554
|
+
assert findings[0].severity == "MEDIUM"
|
|
555
|
+
assert findings[0].primary_url == "https://codeql.github.com/path"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_codeql_severity_unknown_when_level_missing():
|
|
559
|
+
"""severity = 'UNKNOWN' when level is not a string (line 51)."""
|
|
560
|
+
payload = {
|
|
561
|
+
"runs": [
|
|
562
|
+
{
|
|
563
|
+
"tool": {"driver": {"rules": []}},
|
|
564
|
+
"results": [{"ruleId": "some-rule", "message": {"text": "Issue"}}],
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
findings = parse_codeql_json(payload)
|
|
569
|
+
assert findings[0].severity == "UNKNOWN"
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_codeql_physical_location_target():
|
|
573
|
+
"""Physical location uri is used as target (lines 58-60)."""
|
|
574
|
+
payload = {
|
|
575
|
+
"runs": [
|
|
576
|
+
{
|
|
577
|
+
"tool": {"driver": {"rules": [{"id": "py/sql-injection", "shortDescription": {"text": "SQL"}}]}},
|
|
578
|
+
"results": [
|
|
579
|
+
{
|
|
580
|
+
"ruleId": "py/sql-injection",
|
|
581
|
+
"level": "error",
|
|
582
|
+
"message": {"text": "SQL injection"},
|
|
583
|
+
"locations": [
|
|
584
|
+
{
|
|
585
|
+
"physicalLocation": {
|
|
586
|
+
"artifactLocation": {"uri": "src/db/queries.py"}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
],
|
|
590
|
+
}
|
|
591
|
+
],
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
findings = parse_codeql_json(payload)
|
|
596
|
+
assert findings[0].target == "src/db/queries.py"
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_codeql_rule_index_none_when_no_logical_locations():
|
|
600
|
+
"""_extract_rule_index returns None when locations list is present but has no logicalLocations."""
|
|
601
|
+
payload = {
|
|
602
|
+
"runs": [
|
|
603
|
+
{
|
|
604
|
+
"tool": {"driver": {"rules": []}},
|
|
605
|
+
"results": [
|
|
606
|
+
{
|
|
607
|
+
"ruleId": "some-rule",
|
|
608
|
+
"level": "note",
|
|
609
|
+
"message": {"text": "Note"},
|
|
610
|
+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "app.py"}}}],
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
findings = parse_codeql_json(payload)
|
|
617
|
+
assert len(findings) == 1
|
|
618
|
+
assert findings[0].severity == "LOW"
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# ---------- osv-scanner severity branches ----------
|
|
622
|
+
|
|
623
|
+
def test_osv_scanner_severity_database_specific():
|
|
624
|
+
"""_severity: database_specific.severity branch (line 10)."""
|
|
625
|
+
payload = {
|
|
626
|
+
"results": [
|
|
627
|
+
{
|
|
628
|
+
"source": {"path": "requirements.txt"},
|
|
629
|
+
"packages": [
|
|
630
|
+
{
|
|
631
|
+
"package": {"name": "pkg", "version": "1.0"},
|
|
632
|
+
"vulnerabilities": [
|
|
633
|
+
{"id": "OSV-1", "database_specific": {"severity": "CRITICAL"}}
|
|
634
|
+
],
|
|
635
|
+
}
|
|
636
|
+
],
|
|
637
|
+
}
|
|
638
|
+
]
|
|
639
|
+
}
|
|
640
|
+
findings = parse_osv_scanner_json(payload)
|
|
641
|
+
assert findings[0].severity == "CRITICAL"
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def test_osv_scanner_severity_cvss_critical():
|
|
645
|
+
"""_severity: CVSS_V3 score >= 9.0 → CRITICAL (lines 12-20)."""
|
|
646
|
+
vuln = {"severity": [{"type": "CVSS_V3", "score": "9.5"}]}
|
|
647
|
+
payload = {
|
|
648
|
+
"results": [
|
|
649
|
+
{
|
|
650
|
+
"source": {"path": "requirements.txt"},
|
|
651
|
+
"packages": [{"package": {"name": "pkg", "version": "1.0"}, "vulnerabilities": [{"id": "X", **vuln}]}],
|
|
652
|
+
}
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
findings = parse_osv_scanner_json(payload)
|
|
656
|
+
assert findings[0].severity == "CRITICAL"
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_osv_scanner_severity_cvss_high():
|
|
660
|
+
"""_severity: CVSS_V3 score >= 7.0 → HIGH."""
|
|
661
|
+
vuln = {"severity": [{"type": "CVSS_V3", "score": "7.5"}]}
|
|
662
|
+
payload = {
|
|
663
|
+
"results": [
|
|
664
|
+
{
|
|
665
|
+
"source": {"path": "requirements.txt"},
|
|
666
|
+
"packages": [{"package": {"name": "pkg", "version": "1.0"}, "vulnerabilities": [{"id": "X", **vuln}]}],
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
}
|
|
670
|
+
findings = parse_osv_scanner_json(payload)
|
|
671
|
+
assert findings[0].severity == "HIGH"
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def test_osv_scanner_severity_cvss_medium():
|
|
675
|
+
"""_severity: CVSS_V3 score >= 4.0 → MEDIUM."""
|
|
676
|
+
vuln = {"severity": [{"type": "CVSS_V3", "score": "5.0"}]}
|
|
677
|
+
payload = {
|
|
678
|
+
"results": [
|
|
679
|
+
{
|
|
680
|
+
"source": {"path": "requirements.txt"},
|
|
681
|
+
"packages": [{"package": {"name": "pkg", "version": "1.0"}, "vulnerabilities": [{"id": "X", **vuln}]}],
|
|
682
|
+
}
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
findings = parse_osv_scanner_json(payload)
|
|
686
|
+
assert findings[0].severity == "MEDIUM"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def test_osv_scanner_severity_cvss_low():
|
|
690
|
+
"""_severity: CVSS_V3 score < 4.0 → LOW."""
|
|
691
|
+
vuln = {"severity": [{"type": "CVSS_V3", "score": "2.0"}]}
|
|
692
|
+
payload = {
|
|
693
|
+
"results": [
|
|
694
|
+
{
|
|
695
|
+
"source": {"path": "requirements.txt"},
|
|
696
|
+
"packages": [{"package": {"name": "pkg", "version": "1.0"}, "vulnerabilities": [{"id": "X", **vuln}]}],
|
|
697
|
+
}
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
findings = parse_osv_scanner_json(payload)
|
|
701
|
+
assert findings[0].severity == "LOW"
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_osv_scanner_severity_cvss_invalid_score():
|
|
705
|
+
"""_severity: CVSS_V3 score cannot be converted to float → UNKNOWN."""
|
|
706
|
+
vuln = {"severity": [{"type": "CVSS_V3", "score": "not-a-number"}]}
|
|
707
|
+
payload = {
|
|
708
|
+
"results": [
|
|
709
|
+
{
|
|
710
|
+
"source": {"path": "requirements.txt"},
|
|
711
|
+
"packages": [{"package": {"name": "pkg", "version": "1.0"}, "vulnerabilities": [{"id": "X", **vuln}]}],
|
|
712
|
+
}
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
findings = parse_osv_scanner_json(payload)
|
|
716
|
+
assert findings[0].severity == "UNKNOWN"
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def test_osv_scanner_vuln_id_falls_back_to_alias():
|
|
720
|
+
"""vuln_id uses aliases[0] when id is missing."""
|
|
721
|
+
payload = {
|
|
722
|
+
"results": [
|
|
723
|
+
{
|
|
724
|
+
"source": {"path": "requirements.txt"},
|
|
725
|
+
"packages": [
|
|
726
|
+
{
|
|
727
|
+
"package": {"name": "pkg", "version": "1.0"},
|
|
728
|
+
"vulnerabilities": [{"aliases": ["CVE-2024-9999"]}],
|
|
729
|
+
}
|
|
730
|
+
],
|
|
731
|
+
}
|
|
732
|
+
]
|
|
733
|
+
}
|
|
734
|
+
findings = parse_osv_scanner_json(payload)
|
|
735
|
+
assert findings[0].vulnerability_id == "CVE-2024-9999"
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def test_osv_scanner_primary_url_from_references():
|
|
739
|
+
"""primary_url extracted from references[0].url."""
|
|
740
|
+
payload = {
|
|
741
|
+
"results": [
|
|
742
|
+
{
|
|
743
|
+
"source": {"path": "requirements.txt"},
|
|
744
|
+
"packages": [
|
|
745
|
+
{
|
|
746
|
+
"package": {"name": "pkg", "version": "1.0"},
|
|
747
|
+
"vulnerabilities": [
|
|
748
|
+
{
|
|
749
|
+
"id": "OSV-2",
|
|
750
|
+
"references": [{"url": "https://example.com/advisory"}],
|
|
751
|
+
}
|
|
752
|
+
],
|
|
753
|
+
}
|
|
754
|
+
],
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
}
|
|
758
|
+
findings = parse_osv_scanner_json(payload)
|
|
759
|
+
assert findings[0].primary_url == "https://example.com/advisory"
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
# ---------- semgrep metadata branches ----------
|
|
763
|
+
|
|
764
|
+
def test_semgrep_severity_from_metadata():
|
|
765
|
+
"""_severity: falls back to metadata.severity when extra.severity missing (line 10)."""
|
|
766
|
+
payload = {
|
|
767
|
+
"results": [
|
|
768
|
+
{
|
|
769
|
+
"check_id": "python.security.audit",
|
|
770
|
+
"path": "app.py",
|
|
771
|
+
"extra": {"metadata": {"severity": "HIGH"}, "message": "Issue"},
|
|
772
|
+
}
|
|
773
|
+
]
|
|
774
|
+
}
|
|
775
|
+
findings = parse_semgrep_json(payload)
|
|
776
|
+
assert findings[0].severity == "HIGH"
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def test_semgrep_primary_url_from_metadata_references():
|
|
780
|
+
"""_primary_url: returns first URL from metadata.references list (lines 17-19)."""
|
|
781
|
+
payload = {
|
|
782
|
+
"results": [
|
|
783
|
+
{
|
|
784
|
+
"check_id": "python.security.audit",
|
|
785
|
+
"path": "app.py",
|
|
786
|
+
"extra": {
|
|
787
|
+
"severity": "MEDIUM",
|
|
788
|
+
"message": "Issue",
|
|
789
|
+
"metadata": {"references": ["https://example.com/rule"]},
|
|
790
|
+
},
|
|
791
|
+
}
|
|
792
|
+
]
|
|
793
|
+
}
|
|
794
|
+
findings = parse_semgrep_json(payload)
|
|
795
|
+
assert findings[0].primary_url == "https://example.com/rule"
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# ---------- tfsec start_line branch ----------
|
|
799
|
+
|
|
800
|
+
def test_tfsec_title_includes_start_line():
|
|
801
|
+
"""Title is decorated with filename:start_line when start_line is an int (line 16)."""
|
|
802
|
+
payload = {
|
|
803
|
+
"results": [
|
|
804
|
+
{
|
|
805
|
+
"rule_id": "AWS001",
|
|
806
|
+
"description": "S3 bucket is publicly accessible",
|
|
807
|
+
"severity": "HIGH",
|
|
808
|
+
"location": {"filename": "main.tf", "start_line": 42},
|
|
809
|
+
}
|
|
810
|
+
]
|
|
811
|
+
}
|
|
812
|
+
findings = parse_tfsec_json(payload)
|
|
813
|
+
assert "main.tf:42" in findings[0].title
|
|
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
|