diff --git a/checkov/common/typing.py b/checkov/common/typing.py index 9a0da970377..ae52f693a05 100644 --- a/checkov/common/typing.py +++ b/checkov/common/typing.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Dict from typing_extensions import TypeAlias, TypedDict if TYPE_CHECKING: @@ -13,6 +13,10 @@ [str, "BaseCheck", "_SkippedCheck", "dict[str, Any]", str, str, "dict[str, Any]"], None ] +_Resource: TypeAlias = str +_Attribute: TypeAlias = str +ResourceAttributesToOmit: TypeAlias = Dict[_Resource, _Attribute] + class _CheckResult(TypedDict, total=False): result: "CheckResult" | tuple["CheckResult", dict[str, Any]] diff --git a/checkov/common/util/secrets.py b/checkov/common/util/secrets.py index b655c27e80d..8d75f4f5afd 100644 --- a/checkov/common/util/secrets.py +++ b/checkov/common/util/secrets.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from checkov.common.checks.base_check import BaseCheck - from checkov.common.typing import _CheckResult + from checkov.common.typing import _CheckResult, ResourceAttributesToOmit from pycep.typing import ParameterAttributes, ResourceAttributes @@ -109,7 +109,7 @@ def string_has_secrets(s: str, *categories: str) -> bool: return False -def omit_multiple_secret_values_from_line(secrets: list[str], line_text: str) -> str: +def omit_multiple_secret_values_from_line(secrets: set[str], line_text: str) -> str: censored_line = line_text for secret in secrets: censored_line = omit_secret_value_from_line(secret, censored_line) @@ -133,20 +133,28 @@ def omit_secret_value_from_line(secret: str, line_text: str) -> str: def omit_secret_value_from_checks(check: BaseCheck, check_result: dict[str, CheckResult] | _CheckResult, entity_code_lines: list[tuple[int, str]], - entity_config: dict[str, Any] | ParameterAttributes | ResourceAttributes) -> \ + entity_config: dict[str, Any] | ParameterAttributes | ResourceAttributes, + resource_attributes_to_omit: ResourceAttributesToOmit | None = None) -> \ list[tuple[int, str]]: + secrets = set() # a set, to efficiently avoid duplicates in case the same secret is found in the following conditions + censored_code_lines = [] + if CheckCategories.SECRETS in check.categories and check_result.get('result') == CheckResult.FAILED: - censored_code_lines = [] - secrets = [str(secret) for key, secret in entity_config.items() if key.startswith(f'{check.id}_secret')] - if not secrets: - logging.debug(f"Secret was not saved in {check.id}, can't omit") - return entity_code_lines - - for idx, line in entity_code_lines: - censored_line = omit_multiple_secret_values_from_line(secrets, line) - censored_code_lines.append((idx, censored_line)) - else: - censored_code_lines = entity_code_lines + secrets.update([str(secret) for key, secret in entity_config.items() if key.startswith(f'{check.id}_secret')]) + + if resource_attributes_to_omit and check.entity_type in resource_attributes_to_omit and \ + resource_attributes_to_omit.get(check.entity_type) in entity_config: + secret = entity_config.get(resource_attributes_to_omit.get(check.entity_type, ''), []) + if isinstance(secret, list) and secret: + secrets.add(secret[0]) + + if not secrets: + logging.debug(f"Secret was not saved in {check.id}, can't omit") + return entity_code_lines + + for idx, line in entity_code_lines: + censored_line = omit_multiple_secret_values_from_line(secrets, line) + censored_code_lines.append((idx, censored_line)) return censored_code_lines diff --git a/checkov/terraform/plan_runner.py b/checkov/terraform/plan_runner.py index 1a8ada4262a..6fcb5b4f8a0 100644 --- a/checkov/terraform/plan_runner.py +++ b/checkov/terraform/plan_runner.py @@ -13,6 +13,7 @@ from checkov.common.checks_infra.registry import get_graph_checks_registry from checkov.common.graph.graph_builder.graph_components.attribute_names import CustomAttributes from checkov.common.output.record import Record +from checkov.common.util.secrets import omit_secret_value_from_checks from checkov.common.bridgecrew.check_type import CheckType from checkov.common.output.report import Report @@ -31,6 +32,14 @@ "CKV_GCP_82", } +RESOURCE_ATTRIBUTES_TO_OMIT = { + 'azurerm_key_vault_secret': 'value', + 'aws_secretsmanager_secret_version': 'secret_string', + 'google_kms_secret_ciphertext': 'plaintext', + 'aws_ssm_parameter': 'value', + 'aws_db_instance': 'password' +} + class Runner(TerraformRunner): check_type = CheckType.TERRAFORM_PLAN # noqa: CCE003 # a static attribute @@ -115,16 +124,21 @@ def run_block(self, entities, entity_lines_range = [entity_context.get('start_line'), entity_context.get('end_line')] entity_code_lines = entity_context.get('code_lines') entity_address = entity_context.get('address') + _, _, entity_config = registry.extract_entity_details(entity) results = registry.scan(scanned_file, entity, [], runner_filter, report_type=CheckType.TERRAFORM_PLAN) for check, check_result in results.items(): if check.id in TF_LIFECYCLE_CHECK_IDS: # can't be evaluated in TF plan continue - + censored_code_lines = omit_secret_value_from_checks(check=check, + check_result=check_result, + entity_code_lines=entity_code_lines, + entity_config=entity_config, + resource_attributes_to_omit=RESOURCE_ATTRIBUTES_TO_OMIT) record = Record(check_id=check.id, bc_check_id=check.bc_id, check_name=check.name, check_result=check_result, - code_block=entity_code_lines, file_path=scanned_file, + code_block=censored_code_lines, file_path=scanned_file, file_line_range=entity_lines_range, resource=entity_id, resource_address=entity_address, evaluations=None, check_class=check.__class__.__module__, file_abs_path=full_file_path, diff --git a/tests/common/utils/conftest.py b/tests/common/utils/conftest.py index afaf530a17e..f1beb49f226 100644 --- a/tests/common/utils/conftest.py +++ b/tests/common/utils/conftest.py @@ -55,4 +55,99 @@ def scan_result_success_response() -> Dict[str, Any]: "p8l4+R7ZCLcic6WOF3Wq5X+G2TnZ263529x9Lw54L51uGzaNZDrhKoupKXEtWsR1UOrfufqwSxflFS3KLjnd5ueu" "anz3q7neGie2cS8HcBin/BL8U8U/AL0XOSX+jt75P82r6+RIV6DoZDXW14oKMNz5rR2TA6fr6j3WG52dFumrjvsG" "sp7dAH12j5wbWz+sG1vfOD6+m3b/8HQd/FwVgXAAA=", - 'compressionMethod': 'gzip'} \ No newline at end of file + 'compressionMethod': 'gzip'} + + +@pytest.fixture +def aws_provider_config_with_secrets(): + return { + '__end_line__': 12, + '__start_line__': 7, + 'access_key': ['AKIAIOSFODNN7EXAMPLE'], + 'alias': ['plain_text_access_keys_provider'], + 'region': ['us-west-1'], + 'secret_key': ['wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'], + 'CKV_AWS_41_secret_access_key': 'AKIAIOSFODNN7EXAMPLE', + 'CKV_AWS_41_secret_secret_key': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + } + + +@pytest.fixture +def aws_provider_lines_with_secrets(): + return [(7, 'provider "aws" {\n'), + (8, ' alias = "plain_text_access_keys_provider"\n'), + (9, ' region = "us-west-1"\n'), + (10, ' access_key = "AKIAIOSFODNN7EXAMPLE"\n'), + (11, ' secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n'), + (12, '}\n')] + + +@pytest.fixture +def aws_provider_lines_without_secrets(): + return [(7, 'provider "aws" {\n'), + (8, ' alias = "plain_text_access_keys_provider"\n'), + (9, ' region = "us-west-1"\n'), + (10, ' access_key = "AKIAI***************"\n'), + (11, ' secret_key = "wJalrXUtnF******************************"\n'), + (12, '}\n')] + + +@pytest.fixture +def tfplan_resource_config_with_secrets(): + return { + 'content_type': [''], + 'expiration_date': [None], + 'id': ['https://test-123-abcdse-02.vault.azure.net/secrets/test-123-abcdse-02-primary-key/352d0b63ac873c528170cb366b570da5'], + 'key_vault_id': ['/subscriptions/resourceGroups/'], + 'name': ['test-123-abcdse-02-primary-key'], + 'not_before_date': [None], + 'resource_id': ['/subscriptions/resourceGroups/'], + 'resource_versionless_id': ['/subscriptions/resourceGroups/'], + 'tags': [{'__startline__': 45, '__endline__': 45, 'start_line': 44, 'end_line': 44}], + 'timeouts': [None], + 'value': ['IClnjeTb8fgd14LyV9m1qG0xvFfUyQY3qHq/slUIrk5='], + 'version': ['123d0b12ab123c123456ab123e120bc1'], + 'versionless_id': ['https://test-123-abcdse-02.vault.azure.net/secrets/test-123-abcdse-02'], + '__startline__': [35], + '__endline__': [50], + 'start_line': [34], + 'end_line': [49], + '__address__': 'module.test.azurerm_key_vault_secret.te_primary_key["test-123-abcdse-02"]'} + + +@pytest.fixture +def tfplan_resource_lines_with_secrets(): + return [(35, ' {\n'), + (36, ' "content_type": "",\n'), + (37, ' "expiration_date": null,\n'), + (38, ' "id": "https://test-123-abcdse-02.vault.azure.net/secrets/test-123-abcdse-02-primary-key/352d0b63ac873c528170cb366b570da5",\n'), + (39, ' "key_vault_id": "abcd/subscriptions/123/resourceGroups/abcd",\n'), + (40, ' "name": "test-123-abcdse-02-primary-key",\n'), + (41, ' "not_before_date": null,\n'), + (42, ' "resource_id": "abcd/subscriptions/123/resourceGroups/abcd",\n'), + (43, ' "resource_versionless_id": "abcd/subscriptions/123/resourceGroups/abcd",\n'), + (44, ' "tags":\n'), + (45, ' {},\n'), + (46, ' "timeouts": null,\n'), + (47, ' "value": "IClnjeTb8fgd14LyV9m1qG0xvFfUyQY3qHq/slUIrk5=",\n'), + (48, ' "version": "123d0b12ab123c123456ab123e120bc1",\n'), + (49, ' "versionless_id": "https://test-123-abcdse-02.vault.azure.net/secrets/test-123-abcdse-02"\n')] + + +@pytest.fixture +def tfplan_resource_lines_without_secrets(): + return [(35, ' {\n'), + (36, ' "content_type": "",\n'), + (37, ' "expiration_date": null,\n'), + (38, ' "id": "https://test-123-abcdse-02.vault.azure.net/secrets/test-123-abcdse-02-primary-key/352d0b63ac873c528170cb366b570da5",\n'), + (39, ' "key_vault_id": "abcd/subscriptions/123/resourceGroups/abcd",\n'), + (40, ' "name": "test-123-abcdse-02-primary-key",\n'), + (41, ' "not_before_date": null,\n'), + (42, ' "resource_id": "abcd/subscriptions/123/resourceGroups/abcd",\n'), + (43, ' "resource_versionless_id": "abcd/subscriptions/123/resourceGroups/abcd",\n'), + (44, ' "tags":\n'), + (45, ' {},\n'), + (46, ' "timeouts": null,\n'), + (47, ' "value": "IClnjeTb8fg*********************************",\n'), + (48, ' "version": "123d0b12ab123c123456ab123e120bc1",\n'), + (49, ' "versionless_id": "https://test-123-abcdse-02.vault.azure.net/secrets/test-123-abcdse-02"\n')] \ No newline at end of file diff --git a/tests/common/utils/test_secrets_utils.py b/tests/common/utils/test_secrets_utils.py new file mode 100644 index 00000000000..383648dc754 --- /dev/null +++ b/tests/common/utils/test_secrets_utils.py @@ -0,0 +1,26 @@ +from checkov.common.util.secrets import omit_secret_value_from_checks +from checkov.common.models.enums import CheckResult +from checkov.terraform.checks.resource.azure.SecretExpirationDate import SecretExpirationDate +from checkov.terraform.checks.provider.aws.credentials import AWSCredentials + + +def test_omit_secret_value_from_checks_by_attribute(tfplan_resource_lines_with_secrets, tfplan_resource_config_with_secrets, + tfplan_resource_lines_without_secrets): + check = SecretExpirationDate() + check.entity_type = 'azurerm_key_vault_secret' + check_result = {'result': CheckResult.FAILED} + resource_attributes_to_omit = {'azurerm_key_vault_secret': 'value'} + + assert omit_secret_value_from_checks(check, check_result, tfplan_resource_lines_with_secrets, + tfplan_resource_config_with_secrets, resource_attributes_to_omit + ) == tfplan_resource_lines_without_secrets + + +def test_omit_secret_value_from_checks_by_secret(aws_provider_lines_with_secrets, aws_provider_config_with_secrets, + aws_provider_lines_without_secrets): + check = AWSCredentials() + check_result = {'result': CheckResult.FAILED} + + assert omit_secret_value_from_checks(check, check_result, aws_provider_lines_with_secrets, + aws_provider_config_with_secrets + ) == aws_provider_lines_without_secrets