Repository: bridgecrewio/checkov

Issue:
CKV_GIT_4 GitHub "Ensure Secrets are encrypted" is based on a misinterpretation
note: I am not a user of Checkov but I contributed to the GitHub terraform provider so [this comment](https://github.com/integrations/terraform-provider-github/issues/888#issuecomment-1033071672) by @ned1313 made me aware or Checkov behaviour which I believe to be incorrect.

**Describe the issue**

The GitHub [Ensure Secrets are encrypted](https://github.com/bridgecrewio/checkov/blob/0700739e71db58e2dc7987694c33757642b7d38b/checkov/terraform/checks/resource/github/SecretsEncrypted.py) check appears to be based on a misinterpretation of the GitHub functionality and the GitHub terraform provider documentation, because of ambiguity in the argument's naming scheme and the documentation.

Secrets can be provided to GitHub in either a plain text form or an encrypted form. GitHub will encrypt any plain text secrets that arrive, and leave encrypted secrets as is. Regardless of the choice of how to provide the input (plain text or encrypted) it will be stored encrypted by GitHub, then decrypted by GitHub Actions at runtime using the private key.

The choice to use `encrypted_value` over `plaintext_value` is made when you have a secret that you are providing as an input to terraform that you do not want to end up in your terraform state in plain text. If a secret has been generated by a different terraform provider (e.g: a cloud provider access token) then it will already exist in the terraform state, so passing it as a `plaintext_value` to GitHub doesn't introduce any additional exposure.

```python
class SecretsEncrypted(BaseResourceNegativeValueCheck):
    def __init__(self) -> None:
        #  -from github docs "It is also advised that you do not store plaintext values in your code but rather populate
        #  the encrypted_value using fields from a resource, data source or variable as,
        #  while encrypted in state, these will be easily accessible in your code"
```

The full quote here the [GitHub documentation](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_environment_secret) is...

> For the purposes of security, the contents of the `plaintext_value` field have been marked as sensitive to Terraform, but it is important to note that this does not hide it from state files. You should treat state as sensitive always. It is also advised that you do not store plaintext values in your code but rather populate the `encrypted_value` using fields from a resource, data source or variable as, while encrypted in state, these will be easily accessible in your code. 

Rather than serve to describe the behaviour of `encrypted_value` and `plaintext_value` the statement is actually just a generic warning about terraform best practices.

Here's an example of what would Checkov currently produces a warning for, despite it being a valid and secure use:

```hcl
resource "azuread_service_principal" "gh_actions" {
  application_id = azuread_application.gh_actions.application_id
  owners         = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal_password" "gh_actions" {
  service_principal_id = azuread_service_principal.gh_actions.object_id
}

resource "github_actions_secret" "example_secret" {
  repository       = "example_repository"
  secret_name      = "example_secret_name"
  plaintext_value  = azuread_service_principal_password.gh_actions.value
}
```

The check could either be removed completely (because `plaintext_value` is appropriate to use in most situations) or, if possible, modified to only warn when the value for the `plaintext_value` argument is a string value directly written into the terraform configuration.

I hope that's clear, please let me know if I can clarify anything.


Discussion:
@shrink thanks for the detailed explanation. I like both of your suggestions, either to remove the check or limit it to hardcoded values. @schosterbarak any thoughts?
@gruebel let's limit to hard coded values

Resolving patch:
diff --git a/checkov/terraform/checks/resource/base_resource_check.py b/checkov/terraform/checks/resource/base_resource_check.py
--- a/checkov/terraform/checks/resource/base_resource_check.py
+++ b/checkov/terraform/checks/resource/base_resource_check.py
@@ -9,6 +9,19 @@
 from checkov.terraform.parser_utils import find_var_blocks
 
 
+PROVIDER_PREFIXES = (
+    "aws_",
+    "azurerm_",
+    "azuread_",
+    "digitalocean_",
+    "google_",
+    "github_",
+    "linode_",
+    "oci_",
+    "openstack_",
+)
+
+
 class BaseResourceCheck(BaseCheck):
     def __init__(
         self,
@@ -34,7 +47,10 @@ def _is_variable_dependant(value: Any) -> bool:
         if not isinstance(value, str):
             return False
 
-        if value.startswith(('var.', 'local.', 'module.')):
+        if value.startswith(("var.", "local.", "module.")):
+            return True
+
+        if value.startswith(PROVIDER_PREFIXES):
             return True
 
         if "${" not in value:
diff --git a/checkov/terraform/checks/resource/base_resource_value_check.py b/checkov/terraform/checks/resource/base_resource_value_check.py
--- a/checkov/terraform/checks/resource/base_resource_value_check.py
+++ b/checkov/terraform/checks/resource/base_resource_value_check.py
@@ -10,8 +10,6 @@
 from checkov.common.util.type_forcers import force_list
 from checkov.terraform.graph_builder.utils import get_referenced_vertices_in_value
 from checkov.terraform.parser_functions import handle_dynamic_values
-from checkov.terraform.parser_utils import find_var_blocks
-
 
 
 class BaseResourceValueCheck(BaseResourceCheck):
diff --git a/checkov/terraform/checks/resource/github/SecretsEncrypted.py b/checkov/terraform/checks/resource/github/SecretsEncrypted.py
--- a/checkov/terraform/checks/resource/github/SecretsEncrypted.py
+++ b/checkov/terraform/checks/resource/github/SecretsEncrypted.py
@@ -1,6 +1,6 @@
-from typing import List, Any
+from typing import List, Any, Dict
 
-from checkov.common.models.enums import CheckCategories
+from checkov.common.models.enums import CheckCategories, CheckResult
 from checkov.terraform.checks.resource.base_resource_negative_value_check import BaseResourceNegativeValueCheck
 from checkov.common.models.consts import ANY_VALUE
 
@@ -12,12 +12,21 @@ def __init__(self) -> None:
         #  while encrypted in state, these will be easily accessible in your code"
         name = "Ensure Secrets are encrypted"
         id = "CKV_GIT_4"
-        supported_resources = ["github_actions_environment_secret",
-                               "github_actions_organization_secret",
-                               "github_actions_secret"]
-        categories = [CheckCategories.ENCRYPTION]
+        supported_resources = (
+            "github_actions_environment_secret",
+            "github_actions_organization_secret",
+            "github_actions_secret",
+        )
+        categories = (CheckCategories.ENCRYPTION,)
         super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)
 
+    def scan_resource_conf(self, conf: Dict[str, List[Any]]) -> CheckResult:
+        plaintext = conf.get("plaintext_value")
+        if plaintext and self._is_variable_dependant(plaintext[0]):
+            return CheckResult.PASSED
+
+        return super().scan_resource_conf(conf)
+
     def get_inspected_key(self) -> str:
         return "plaintext_value"
 


Test patch:
diff --git a/tests/terraform/checks/resource/github/example_SecretsEncrypted/main.tf b/tests/terraform/checks/resource/github/example_SecretsEncrypted/main.tf
--- a/tests/terraform/checks/resource/github/example_SecretsEncrypted/main.tf
+++ b/tests/terraform/checks/resource/github/example_SecretsEncrypted/main.tf
@@ -38,4 +38,16 @@ resource "github_actions_secret" "pass" {
   environment       = "example_environment"
   secret_name       = "example_secret_name"
   encrypted_value   = "WOULDBEENCRYPTED"
-}
\ No newline at end of file
+}
+
+# value ref
+
+resource "azuread_service_principal_password" "gh_actions" {
+  service_principal_id = azuread_service_principal.gh_actions.object_id
+}
+
+resource "github_actions_secret" "value_ref" {
+  repository       = "example_repository"
+  secret_name      = "example_secret_name"
+  plaintext_value  = azuread_service_principal_password.gh_actions.value
+}
diff --git a/tests/terraform/checks/resource/github/test_SecretsEncrypted.py b/tests/terraform/checks/resource/github/test_SecretsEncrypted.py
--- a/tests/terraform/checks/resource/github/test_SecretsEncrypted.py
+++ b/tests/terraform/checks/resource/github/test_SecretsEncrypted.py
@@ -1,5 +1,5 @@
-import os
 import unittest
+from pathlib import Path
 
 from checkov.runner_filter import RunnerFilter
 from checkov.terraform.checks.resource.github.SecretsEncrypted import check
@@ -8,17 +8,20 @@
 
 class TestSecretsEncrypted(unittest.TestCase):
     def test(self):
-        runner = Runner()
-        current_dir = os.path.dirname(os.path.realpath(__file__))
+        # given
+        test_files_dir = Path(__file__).parent / "example_SecretsEncrypted"
 
-        test_files_dir = current_dir + "/example_SecretsEncrypted"
-        report = runner.run(root_folder=test_files_dir, runner_filter=RunnerFilter(checks=[check.id]))
+        # when
+        report = Runner().run(root_folder=str(test_files_dir), runner_filter=RunnerFilter(checks=[check.id]))
+
+        # then
         summary = report.get_summary()
 
         passing_resources = {
             "github_actions_environment_secret.pass",
             "github_actions_organization_secret.pass",
             "github_actions_secret.pass",
+            "github_actions_secret.value_ref",
         }
         failing_resources = {
             "github_actions_environment_secret.fail",
@@ -26,10 +29,10 @@ def test(self):
             "github_actions_secret.fail",
         }
 
-        passed_check_resources = set([c.resource for c in report.passed_checks])
-        failed_check_resources = set([c.resource for c in report.failed_checks])
+        passed_check_resources = {c.resource for c in report.passed_checks}
+        failed_check_resources = {c.resource for c in report.failed_checks}
 
-        self.assertEqual(summary["passed"], 3)
+        self.assertEqual(summary["passed"], 4)
         self.assertEqual(summary["failed"], 3)
         self.assertEqual(summary["skipped"], 0)
         self.assertEqual(summary["parsing_errors"], 0)
diff --git a/tests/terraform/checks/test_base_resource_check.py b/tests/terraform/checks/test_base_resource_check.py
--- a/tests/terraform/checks/test_base_resource_check.py
+++ b/tests/terraform/checks/test_base_resource_check.py
@@ -1,28 +1,24 @@
-import os
 import unittest
 
-from checkov.common.models.enums import CheckCategories, CheckResult
 from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
-from checkov.terraform.checks.resource.registry import resource_registry as registry
-from checkov.terraform.runner import Runner
-from checkov.runner_filter import RunnerFilter
 
 
 class TestWildcardEntities(unittest.TestCase):
-
     def test_contains_unrendered_variable(self):
-        self.assertTrue(BaseResourceCheck._is_variable_dependant('var.xyz'))
-        self.assertTrue(BaseResourceCheck._is_variable_dependant('local.xyz'))
-        self.assertTrue(BaseResourceCheck._is_variable_dependant('module.xyz'))
-        self.assertTrue(BaseResourceCheck._is_variable_dependant('${var.xyz}'))
-        self.assertTrue(BaseResourceCheck._is_variable_dependant('${local.xyz}'))
-        self.assertTrue(BaseResourceCheck._is_variable_dependant('${module.xyz}'))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("var.xyz"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("local.xyz"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("module.xyz"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("${var.xyz}"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("${local.xyz}"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("${module.xyz}"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("aws_ssm_parameter.secret.value"))
+        self.assertTrue(BaseResourceCheck._is_variable_dependant("azuread_service_principal_password.gh_actions.value"))
 
-        self.assertFalse(BaseResourceCheck._is_variable_dependant('xyz'))
-        self.assertFalse(BaseResourceCheck._is_variable_dependant('123'))
+        self.assertFalse(BaseResourceCheck._is_variable_dependant("xyz"))
+        self.assertFalse(BaseResourceCheck._is_variable_dependant("123"))
         self.assertFalse(BaseResourceCheck._is_variable_dependant(123))
         self.assertFalse(BaseResourceCheck._is_variable_dependant(True))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()