Repository: bridgecrewio/checkov

Issue:
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
```


Discussion:

Resolving patch:
diff --git a/checkov/common/checks_infra/checks_parser.py b/checkov/common/checks_infra/checks_parser.py
--- 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
--- 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
--- 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"]


Test patch:
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
--- /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
--- /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
--- /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
--- 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)