From e390dc5d5752889d2b3b4fbafc40e76541f8ff0d Mon Sep 17 00:00:00 2001 From: Manuel Solis Date: Fri, 24 Jul 2026 11:47:01 -0600 Subject: [PATCH 1/3] fix(engine_iac): handle checkov list output when scanning multiple frameworks Checkov returns a JSON list (one entry per framework) instead of a single dict when more than one framework is scanned in the same run (e.g. framework=[terraform, terraform_plan], triggered by --platform terraform/all with RULES_TERRAFORM). _async_scan always did result.append(json.loads(output)), assuming a dict. When output was actually a list, it produced a nested list inside result_scans, which later made CheckovDeserealizator fail with: list indices must be integers or slices, not str at checkov_deserealizator.py line 19 (result[results]). Fix is backward compatible: extend result with parsed_output when it is a list, keep append for the single-dict case, so other subsidiaries relying on the single-framework behavior see no change. Added regression test test_async_scan_with_multiple_frameworks_list_output. --- .../driven_adapters/checkov/checkov_tool.py | 11 ++++++- .../checkov/test_checkov_tool.py | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py index 4b6041fca..7a5091e91 100755 --- a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py +++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py @@ -413,7 +413,16 @@ def _async_scan(self, queue, checkov_config: CheckovConfig, command_prefix): result = [] try: output = self._execute(checkov_config, command_prefix) - result.append(json.loads(output)) + parsed_output = json.loads(output) + # Checkov returns a JSON list (one entry per framework) instead of + # a single dict when more than one framework is scanned in the + # same run (e.g. framework=["terraform", "terraform_plan"]). + # Extend instead of append so downstream code always receives a + # flat list of per-framework result dicts. + if isinstance(parsed_output, list): + result.extend(parsed_output) + else: + result.append(parsed_output) except json.JSONDecodeError as e: error_msg = f"Failed to parse Checkov output as JSON: {e}" logger.error(error_msg) diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py index 6d68c6015..9a0fdd421 100755 --- a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py +++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py @@ -110,6 +110,37 @@ def test_async_scan(mock_checkov_tool, checkov_tool): assert output_queue.get() == [{"key": "value"}] +@patch( + "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.checkov.checkov_tool.CheckovTool._execute", + autospec=True, +) +def test_async_scan_with_multiple_frameworks_list_output(mock_checkov_tool, checkov_tool): + # When checkov scans more than one framework in the same run + # (e.g. framework=["terraform", "terraform_plan"]) it returns a JSON + # list with one result dict per framework instead of a single dict. + checkov_config = MagicMock() + checkov_config.path_config_file = "/path/to/config/" + checkov_config.config_file_name = "checkov_config" + + output_queue = Queue() + + mock_checkov_tool.return_value = ( + '[{"check_type": "terraform", "results": {"failed_checks": []}}, ' + '{"check_type": "terraform_plan", "results": {"failed_checks": []}}]' + ) + + checkov_tool._async_scan(output_queue, checkov_config, "checkov") + + result = output_queue.get() + assert result == [ + {"check_type": "terraform", "results": {"failed_checks": []}}, + {"check_type": "terraform_plan", "results": {"failed_checks": []}}, + ] + # Each entry must be a dict, never a nested list, otherwise downstream + # CheckovDeserealizator.get_list_finding fails with + # "list indices must be integers or slices, not str". + assert all(isinstance(entry, dict) for entry in result) + @patch( "devsecops_engine_tools.engine_sast.engine_iac.src.infrastructure.driven_adapters.checkov.checkov_tool.CheckovTool._execute", autospec=True, From da4c86fa67dd0d613741f2aef356e741150704d0 Mon Sep 17 00:00:00 2001 From: Manuel Solis Date: Fri, 24 Jul 2026 12:25:22 -0600 Subject: [PATCH 2/3] style(engine_iac): trim inline comments on checkov list-output fix Explanatory detail already lives in the fix commit message per repo conventions; keep the diff itself minimal. --- .../infrastructure/driven_adapters/checkov/checkov_tool.py | 5 ----- .../driven_adapters/checkov/test_checkov_tool.py | 6 ------ 2 files changed, 11 deletions(-) diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py index 7a5091e91..b29b18218 100755 --- a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py +++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_tool.py @@ -414,11 +414,6 @@ def _async_scan(self, queue, checkov_config: CheckovConfig, command_prefix): try: output = self._execute(checkov_config, command_prefix) parsed_output = json.loads(output) - # Checkov returns a JSON list (one entry per framework) instead of - # a single dict when more than one framework is scanned in the - # same run (e.g. framework=["terraform", "terraform_plan"]). - # Extend instead of append so downstream code always receives a - # flat list of per-framework result dicts. if isinstance(parsed_output, list): result.extend(parsed_output) else: diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py index 9a0fdd421..647511189 100755 --- a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py +++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_tool.py @@ -115,9 +115,6 @@ def test_async_scan(mock_checkov_tool, checkov_tool): autospec=True, ) def test_async_scan_with_multiple_frameworks_list_output(mock_checkov_tool, checkov_tool): - # When checkov scans more than one framework in the same run - # (e.g. framework=["terraform", "terraform_plan"]) it returns a JSON - # list with one result dict per framework instead of a single dict. checkov_config = MagicMock() checkov_config.path_config_file = "/path/to/config/" checkov_config.config_file_name = "checkov_config" @@ -136,9 +133,6 @@ def test_async_scan_with_multiple_frameworks_list_output(mock_checkov_tool, chec {"check_type": "terraform", "results": {"failed_checks": []}}, {"check_type": "terraform_plan", "results": {"failed_checks": []}}, ] - # Each entry must be a dict, never a nested list, otherwise downstream - # CheckovDeserealizator.get_list_finding fails with - # "list indices must be integers or slices, not str". assert all(isinstance(entry, dict) for entry in result) @patch( From 74e5e48ea4973f2c4b31cca643e16094c7298417 Mon Sep 17 00:00:00 2001 From: Manuel Solis Date: Fri, 24 Jul 2026 16:53:10 -0600 Subject: [PATCH 3/3] fix(engine_iac): stop false-positive error detection in checkov deserializer get_list_finding used "'error' in str(result)" to detect failed scans. This substring check false-positives whenever a legitimate failed check's name/guideline/resource text contains the word error (common in security rule descriptions), discarding all already-collected findings and raising Exception(None) since result.get(error) returns None on a normal results dict. Replaced with isinstance(result, dict) and error in result, which only matches the real error dicts built in CheckovTool._async_scan (e.g. subprocess/JSON decode failures). Added regression tests: one for the false-positive case (finding text containing error), one confirming the real error path still raises. --- .../checkov/checkov_deserealizator.py | 2 +- .../checkov/test_checkov_deserealizator.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_deserealizator.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_deserealizator.py index 013ed6f80..c7f8a8817 100644 --- a/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_deserealizator.py +++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/src/infrastructure/driven_adapters/checkov/checkov_deserealizator.py @@ -46,6 +46,6 @@ def get_list_finding( ) list_open_findings.append(finding_open) - if "'error'" in str(result): + if isinstance(result, dict) and "error" in result: raise Exception(result.get("error")) return list_open_findings diff --git a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_deserealizator.py b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_deserealizator.py index 9f4576a55..b0fbf6ea9 100644 --- a/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_deserealizator.py +++ b/tools/devsecops_engine_tools/engine_sast/engine_iac/test/infrastructure/driven_adapters/checkov/test_checkov_deserealizator.py @@ -6,6 +6,7 @@ Category, ) from datetime import datetime +import pytest def test_get_list_finding(): results_scan_list = [ @@ -130,3 +131,38 @@ def test_get_list_finding(): ) assert list_findings == list_findings_compare + + +def test_get_list_finding_does_not_raise_when_check_text_contains_error_word(): + results_scan_list = [ + { + "check_type": "terraform", + "results": { + "failed_checks": [ + { + "check_id": "CKV_AWS_1", + "check_name": "Ensure proper error handling is configured", + "resource": "aws_lambda_function.this", + "repo_file_path": "/main.tf", + "guideline": "Add error handling", + } + ] + }, + } + ] + + list_findings = CheckovDeserealizator.get_list_finding( + results_scan_list, {}, "high", "vulnerability" + ) + + assert len(list_findings) == 1 + assert list_findings[0].id == "CKV_AWS_1" + + +def test_get_list_finding_raises_on_real_error_result(): + results_scan_list = [ + {"error": "Checkov execution failed", "checkov_config": "terraform"} + ] + + with pytest.raises(Exception, match="Checkov execution failed"): + CheckovDeserealizator.get_list_finding(results_scan_list, {}, "high", "vulnerability")