Test cases and top-level loop support
MODIFY
checkov/terraform/parser.py
checkov/terraform/parser.py
@@ -310,9 +310,28 @@ class Parser:
if isinstance(value, str):
altered_value = value
- had_pattern_match = False
-
+ had_var_block_match = False
+
+ # The way in which matches are processed is important as they are ordered and may contain
+ # portions of one another. For example:
+ # ${merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})}
+ # In this case, we expect blocks similar to this:
+ # 1) ${var.ENVIRONMENT}
+ # 2) ${var.REGION}
+ # 3) ${merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})}
+ # If either of the first two are replaced, we can still process the outer eval block
+ # if the substitutions made to the earlier vars are also made to the later. That allows
+ # knowing what the string should really look like so substitutions can be made properly.
+ # If this proves not to work well, the other option is to abort the later (because
+ # the full string isn't found in the value anymore) and come back to it on another
+ # processor loop. This works... but requires another processor loop.
+ # (If you're thinking we should make a DAG and do this properly... you're probably right.)
+ prev_matches: List[Tuple[str, str]] = [] # original value -> replaced
for match in _find_var_blocks(value):
+ # Update what's expected in the match, see comment above
+ for prev_match in prev_matches:
+ match.replace(prev_match[0], prev_match[1])
+
var_base = match.var_only
# Expressions such as (from variable definition):
@@ -321,7 +340,8 @@ class Parser:
# "type = ${string}"
if var_base in _SIMPLE_TYPES and match.full_str == value:
altered_value = var_base
- had_pattern_match = True
+ had_var_block_match = True
+ prev_matches.append((match.full_str, var_base))
else:
replaced = _handle_single_var_pattern(var_base, var_value_and_file_map,
locals_values, resource_list,
@@ -333,9 +353,10 @@ class Parser:
altered_value = replaced
else:
altered_value = altered_value.replace(match.full_str, str(replaced))
- had_pattern_match = True
+ prev_matches.append((match.full_str, replaced))
+ had_var_block_match = True
- if not had_pattern_match:
+ if not had_var_block_match:
# tomap is annoying because the curly braces in the string break the regex. Rather than
# coming up with something that's significantly more complex, we'll special case this
# check. Only do this when there wasn't a pattern match to make sure there are no
@@ -812,10 +833,15 @@ class VarBlockMatch:
full_str: str # Example: ${local.foo}
var_only: str # Example: local.fop
+ def replace(self, original: str, replaced: str):
+ self.full_str = self.full_str.replace(original, replaced)
+ self.var_only = self.var_only.replace(original, replaced)
+
def _find_var_blocks(value: str) -> List[VarBlockMatch]:
"""
- Find and return all the var blocks within a given string.
+ Find and return all the var blocks within a given string. Order is important and may contain portions of
+ one another.
"""
# Note: This used to be implemented with a regex: r'\${([^{}]+?)}')

ADD
tests/terraform/parser/resources/parser_scenarios/merge_function_unresolved_var/expected.json
+{
+ "main.tf": {
+ "locals": [
+ {
+ "common_tags": [
+ {
+ "Tag1": "one",
+ "Tag2": "two"
+ }
+ ]
+ }
+ ],
+ "variable": [
+ {
+ "ENV": {}
+ }
+ ],
+ "resource": [
+ {
+ "aws_s3_bucket": {
+ "bucket": {
+ "tags": [
+ {
+ "Tag1": "one",
+ "Tag2": "two",
+ "Name": "my-bucket-${var.ENV}"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/merge_function_unresolved_var/main.tf
+locals {
+ common_tags = {
+ Tag1 = "one"
+ Tag2 = "two"
+ }
+}
+
+variable "ENV" {}
+
+resource "aws_s3_bucket" "bucket" {
+ # var.ENV has no default, so need to evaluate the merge without the
+ # fully resolved statement.
+ tags = merge(local.common_tags, {Name = "my-bucket-${var.ENV}"})
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/variable_defaults_separate_files/eval.json
+{
+ "main.tf": {
+ "BUCKET_NAME": {
+ "var_file": "variables.tf",
+ "value": "this-is-my-default",
+ "definitions": [
+ {
+ "definition_name": "BUCKET_NAME",
+ "definition_expression": "${var.BUCKET_NAME}",
+ "definition_path": "resource/0/aws_s3_bucket/test/bucket/0"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/variable_defaults_separate_files/expected.json
+{
+ "main.tf": {
+ "resource": [
+ {
+ "aws_s3_bucket": {
+ "test": {
+ "bucket": [
+ "this-is-my-default"
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "variables.tf": {
+ "variable": [
+ {
+ "BUCKET_NAME": {
+ "type": ["string"],
+ "default": ["this-is-my-default"]
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file

ADD
tests/terraform/parser/resources/parser_scenarios/variable_defaults_separate_files/main.tf
+resource "aws_s3_bucket" "test" {
+ bucket = var.BUCKET_NAME
+}

ADD
tests/terraform/parser/resources/parser_scenarios/variable_defaults_separate_files/variables.tf
+variable "BUCKET_NAME" {
+ type = string
+ default = "this-is-my-default"
+}
\ No newline at end of file

RENAME
tests/terraform/evaluation/test_hcl2_load_assumptions.py
tests/terraform/parser/test_hcl2_load_assumptions.py
@@ -55,6 +55,24 @@ class TestHCL2LoadAssumptions(unittest.TestCase):
}
self.go(tf, expect)
+ def test_merge_with_inner_var(self):
+ tf = '''
+ resource "aws_s3_bucket" "foo" {
+ tags = merge(local.common_tags, local.common_data_tags, {Name = "my-thing-${var.ENVIRONMENT}-${var.REGION}"})
+ }'''
+ expect = {
+ "resource": [
+ {
+ "aws_s3_bucket": {
+ "foo": {
+ "tags": ["${merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})}"]
+ }
+ }
+ }
+ ]
+ }
+ self.go(tf, expect)
+
def test_variable_block(self):
tf = '''
variable "my_var" {

RENAME
tests/terraform/test_parser_internals.py
tests/terraform/parser/test_parser_internals.py
@@ -70,7 +70,16 @@ class TestParserInternals(unittest.TestCase):
"merge({\"a\": \"}, evil\"},{\"b\": \"\\\" , evil\"})")]),
("$${foo}", []), # escape interpolation
('${merge({\'a\': \'}, evil\'})}',
- [VBM('${merge({\'a\': \'}, evil\'})}', 'merge({\'a\': \'}, evil\'})')])
+ [VBM('${merge({\'a\': \'}, evil\'})}', 'merge({\'a\': \'}, evil\'})')]),
+
+ # Ordered returning of sub-vars and then outer var.
+ ("${merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})}",
+ [
+ VBM("${var.ENVIRONMENT}", "var.ENVIRONMENT"),
+ VBM("${var.REGION}", "var.REGION"),
+ VBM("${merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})}",
+ "merge(local.common_tags,local.common_data_tags,{'Name': 'my-thing-${var.ENVIRONMENT}-${var.REGION}'})")
+ ])
]
for case in cases:
actual = parser._find_var_blocks(case[0])

MODIFY
tests/terraform/parser/test_parser_scenarios.py
tests/terraform/parser/test_parser_scenarios.py
@@ -28,6 +28,9 @@ class TestParserScenarios(unittest.TestCase):
def test_variable_defaults(self):
self.go("variable_defaults")
+ def test_variable_defaults_separate_files(self):
+ self.go("variable_defaults_separate_files")
+
def test_local_block(self):
self.go("local_block")
@@ -40,6 +43,9 @@ class TestParserScenarios(unittest.TestCase):
def test_merge_function(self):
self.go("merge_function")
+ def test_merge_function_unresolved_var(self):
+ self.go("merge_function_unresolved_var")
+
def test_tobool_function(self):
self.go("tobool_function")

Test cases and top-level loop support