Implementation of `map` function
MODIFY
checkov/terraform/parser_functions.py
checkov/terraform/parser_functions.py
import logging
+from typing import Any, Dict, Optional
from checkov.common.util.type_forcers import convert_str_to_bool
from checkov.terraform.parser_utils import eval_string, split_merge_args, string_to_native, to_string
@@ -20,6 +21,7 @@ FUNCTION_FAILED = "____FUNCTION_FAILED____"
def merge(original, var_resolver, **_):
+ # https://www.terraform.io/docs/language/functions/merge.html
args = split_merge_args(original)
if args is None:
return FUNCTION_FAILED
@@ -39,6 +41,7 @@ def merge(original, var_resolver, **_):
def concat(original, var_resolver, **_):
+ # https://www.terraform.io/docs/language/functions/concat.html
args = split_merge_args(original)
if args is None:
return FUNCTION_FAILED
@@ -112,21 +115,41 @@ def toset(original, **_):
def tomap(original, **_):
+ # https://www.terraform.io/docs/language/functions/tomap.html
original = original.replace(":", "=") # converted to colons by parser #shrug
altered_value = eval_string(original)
if altered_value is None or not isinstance(altered_value, dict):
return FUNCTION_FAILED
+ return _check_map_type_consistency(altered_value)
+
+def map(original, **_):
+ # https://www.terraform.io/docs/language/functions/map.html
+
+ # NOTE: Splitting by commas is annoying due to possible commas in strings. To avoid
+ # the issue, act like it's a list (to allow comma separation) and let the HCL
+ # parser deal with it. Then iterating the list is easy.
+ converted_to_list = eval_string(f"[{original}]")
+ if converted_to_list is None or len(converted_to_list) & 1: # none or odd number of args
+ return FUNCTION_FAILED
+
+ new_map = {}
+ for i in range(0, len(converted_to_list), 2):
+ new_map[converted_to_list[i]] = converted_to_list[i + 1]
+ return _check_map_type_consistency(new_map)
+
+
+def _check_map_type_consistency(value: Dict) -> Dict:
# If there is a string and anything else, convert to string
had_string = False
had_something_else = False
- for k, v in altered_value.items():
+ for k, v in value.items():
if v == "${True}":
- altered_value[k] = True
+ value[k] = True
v = True
elif v == "${False}":
- altered_value[k] = False
+ value[k] = False
v = False
if isinstance(v, str):
@@ -138,5 +161,5 @@ def tomap(original, **_):
if had_string:
break
if had_string and had_something_else:
- return {k: to_string(v) for k, v in altered_value.items()}
- return altered_value
+ value = {k: to_string(v) for k, v in value.items()}
+ return value

MODIFY
checkov/terraform/parser_utils.py
checkov/terraform/parser_utils.py
@@ -242,6 +242,7 @@ def string_to_native(value: str) -> Optional[Dict]:
except Exception:
return None
+
def to_string(value: Any) -> str:
if value is True:
return "true"

MODIFY
tests/terraform/parser/resources/parser_scenarios/concat_function/expected.json
tests/terraform/parser/resources/parser_scenarios/concat_function/expected.json
"b",
"c"
]
+ ],
+ "single_item_list": [
+ ["a"]
+ ],
+ "single_item_trailing_list": [
+ ["a"]
]
}
],
]
}
}
+ },
+ {
+ "aws_autoscaling_group": {
+ "bar_simplified": {
+ "name": [
+ "bar_simplified_group"
+ ],
+ "max_size": [
+ 5
+ ],
+ "min_size": [
+ 2
+ ],
+ "tags": [
+ [
+ {
+ "key": "interpolation1",
+ "value": "value3",
+ "propagate_at_launch": true
+ },
+ {
+ "key": "interpolation2",
+ "value": "value4",
+ "propagate_at_launch": true
+ }
+ ]
+ ]
+ }
+ }
}
]
}

MODIFY
tests/terraform/parser/resources/parser_scenarios/concat_function/main.tf
tests/terraform/parser/resources/parser_scenarios/concat_function/main.tf
@@ -45,8 +45,30 @@ resource "aws_autoscaling_group" "bar" {
)
}
+resource "aws_autoscaling_group" "bar_simplified" {
+ name = "bar_simplified_group"
+ max_size = 5
+ min_size = 2
+ tags = concat(
+ [
+ {
+ "key" = "interpolation1"
+ "value" = "value3"
+ "propagate_at_launch" = true
+ },
+ {
+ "key" = "interpolation2"
+ "value" = "value4"
+ "propagate_at_launch" = true
+ }
+ ]
+ )
+}
+
# From https://www.terraform.io/docs/language/functions/concat.html
locals {
simple_list = concat(["a", ""], ["b", "c"])
simple_list2 = concat(["a"], [""], ["b"], ["c"])
+ single_item_list = concat(["a"])
+ single_item_trailing_list = concat(["a"],)
}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/map_function/expected.json
+{
+ "main.tf": {
+ "locals": [
+ {
+ "INTS": [
+ {
+ "a": 1,
+ "b": 2
+ }
+ ],
+ "FLOATS": [
+ {
+ "a": 1.1,
+ "b": 2.2
+ }
+ ],
+ "STRINGS": [
+ {
+ "a": "one",
+ "b": "two"
+ }
+ ],
+ "BOOLS": [
+ {
+ "a": true,
+ "b": false
+ }
+ ],
+ "MIXED_BOOL": [
+ {
+ "a": "foo",
+ "b": "true"
+ }
+ ],
+ "MIXED_FLOAT": [
+ {
+ "a": "foo",
+ "b": "1.2"
+ }
+ ],
+ "ANNOYING_SPLIT": [
+ {
+ "this, is": "really, annoying"
+ }
+ ],
+ "INVALID_ODD_ARGS": ["${map(\"only one\")}"],
+ "common_tags": [
+ {
+ "App": "my_app",
+ "Product": "my_product",
+ "Team": "my_team"
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/map_function/main.tf
+locals {
+ INTS = map("a", 1, "b", 2)
+ FLOATS = map("a", 1.1, "b", 2.2)
+ STRINGS = map("a", "one", "b", "two")
+ BOOLS = map("a", true, "b", false)
+
+ MIXED_BOOL = map("a", "foo", "b", true)
+ MIXED_FLOAT = map("a", "foo", "b", 1.2)
+
+ ANNOYING_SPLIT = map("this, is", "really, annoying")
+
+ INVALID_ODD_ARGS = map("only one")
+
+ common_tags = "${map(
+ "App", "my_app",
+ "Product", "my_product",
+ "Team", "my_team",
+ )}"
+}
\ No newline at end of file

MODIFY
tests/terraform/parser/test_parser_scenarios.py
tests/terraform/parser/test_parser_scenarios.py
@@ -58,6 +58,9 @@ class TestParserScenarios(unittest.TestCase):
def test_tomap_function(self):
self.go("tomap_function")
+ def test_map_function(self):
+ self.go("map_function")
+
def test_tonumber_function(self):
self.go("tonumber_function")

Implementation of `map` function (#937)