feat(kms): add kms_cmk_not_multi_region AWS check (#6794)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Ogonna Iwunze
2025-02-05 16:20:29 +00:00
committed by GitHub
parent 567c729e9e
commit 8d1c476aed
7 changed files with 212 additions and 5 deletions
@@ -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."
}
@@ -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
@@ -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] = []
@@ -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"]
@@ -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)