diff --git a/checkov/terraform/plan_parser.py b/checkov/terraform/plan_parser.py index c5015b8b45..e7f72ae42e 100644 --- a/checkov/terraform/plan_parser.py +++ b/checkov/terraform/plan_parser.py @@ -261,11 +261,24 @@ 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): - 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]) + 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: + """ + If the inner key is a list - we want to check it's not empty, if not we handle it in the original way. + """ + 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..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 @@ -146,6 +150,33 @@ 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 = [{"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