diff --git a/checkov/common/checks_infra/solvers/attribute_solvers/base_attribute_solver.py b/checkov/common/checks_infra/solvers/attribute_solvers/base_attribute_solver.py index 02f26ac9869..5c5df1b16b0 100644 --- a/checkov/common/checks_infra/solvers/attribute_solvers/base_attribute_solver.py +++ b/checkov/common/checks_infra/solvers/attribute_solvers/base_attribute_solver.py @@ -53,13 +53,24 @@ def run(self, graph_connector: DiGraph) -> Tuple[List[Dict[str, Any]], List[Dict return passed_vertices, failed_vertices, unknown_vertices def get_operation(self, vertex: Dict[str, Any]) -> Optional[bool]: - attr_val = vertex.get(self.attribute) # type:ignore[arg-type] # due to attribute can be None # if this value contains an underendered variable, then we cannot evaluate value checks, # and will return None (for UNKNOWN) # handle edge cases in some policies that explicitly look for blank values - if self.is_value_attribute_check and self._is_variable_dependant(attr_val, vertex['source_']) \ - and self.value != '': - return None + # we also need to check the attribute stack - e.g., if they are looking for tags.component, but tags = local.tags, + # then we actually need to see if tags is variable dependent as well + attr_parts = self.attribute.split('.') # type:ignore[union-attr] # due to attribute can be None (but not really) + attr_to_check = None + for attr in attr_parts: + attr_to_check = f'{attr_to_check}.{attr}' if attr_to_check else attr + value_to_check = vertex.get(attr_to_check) + + # we can only check is_attribute_value_check when evaluating the full attribute + # for example, if we have a policy that says "tags.component exists", and tags = local.tags, then + # we need to check if tags is variable dependent even though this is a not value_attribute check + if (attr_to_check != self.attribute or self.is_value_attribute_check) \ + and self._is_variable_dependant(value_to_check, vertex['source_']) \ + and self.value != '': + return None if self.attribute and (self.is_jsonpath_check or re.match(WILDCARD_PATTERN, self.attribute)): attribute_matches = self.get_attribute_matches(vertex) diff --git a/tests/terraform/runner/resources/unrendered_vars/bucket_equals.yaml b/tests/terraform/runner/resources/unrendered_vars/bucket_equals.yaml new file mode 100644 index 00000000000..7ab0ce7d334 --- /dev/null +++ b/tests/terraform/runner/resources/unrendered_vars/bucket_equals.yaml @@ -0,0 +1,11 @@ +metadata: + id: "BUCKET_EQUALS" + name: "Ensure S3 bucket name is xyz" + category: "general" +definition: + cond_type: "attribute" + resource_types: + - "aws_s3_bucket" + attribute: "bucket" + operator: "equals" + value: "xyz" diff --git a/tests/terraform/runner/resources/unrendered_vars/bucket_exists.yaml b/tests/terraform/runner/resources/unrendered_vars/bucket_exists.yaml new file mode 100644 index 00000000000..4fbc428d186 --- /dev/null +++ b/tests/terraform/runner/resources/unrendered_vars/bucket_exists.yaml @@ -0,0 +1,10 @@ +metadata: + id: "BUCKET_EXISTS" + name: "Ensure S3 bucket name is present" + category: "general" +definition: + cond_type: "attribute" + resource_types: + - "aws_s3_bucket" + attribute: "bucket" + operator: "exists" diff --git a/tests/terraform/runner/resources/unrendered_vars/component_equals.yaml b/tests/terraform/runner/resources/unrendered_vars/component_equals.yaml new file mode 100644 index 00000000000..68d7357a4d3 --- /dev/null +++ b/tests/terraform/runner/resources/unrendered_vars/component_equals.yaml @@ -0,0 +1,11 @@ +metadata: + id: "COMPONENT_EQUALS" + name: "Ensure S3 bucket has a component tag" + category: "general" +definition: + cond_type: "attribute" + resource_types: + - "aws_s3_bucket" + attribute: "tags.component" + operator: "equals" + value: "xyz" diff --git a/tests/terraform/runner/resources/unrendered_vars/component_exists.yaml b/tests/terraform/runner/resources/unrendered_vars/component_exists.yaml new file mode 100644 index 00000000000..b49128498b5 --- /dev/null +++ b/tests/terraform/runner/resources/unrendered_vars/component_exists.yaml @@ -0,0 +1,10 @@ +metadata: + id: "COMPONENT_EXISTS" + name: "Ensure S3 bucket has a component tag" + category: "general" +definition: + cond_type: "attribute" + resource_types: + - "aws_s3_bucket" + attribute: "tags.component" + operator: "exists" diff --git a/tests/terraform/runner/resources/unrendered_vars/nested.tf b/tests/terraform/runner/resources/unrendered_vars/nested.tf new file mode 100644 index 00000000000..ef10a797c29 --- /dev/null +++ b/tests/terraform/runner/resources/unrendered_vars/nested.tf @@ -0,0 +1,40 @@ + +variable "tags_without_component" { + default = { + something = "something" + } +} + +variable "tags_with_component" { + default = { + component = "xyz" + } +} + +variable "component" { + default = "xyz" +} + +resource "aws_s3_bucket" "unknown_nested_unknown" { + tags = var.unknown_tags +} + +resource "aws_s3_bucket" "unknown_nested_2_pass" { + tags = { + component = var.unknown_component + } +} + +resource "aws_s3_bucket" "known_nested_pass" { + tags = var.tags_with_component +} + +resource "aws_s3_bucket" "known_nested_2_pass" { + tags = { + component = var.component + } +} + +resource "aws_s3_bucket" "known_nested_fail" { + tags = var.tags_without_component +} diff --git a/tests/terraform/runner/resources/unrendered_vars/simple.tf b/tests/terraform/runner/resources/unrendered_vars/simple.tf new file mode 100644 index 00000000000..f9f73908e11 --- /dev/null +++ b/tests/terraform/runner/resources/unrendered_vars/simple.tf @@ -0,0 +1,11 @@ +variable "bucket" { + default = "xyz" +} + +resource "aws_s3_bucket" "unknown_simple" { + bucket = var.unknown_bucket +} + +resource "aws_s3_bucket" "known_simple_pass" { + bucket = var.bucket +} diff --git a/tests/terraform/runner/test_runner.py b/tests/terraform/runner/test_runner.py index 1b936714e8e..fb5447374f7 100644 --- a/tests/terraform/runner/test_runner.py +++ b/tests/terraform/runner/test_runner.py @@ -1273,6 +1273,50 @@ def test_resource_negative_values_do_exist(self): self.assertEqual(len(report.passed_checks), 3) self.assertEqual(len(report.failed_checks), 3) + def test_unrendered_simple_var(self): + resources_dir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "resources", "unrendered_vars") + file_to_scan = os.path.join(resources_dir, "simple.tf") + checks = ['BUCKET_EQUALS', 'BUCKET_EXISTS'] + + runner = Runner() + runner_filter = RunnerFilter(framework=['terraform'], checks=checks) + report = runner.run(root_folder=None, files=[file_to_scan], external_checks_dir=[resources_dir], runner_filter=runner_filter) + + # plus 1 unknown + self.assertEqual(len(report.passed_checks), 3) + self.assertEqual(len(report.failed_checks), 0) + + self.assertTrue(any(r.check_id == 'BUCKET_EXISTS' and r.resource == 'aws_s3_bucket.known_simple_pass' for r in report.passed_checks)) + self.assertTrue(any(r.check_id == 'BUCKET_EQUALS' and r.resource == 'aws_s3_bucket.known_simple_pass' for r in report.passed_checks)) + + self.assertTrue(any(r.check_id == 'BUCKET_EXISTS' and r.resource == 'aws_s3_bucket.unknown_simple' for r in report.passed_checks)) + + def test_unrendered_nested_var(self): + resources_dir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "resources", "unrendered_vars") + file_to_scan = os.path.join(resources_dir, "nested.tf") + checks = ['COMPONENT_EQUALS', 'COMPONENT_EXISTS'] + + runner = Runner() + runner_filter = RunnerFilter(framework=['terraform'], checks=checks) + report = runner.run(root_folder=None, files=[file_to_scan], external_checks_dir=[resources_dir], runner_filter=runner_filter) + + # plus 3 unknown + self.assertEqual(len(report.passed_checks), 5) + self.assertEqual(len(report.failed_checks), 2) + + self.assertTrue(any(r.check_id == 'COMPONENT_EXISTS' and r.resource == 'aws_s3_bucket.unknown_nested_2_pass' for r in report.passed_checks)) + + self.assertTrue(any(r.check_id == 'COMPONENT_EXISTS' and r.resource == 'aws_s3_bucket.known_nested_pass' for r in report.passed_checks)) + self.assertTrue(any(r.check_id == 'COMPONENT_EQUALS' and r.resource == 'aws_s3_bucket.known_nested_pass' for r in report.passed_checks)) + + self.assertTrue(any(r.check_id == 'COMPONENT_EXISTS' and r.resource == 'aws_s3_bucket.known_nested_2_pass' for r in report.passed_checks)) + self.assertTrue(any(r.check_id == 'COMPONENT_EQUALS' and r.resource == 'aws_s3_bucket.known_nested_2_pass' for r in report.passed_checks)) + + self.assertTrue(any(r.check_id == 'COMPONENT_EXISTS' and r.resource == 'aws_s3_bucket.known_nested_fail' for r in report.failed_checks)) + self.assertTrue(any(r.check_id == 'COMPONENT_EQUALS' and r.resource == 'aws_s3_bucket.known_nested_fail' for r in report.failed_checks)) + def test_no_duplicate_results(self): resources_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), "resources", "duplicate_violations")