From 92927929481429e3456acfab28859894665a9844 Mon Sep 17 00:00:00 2001 From: Barak Fatal Date: Sun, 5 Oct 2025 16:56:47 +0300 Subject: [PATCH 1/3] Added check to validate we are not editing an empty list when evaluating after_unknown --- checkov/terraform/plan_parser.py | 16 ++++++++++++++-- tests/terraform/parser/test_plan_parser.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/checkov/terraform/plan_parser.py b/checkov/terraform/plan_parser.py index c5015b8b45..224ff086f1 100644 --- a/checkov/terraform/plan_parser.py +++ b/checkov/terraform/plan_parser.py @@ -263,9 +263,21 @@ def _handle_complex_after_unknown(k: str, resource_conf: dict[str, Any], v: Any) continue if inner_key not in resource_conf[k]: if isinstance(resource_conf[k][0], dict): - resource_conf[k][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) + if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf): + resource_conf[k][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) elif isinstance(resource_conf[k][0], list) and isinstance(resource_conf[k][0][0], dict): - resource_conf[k][0][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) + if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf): + resource_conf[k][0][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) + + +def _validate_after_unknown_list_not_empty(inner_key: str, k: str, resource_conf: dict[str, Any]) -> bool: + """ + If the inner key is a list - we want to check it's not empty, if not we handle it in the original way. + """ + value = resource_conf[k][0] + return ((inner_key not in value) or + (inner_key in value and not isinstance(value[inner_key], list)) or + (isinstance(value[inner_key], list) and value[inner_key] != [] and value[inner_key][0] != [])) def _find_child_modules( diff --git a/tests/terraform/parser/test_plan_parser.py b/tests/terraform/parser/test_plan_parser.py index bddd628131..d591a211bf 100644 --- a/tests/terraform/parser/test_plan_parser.py +++ b/tests/terraform/parser/test_plan_parser.py @@ -146,6 +146,23 @@ def test_handle_complex_after_unknown(self): _handle_complex_after_unknown(key, resource, value) assert resource == {'tags': [[{'custom_tags': ['true_after_unknown']}]]} + def test_handle_complex_after_unknown_with_empty_list(self): + resource = {"network_configuration": [ + { + "endpoint_configuration": [ + ] + } + ]} + key: str = 'network_configuration' + value: list = [ + { + "endpoint_configuration": [ + ] + } + ] + _handle_complex_after_unknown(key, resource, value) + assert resource == {'network_configuration': [{"endpoint_configuration": []}]} + def test_large_file(mocker: MockerFixture): # given From a8e18f7b2b123c3e750255c3af0f79a39802cec7 Mon Sep 17 00:00:00 2001 From: Barak Fatal Date: Wed, 8 Oct 2025 11:52:11 +0300 Subject: [PATCH 2/3] updated function to correctly handle both list and dict --- checkov/terraform/plan_parser.py | 7 +++--- tests/terraform/parser/test_plan_parser.py | 28 ++++++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/checkov/terraform/plan_parser.py b/checkov/terraform/plan_parser.py index 224ff086f1..7263ab8231 100644 --- a/checkov/terraform/plan_parser.py +++ b/checkov/terraform/plan_parser.py @@ -263,18 +263,17 @@ def _handle_complex_after_unknown(k: str, resource_conf: dict[str, Any], v: Any) continue if inner_key not in resource_conf[k]: if isinstance(resource_conf[k][0], dict): - if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf): + if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf[k][0]): resource_conf[k][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) elif isinstance(resource_conf[k][0], list) and isinstance(resource_conf[k][0][0], dict): - if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf): + if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf[k][0][0]): resource_conf[k][0][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) -def _validate_after_unknown_list_not_empty(inner_key: str, k: str, resource_conf: dict[str, Any]) -> bool: +def _validate_after_unknown_list_not_empty(inner_key: str, k: str, value: dict[str, Any]) -> bool: """ If the inner key is a list - we want to check it's not empty, if not we handle it in the original way. """ - value = resource_conf[k][0] return ((inner_key not in value) or (inner_key in value and not isinstance(value[inner_key], list)) or (isinstance(value[inner_key], list) and value[inner_key] != [] and value[inner_key][0] != [])) diff --git a/tests/terraform/parser/test_plan_parser.py b/tests/terraform/parser/test_plan_parser.py index d591a211bf..396b729bfb 100644 --- a/tests/terraform/parser/test_plan_parser.py +++ b/tests/terraform/parser/test_plan_parser.py @@ -1,12 +1,16 @@ +import copy import os import unittest from pathlib import Path +from typing import Any from unittest import mock +import pytest from pytest_mock import MockerFixture from checkov.common.util.consts import TRUE_AFTER_UNKNOWN -from checkov.terraform.plan_parser import parse_tf_plan, _sanitize_count_from_name, _handle_complex_after_unknown +from checkov.terraform.plan_parser import parse_tf_plan, _sanitize_count_from_name, _handle_complex_after_unknown, \ + _validate_after_unknown_list_not_empty from checkov.common.parsers.node import StrNode @@ -154,15 +158,25 @@ def test_handle_complex_after_unknown_with_empty_list(self): } ]} key: str = 'network_configuration' - value: list = [ - { - "endpoint_configuration": [ - ] - } - ] + value = [{"endpoint_configuration": []}] _handle_complex_after_unknown(key, resource, value) assert resource == {'network_configuration': [{"endpoint_configuration": []}]} +@pytest.mark.parametrize("inner_key, k, is_inner_list", [ + ("endpoint_configuration", "network_configuration", False), + ("endpoint_configuration", "network_configuration", True) +]) +def test_handle_complex_after_unknown(inner_key: str, k: str, is_inner_list: bool) -> None: + if is_inner_list: + # We cannot parametrize a dict object, so we use a boolean to decide which conf to use + resource_conf = {'network_configuration': [[{"endpoint_configuration": []}]]} + else: + resource_conf = {'network_configuration': [{"endpoint_configuration": []}]} + value = [{"endpoint_configuration": []}] + resource_conf_copy = copy.deepcopy(resource_conf) + _handle_complex_after_unknown(k, resource_conf, value) + assert resource_conf == resource_conf_copy + def test_large_file(mocker: MockerFixture): # given From b61fb4c715a86e1a50822469cd272421b706f70e Mon Sep 17 00:00:00 2001 From: Barak Fatal Date: Wed, 8 Oct 2025 11:58:49 +0300 Subject: [PATCH 3/3] handled cases with more than 1 list item --- checkov/terraform/plan_parser.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/checkov/terraform/plan_parser.py b/checkov/terraform/plan_parser.py index 7263ab8231..e7f72ae42e 100644 --- a/checkov/terraform/plan_parser.py +++ b/checkov/terraform/plan_parser.py @@ -261,13 +261,15 @@ def _handle_complex_after_unknown(k: str, resource_conf: dict[str, Any], v: Any) if inner_key in (START_LINE, END_LINE): # skip inner checkov keys continue - if inner_key not in resource_conf[k]: - if isinstance(resource_conf[k][0], dict): - if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf[k][0]): - resource_conf[k][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) - elif isinstance(resource_conf[k][0], list) and isinstance(resource_conf[k][0][0], dict): - if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf[k][0][0]): - resource_conf[k][0][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) + resource_conf_value = resource_conf[k] + if inner_key not in resource_conf_value and isinstance(resource_conf_value, list): + for i in range(len(resource_conf_value)): + if isinstance(resource_conf_value[i], dict): + if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf_value[i]): + resource_conf_value[i][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) + elif isinstance(resource_conf_value[i], list) and isinstance(resource_conf_value[i][0], dict): + if _validate_after_unknown_list_not_empty(inner_key, k, resource_conf_value[i][0]): + resource_conf_value[i][0][inner_key] = _clean_simple_type_list([TRUE_AFTER_UNKNOWN]) def _validate_after_unknown_list_not_empty(inner_key: str, k: str, value: dict[str, Any]) -> bool: