Repository: bridgecrewio/checkov
Pull request: feat(terraform): support provider blocks yaml policy checks
**By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.**

## Description

*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.*

This PR support provider yaml policy checks and allows the customer to write custom checks involving various resource types in terraform

Fixes #4341

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] I have added tests that prove my feature, policy, or fix is effective and works
- [ ] New and existing tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules


Problem statement:
Ability to write custom checks on provider block in Yaml
While checkov allows us to write custom checks involving various resource types in terraform, am not sure if there is a way to add custom checks for the provider block in terraform. The default check that runs for the provider block does scan for secrets, however, am looking to check if the provider block has a default_tags definition. The reason I need to check this is that while we can write custom checks to check tags for all supported resource types, we would need to avoid having to repeat the tags for individual resources and use the default_tags feature in terraform to apply the same set of tags to all resources, then there should be a way to check this and If defined then not check the individual resource types for the tags.

```hcl
provider "aws" {
  profile = "default"
  region = var.aws_region
  default_tags {
    tags = {
      Environment = "Test"
      Name        = "Provider Tag"
    }
  }
}
```

```yaml
scope:
  provider: aws
definition:
  and:
   - cond_type: "attribute"
     resource_types:
       - "provider.aws"
     attribute: "default_tags"
     operator: exists
```

Hints from issue discussion:
hey @rannaladasu thanks for reaching out.

Sounds like a good feature request. We currently only support `resource`, `module` and `data` blocks.
You can write this check easily enough already in python for a provider what you cant do is write a yaml check for providers - see the draft https://github.com/bridgecrewio/checkov/pull/4498 for how. Im pretty sure (certain) were have visited the idea of a check for default tags before @gruebel and we rejected due to the high level of noise it would introduce?
yeah correct, we currently don't plan to add such a check, but there are plans to add `provider` as a resource type to our graph/YAML based checks 🙂

Commit message: add whitespaces

Patch to review:
diff --git a/checkov/common/checks_infra/checks_parser.py b/checkov/common/checks_infra/checks_parser.py
index 3792516fca..d1a63453d5 100644
--- a/checkov/common/checks_infra/checks_parser.py
+++ b/checkov/common/checks_infra/checks_parser.py
@@ -185,8 +185,9 @@ def validate_check_config(self, file_path: str, raw_check: dict[str, dict[str, A
         return True
 
     def parse_raw_check(self, raw_check: Dict[str, Dict[str, Any]], **kwargs: Any) -> BaseGraphCheck:
+        provider = raw_check.get("scope", {}).get("provider", "").lower()
         policy_definition = raw_check.get("definition", {})
-        check = self._parse_raw_check(policy_definition, kwargs.get("resources_types"))
+        check = self._parse_raw_check(policy_definition, kwargs.get("resources_types"), provider)
         check.id = raw_check.get("metadata", {}).get("id", "")
         check.name = raw_check.get("metadata", {}).get("name", "")
         check.category = raw_check.get("metadata", {}).get("category", "")
@@ -198,7 +199,7 @@ def parse_raw_check(self, raw_check: Dict[str, Dict[str, Any]], **kwargs: Any) -
 
         return check
 
-    def _parse_raw_check(self, raw_check: Dict[str, Any], resources_types: Optional[List[str]]) -> BaseGraphCheck:
+    def _parse_raw_check(self, raw_check: Dict[str, Any], resources_types: Optional[List[str]], provider: str = "") -> BaseGraphCheck:
         check = BaseGraphCheck()
         complex_operator = get_complex_operator(raw_check)
         if complex_operator:
@@ -212,7 +213,7 @@ def _parse_raw_check(self, raw_check: Dict[str, Any], resources_types: Optional[
                 sub_solvers = [sub_solvers]
 
             for sub_solver in sub_solvers:
-                check.sub_checks.append(self._parse_raw_check(sub_solver, resources_types))
+                check.sub_checks.append(self._parse_raw_check(sub_solver, resources_types, provider))
             resources_types_of_sub_solvers = [
                 force_list(q.resource_types) for q in check.sub_checks if q is not None and q.resource_types is not None
             ]
@@ -228,6 +229,8 @@ def _parse_raw_check(self, raw_check: Dict[str, Any], resources_types: Optional[
                     or (isinstance(resource_type, list) and resource_type[0].lower() == "all")
             ):
                 check.resource_types = resources_types or []
+            elif "provider" in resource_type and provider != "":
+                check.resource_types.append(f"provider.{provider}")
             else:
                 check.resource_types = resource_type
 
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 753a530ac7..84b1072b52 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
@@ -22,7 +22,7 @@
 if TYPE_CHECKING:
     from checkov.common.typing import LibraryGraph
 
-SUPPORTED_BLOCK_TYPES = {BlockType.RESOURCE, TerraformBlockType.DATA, TerraformBlockType.MODULE}
+SUPPORTED_BLOCK_TYPES = {BlockType.RESOURCE, TerraformBlockType.DATA, TerraformBlockType.MODULE, TerraformBlockType.PROVIDER}
 WILDCARD_PATTERN = re.compile(r"(\S+[.][*][.]*)+")
 
 
diff --git a/checkov/common/graph/graph_builder/graph_components/blocks.py b/checkov/common/graph/graph_builder/graph_components/blocks.py
index 15ad4ba532..29de0978bc 100644
--- a/checkov/common/graph/graph_builder/graph_components/blocks.py
+++ b/checkov/common/graph/graph_builder/graph_components/blocks.py
@@ -2,7 +2,7 @@
 
 from collections.abc import Collection
 from copy import deepcopy
-from typing import Union, Dict, Any, List
+from typing import Union, Dict, Any, List, cast
 
 from checkov.common.graph.graph_builder.graph_components.attribute_names import CustomAttributes
 from checkov.common.graph.graph_builder.utils import calculate_hash, join_trimmed_strings
@@ -110,6 +110,13 @@ def get_attribute_dict(self, add_hash: bool = True) -> Dict[str, Any]:
             # since module names are user defined we are just setting 'module' as resource type for easier searching
             base_attributes[CustomAttributes.RESOURCE_TYPE] = "module"
 
+        if self.block_type == BlockType.PROVIDER:
+            # provider_name is always a string, base_attributes needs better typingpipenv run mypy
+            provider_name = cast(str, base_attributes[CustomAttributes.BLOCK_NAME])
+            provider_type = provider_name.split(".")[0]
+            # ex: provider.aws
+            base_attributes[CustomAttributes.RESOURCE_TYPE] = f"provider.{provider_type}"
+
         if "changed_attributes" in base_attributes:
             # removed changed attributes if it was added previously for calculating hash.
             del base_attributes["changed_attributes"]
diff --git a/docs/3.Custom Policies/Examples.md b/docs/3.Custom Policies/Examples.md
index 413faa7d07..398946cefb 100644
--- a/docs/3.Custom Policies/Examples.md	
+++ b/docs/3.Custom Policies/Examples.md	
@@ -41,6 +41,23 @@ definition:
   value: terraform-aws-modules
 ```
 
+## Basic Query - Provider custom policy check
+
+```yaml
+---
+metadata:
+  name: "Ensure a certain region is not added"
+  id: "CKV2_AWS_3"
+  category: "GENERAL_SECURITY"
+definition:
+  cond_type: "attribute"
+  resource_types:
+    - "provider"
+  attribute: "region"
+  operator: "not_contains"
+  value: "us-west-1"
+```
+
 ## Basic Query - Terraform plan resource not deleted
 
 ```yaml
diff --git a/tests/terraform/runner/extra_yaml_checks/aws_provider_check.yaml b/tests/terraform/runner/extra_yaml_checks/aws_provider_check.yaml
new file mode 100644
index 0000000000..0ebc7a90ea
--- /dev/null
+++ b/tests/terraform/runner/extra_yaml_checks/aws_provider_check.yaml
@@ -0,0 +1,11 @@
+metadata:
+  id: "CUSTOM_GRAPH_AWS_4"
+  name: "Ensure a certain region is not added"
+  category: "GENERAL_SECURITY"
+definition:
+  cond_type: "attribute"
+  resource_types:
+    - "provider.aws"
+  attribute: "region"
+  operator: "not_contains"
+  value: "us-west-2"
\ No newline at end of file
diff --git a/tests/terraform/runner/extra_yaml_checks/policy_violations.yaml b/tests/terraform/runner/extra_yaml_checks/policy_violations.yaml
new file mode 100644
index 0000000000..82ad6402c0
--- /dev/null
+++ b/tests/terraform/runner/extra_yaml_checks/policy_violations.yaml
@@ -0,0 +1,20 @@
+metadata:
+  id: "CUSTOM_GRAPH_AWS_3"
+  name: "Ensure a certain region is not added"
+  category: "GENERAL_SECURITY"
+scope:
+  provider: "AWS"
+definition:
+  and:
+  - cond_type: "attribute"
+    resource_types:
+      - "provider"
+    attribute: "default_tags"
+    operator: exists
+  - cond_type: "attribute"
+    resource_types:
+      - "provider"
+    attribute: "region"
+    operator: "not_contains"
+    value: "us-west-1"
+
diff --git a/tests/terraform/runner/resources/provider_blocks/main.tf b/tests/terraform/runner/resources/provider_blocks/main.tf
new file mode 100644
index 0000000000..3988d6324b
--- /dev/null
+++ b/tests/terraform/runner/resources/provider_blocks/main.tf
@@ -0,0 +1,33 @@
+provider "aws" {
+  alias      = "provider_check_1"
+  region     = "us-west-1"
+}
+
+provider "aws" {
+  alias      = "provider_check_2"
+  region     = "ap-northeast-2"
+}
+
+provider "aws" {
+  alias      = "provider_check_3"
+  region     = "ap-northeast-2"
+  default_tags {
+    tags = {
+      Environment = "Test"
+      Name        = "Provider Tag"
+    }
+  }
+}
+
+provider "aws" {
+  alias      = "provider_check_4"
+  region     = "ap-northeast-2"
+  default_tags {
+  }
+}
+
+provider "aws" {
+  alias      = "provider_check_5"
+}
+
+
diff --git a/tests/terraform/runner/test_runner.py b/tests/terraform/runner/test_runner.py
index b8ef494db9..59a2890883 100644
--- a/tests/terraform/runner/test_runner.py
+++ b/tests/terraform/runner/test_runner.py
@@ -199,6 +199,26 @@ def test_runner_extra_yaml_check(self):
         # Remove external checks from registry.
         runner.graph_registry.checks[:] = [check for check in runner.graph_registry.checks if "CUSTOM" not in check.id]
 
+    def test_runner_provider_yaml_check(self):
+        current_dir = os.path.dirname(os.path.realpath(__file__))
+
+        tf_dir_path = current_dir + "/resources/provider_blocks"
+        extra_checks_dir_path = [current_dir + "/extra_yaml_checks"]
+
+        runner = Runner(db_connector=self.db_connector())
+        report = runner.run(root_folder=tf_dir_path, external_checks_dir=extra_checks_dir_path, runner_filter=RunnerFilter(checks=['CUSTOM_GRAPH_AWS_3', 'CUSTOM_GRAPH_AWS_4']))
+        report_json = report.get_json()
+
+        self.assertIsInstance(report_json, str)
+        self.assertIsNotNone(report_json)
+        self.assertIsNotNone(report.get_test_suite())
+
+        self.assertEqual(7, len(report.passed_checks))
+        self.assertEqual(3, len(report.failed_checks))
+
+        # Remove external checks from registry.
+        runner.graph_registry.checks[:] = [check for check in runner.graph_registry.checks if
+                                           "CUSTOM" not in check.id]
     def test_runner_yaml_module_check(self):
         # given
         current_dir = Path(__file__).parent
@@ -1171,7 +1191,7 @@ def test_loading_external_checks_yaml(self):
         current_dir = os.path.dirname(os.path.realpath(__file__))
         extra_checks_dir_path = current_dir + "/extra_yaml_checks"
         runner.load_external_checks([extra_checks_dir_path])
-        self.assertEqual(len(runner.graph_registry.checks), base_len + 3)
+        self.assertEqual(len(runner.graph_registry.checks), base_len + 5)
         runner.graph_registry.checks = runner.graph_registry.checks[:base_len]
 
     def test_loading_external_checks_yaml_multiple_times(self):
@@ -1180,9 +1200,9 @@ def test_loading_external_checks_yaml_multiple_times(self):
         runner.graph_registry.checks = []
         extra_checks_dir_path = [current_dir + "/extra_yaml_checks"]
         runner.load_external_checks(extra_checks_dir_path)
-        self.assertEqual(len(runner.graph_registry.checks), 3)
+        self.assertEqual(len(runner.graph_registry.checks), 5)
         runner.load_external_checks(extra_checks_dir_path)
-        self.assertEqual(len(runner.graph_registry.checks), 3)
+        self.assertEqual(len(runner.graph_registry.checks), 5)
 
         graph_checks = [x.id for x in runner.graph_registry.checks]
         self.assertIn('CUSTOM_GRAPH_AWS_1', graph_checks)


Code review comments:
diff_hunk: @@ -228,6 +229,8 @@ def _parse_raw_check(self, raw_check: Dict[str, Any], resources_types: Optional[
                     or (isinstance(resource_type, list) and resource_type[0].lower() == "all")
             ):
                 check.resource_types = resources_types or []
+            elif "provider" in resource_type and provider != "":
path: checkov/common/checks_infra/checks_parser.py
text: @user1:
```suggestion
            elif "provider" in resource_type and provider:
```