From 8d1c476aedef23455bbb9f4f0963b926753fd181 Mon Sep 17 00:00:00 2001 From: Ogonna Iwunze <1915636+wunzeco@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:20:29 +0000 Subject: [PATCH] feat(kms): add `kms_cmk_not_multi_region` AWS check (#6794) Co-authored-by: MrCloudSec --- .../kms/kms_cmk_not_multi_region/__init__.py | 0 .../kms_cmk_not_multi_region.metadata.json | 32 ++++ .../kms_cmk_not_multi_region.py | 25 +++ .../providers/aws/services/kms/kms_service.py | 2 + .../kms/kms_cmk_not_multi_region/__init__.py | 0 .../kms_cmk_not_multi_region_test.py | 142 ++++++++++++++++++ .../aws/services/kms/kms_service_test.py | 16 +- 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 prowler/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py create mode 100644 prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json create mode 100644 prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py create mode 100644 tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py create mode 100644 tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py diff --git a/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json new file mode 100644 index 0000000000..c8b603893d --- /dev/null +++ b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "kms_cmk_not_multi_region", + "CheckTitle": "AWS KMS customer managed keys should not be multi-Region", + "CheckType": [ + "Data Protection" + ], + "ServiceName": "kms", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:kms:region:account-id:key/resource-id", + "Severity": "high", + "ResourceType": "AwsKmsKey", + "Description": "Ensure that AWS KMS customer managed keys (CMKs) are not multi-region to maintain strict data control and compliance with security best practices.", + "Risk": "Multi-region KMS keys can increase the risk of unauthorized access and data exposure, as managing access controls and auditing across multiple regions becomes more complex. This expanded attack surface may lead to compliance violations and data breaches.", + "RelatedUrl": "https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html#multi-region-concepts", + "Remediation": { + "Code": { + "CLI": "aws kms create-key --no-multi-region", + "NativeIaC": "", + "Other": "", + "Terraform": "resource \"aws_kms_key\" \"example\" { description = \"Single-region key\" multi_region = false }" + }, + "Recommendation": { + "Text": "Identify and replace multi-region keys with single-region KMS keys to enhance security and access control.", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/mrk-when-to-use.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Multi-region keys should be used only when absolutely necessary, such as for cross-region disaster recovery, and should be carefully managed with strict access controls." +} diff --git a/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py new file mode 100644 index 0000000000..e96aa6a8e7 --- /dev/null +++ b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py @@ -0,0 +1,25 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.kms.kms_client import kms_client + + +class kms_cmk_not_multi_region(Check): + """kms_cmk_not_multi_region verifies if a KMS key is multi-regional""" + + def execute(self) -> List[Check_Report_AWS]: + findings = [] + + for key in kms_client.keys: + if key.manager == "CUSTOMER" and key.state == "Enabled": + report = Check_Report_AWS(metadata=self.metadata(), resource=key) + report.status = "PASS" + report.status_extended = f"KMS CMK {key.id} is a single-region key." + + if key.multi_region: + report.status = "FAIL" + report.status_extended = f"KMS CMK {key.id} is a multi-region key." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/kms/kms_service.py b/prowler/providers/aws/services/kms/kms_service.py index 4cd71fbc9d..e814516547 100644 --- a/prowler/providers/aws/services/kms/kms_service.py +++ b/prowler/providers/aws/services/kms/kms_service.py @@ -51,6 +51,7 @@ class KMS(AWSService): key.origin = response["KeyMetadata"]["Origin"] key.manager = response["KeyMetadata"]["KeyManager"] key.spec = response["KeyMetadata"]["CustomerMasterKeySpec"] + key.multi_region = response["KeyMetadata"]["MultiRegion"] except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -121,4 +122,5 @@ class Key(BaseModel): policy: Optional[dict] spec: Optional[str] region: str + multi_region: Optional[bool] tags: Optional[list] = [] diff --git a/tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py new file mode 100644 index 0000000000..a55618cdbf --- /dev/null +++ b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py @@ -0,0 +1,142 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + + +class Test_kms_cmk_not_multi_region: + @mock_aws + def test_kms_no_keys(self) -> None: + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_kms_cmk_disabled_key(self) -> None: + from prowler.providers.aws.services.kms.kms_service import KMS + + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key()["KeyMetadata"] + kms_client.disable_key(KeyId=key["KeyId"]) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_kms_cmk_is_multi_regional(self) -> None: + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key(MultiRegion=True)["KeyMetadata"] + + # The Prowler service import MUST be made within the decorated + # code not to make real API calls to the AWS service. + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"KMS CMK {key['KeyId']} is a multi-region key." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + + @mock_aws + def test_kms_cmk_is_single_regional(self) -> None: + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key(MultiRegion=False)["KeyMetadata"] + + # The Prowler service import MUST be made within the decorated + # code not to make real API calls to the AWS service. + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"KMS CMK {key['KeyId']} is a single-region key." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] diff --git a/tests/providers/aws/services/kms/kms_service_test.py b/tests/providers/aws/services/kms/kms_service_test.py index d08ca29564..082ccf129f 100644 --- a/tests/providers/aws/services/kms/kms_service_test.py +++ b/tests/providers/aws/services/kms/kms_service_test.py @@ -11,7 +11,7 @@ from tests.providers.aws.utils import ( ) -class Test_ACM_Service: +class Test_KMS_Service: # Test KMS Service @mock_aws @@ -68,6 +68,7 @@ class Test_ACM_Service: kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) # Create KMS keys key1 = kms_client.create_key( + MultiRegion=False, Tags=[ {"TagKey": "test", "TagValue": "test"}, ], @@ -80,6 +81,7 @@ class Test_ACM_Service: assert kms.keys[0].state == key1["KeyState"] assert kms.keys[0].origin == key1["Origin"] assert kms.keys[0].manager == key1["KeyManager"] + assert kms.keys[0].multi_region == key1["MultiRegion"] assert kms.keys[0].tags == [ {"TagKey": "test", "TagValue": "test"}, ] @@ -90,8 +92,8 @@ class Test_ACM_Service: # Generate KMS Client kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) # Create KMS keys - key1 = kms_client.create_key()["KeyMetadata"] - key2 = kms_client.create_key()["KeyMetadata"] + key1 = kms_client.create_key(MultiRegion=False)["KeyMetadata"] + key2 = kms_client.create_key(MultiRegion=False)["KeyMetadata"] kms_client.enable_key_rotation(KeyId=key2["KeyId"]) # KMS client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -138,8 +140,12 @@ class Test_ACM_Service: # Generate KMS Client kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) # Create KMS keys - key1 = kms_client.create_key(Policy=default_policy)["KeyMetadata"] - key2 = kms_client.create_key(Policy=public_policy)["KeyMetadata"] + key1 = kms_client.create_key(MultiRegion=False, Policy=default_policy)[ + "KeyMetadata" + ] + key2 = kms_client.create_key(MultiRegion=False, Policy=public_policy)[ + "KeyMetadata" + ] # KMS client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) kms = KMS(aws_provider)