From a859ce258b0b84180210fe15202bf0c574f862c0 Mon Sep 17 00:00:00 2001 From: popie52 Date: Sat, 3 Oct 2026 22:39:10 +0530 Subject: [PATCH] fix(scanner): scope Trivy config scans to Dockerfile --- docksec/findings.py | 29 +++++++++---------------- tests/test_findings.py | 49 +++++++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docksec/findings.py b/docksec/findings.py index 3b8518b..ae54b1c 100644 --- a/docksec/findings.py +++ b/docksec/findings.py @@ -160,13 +160,12 @@ def parse_hadolint_json(raw: str, target: str) -> List[Dict]: def parse_trivy_config_json( - raw: str, target: str, target_basename: Optional[str] = None + raw: str, target: str ) -> List[Dict]: """Convert ``trivy config -f json`` output into structured findings. - ``trivy config`` scans a directory, so its output can include other config - files that happen to sit beside the Dockerfile. ``target_basename`` limits - the results to the file actually being scanned. + Trivy is invoked with the specific Dockerfile path, so its output is expected + to contain findings for that Dockerfile only. """ if not raw or not raw.strip(): return [] @@ -179,10 +178,6 @@ def parse_trivy_config_json( findings = [] for result in (payload or {}).get("Results") or []: - if target_basename: - result_target = str(result.get("Target") or "") - if os.path.basename(result_target) != target_basename: - continue for misconfig in result.get("Misconfigurations") or []: rule_id = normalize_rule_id( misconfig.get("ID") or misconfig.get("AVDID") or "" @@ -266,24 +261,20 @@ def run_hadolint(dockerfile_path: str) -> Tuple[bool, List[Dict], Optional[str]] def run_trivy_config(dockerfile_path: str, offline: bool = False) -> Tuple[bool, List[Dict], Optional[str]]: - """Run ``trivy config`` against the Dockerfile's directory. + """Run ``trivy config`` against the specified Dockerfile. - ``trivy config`` only accepts a directory - given a file it prints its usage - text and exits 0, which parses as "no findings" rather than an error. The - directory is scanned and the results are then filtered back down to the - target file. ``--file-patterns`` narrows the scan so an unusually named - Dockerfile (``Dockerfile.prod``) is still analyzed as one. + Trivy accepts a Dockerfile path directly, so scanning the parent directory + is unnecessary and can cause findings from nested Dockerfiles with the same + basename to be attributed to the requested Dockerfile. """ - directory = os.path.dirname(os.path.abspath(dockerfile_path)) or "." - basename = os.path.basename(dockerfile_path) + dockerfile_path = os.path.abspath(dockerfile_path) cmd = [ "trivy", "config", "-f", "json", "--quiet", "--skip-version-check", - "--file-patterns", f"dockerfile:{basename}", ] if offline: cmd.append("--skip-check-update") - cmd.append(directory) + cmd.append(dockerfile_path) try: result = subprocess.run( @@ -309,7 +300,7 @@ def run_trivy_config(dockerfile_path: str, offline: bool = False) -> Tuple[bool, if not stdout.startswith("{"): return False, [], "Trivy config scan returned unexpected output (not JSON)" - return True, parse_trivy_config_json(stdout, dockerfile_path, basename), None + return True, parse_trivy_config_json(stdout, dockerfile_path), None def scan_dockerfile_findings( diff --git a/tests/test_findings.py b/tests/test_findings.py index 700a76d..70e9ac0 100644 --- a/tests/test_findings.py +++ b/tests/test_findings.py @@ -7,6 +7,7 @@ import json import unittest +from unittest.mock import patch from docksec.findings import ( HADOLINT_SEVERITY, @@ -14,6 +15,7 @@ deduplicate, parse_hadolint_json, parse_trivy_config_json, + run_trivy_config, summarize, ) @@ -87,22 +89,49 @@ def test_missing_line_is_none_not_zero(self): raw = _trivy_result([_misconfig("DS026", "LOW")]) self.assertIsNone(parse_trivy_config_json(raw, "Dockerfile")[0]["Line"]) - def test_results_for_other_files_are_filtered_out(self): - """trivy config scans a directory, so it can report on files beside the - Dockerfile.""" - payload = json.dumps({"Results": [ - {"Target": "Dockerfile", "Misconfigurations": [_misconfig("DS002")]}, - {"Target": "other.yaml", "Misconfigurations": [_misconfig("DS999")]}, - ]}) - findings = parse_trivy_config_json(payload, "Dockerfile", target_basename="Dockerfile") - self.assertEqual([f["VulnerabilityID"] for f in findings], ["DS002"]) - def test_malformed_input_is_survivable(self): for raw in ("", "not json", "{}", '{"Results": null}'): with self.subTest(raw=raw): self.assertEqual(parse_trivy_config_json(raw, "Dockerfile"), []) +class TestTrivyConfigExecution(unittest.TestCase): + def test_scans_exact_dockerfile_path(self): + """Trivy must receive the Dockerfile path, not its parent directory.""" + + raw = _trivy_result([_misconfig("DS002", "HIGH", 7)], target="/repo/Dockerfile") + + with patch("docksec.findings.subprocess.run") as mock_run: + mock_run.return_value.stdout = raw + mock_run.return_value.stderr = "" + mock_run.return_value.returncode = 0 + + ok, findings, error = run_trivy_config("/repo/Dockerfile") + + self.assertTrue(ok) + self.assertIsNone(error) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0]["VulnerabilityID"], "DS002") + + command = mock_run.call_args.args[0] + + self.assertEqual(command, ["trivy", "config", "-f", "json", "--quiet", "--skip-version-check", "/repo/Dockerfile"]) + + def test_offline_mode_adds_skip_check_update(self): + + with patch("docksec.findings.subprocess.run") as mock_run: + mock_run.return_value.stdout = "{}" + mock_run.return_value.stderr = "" + mock_run.return_value.returncode = 0 + + run_trivy_config("/repo/Dockerfile", offline=True) + + command = mock_run.call_args.args[0] + + self.assertIn("--skip-check-update", command) + self.assertEqual(command[-1], "/repo/Dockerfile") + + class TestDeduplication(unittest.TestCase): """Both scanners run because neither is a superset; where they overlap the Trivy finding wins, since its IDs are stable and its severities are real."""