feat(elasticache): Ensure Redis Cache Clusters Automatically Install Minor Updates (#4699)

This commit is contained in:
Hugo Pereira Brito
2024-08-14 14:28:16 +02:00
committed by GitHub
parent 52d83bd83b
commit 097e61ab9d
6 changed files with 260 additions and 9 deletions
@@ -0,0 +1,30 @@
{
"Provider": "aws",
"CheckID": "elasticache_redis_cluster_auto_minor_version_upgrades",
"CheckTitle": "Ensure Elasticache Redis cache clusters have automatic minor upgrades enabled.",
"CheckType": [],
"ServiceName": "elasticache",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
"Severity": "high",
"ResourceType": "AWSElastiCacheClusters",
"Description": "Ensure Elasticache Redis cache clusters have automatic minor upgrades enabled.",
"Risk": "Not enabling automatic minor version upgrades can expose your Redis cluster to security vulnerabilities, performance issues, and increased operational overhead due to the need for manual updates.",
"RelatedUrl": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/VersionManagement.html",
"Remediation": {
"Code": {
"CLI": "aws elasticache modify-cache-cluster --cache-cluster-id <cluster_id> --apply-immediately --auto-minor-version-upgrade",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-2",
"Terraform": ""
},
"Recommendation": {
"Text": "Ensure Elasticache clusters have automatic minor upgrades enabled.",
"Url": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Clusters.html#Modify"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,25 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.elasticache.elasticache_client import (
elasticache_client,
)
class elasticache_redis_cluster_auto_minor_version_upgrades(Check):
def execute(self):
findings = []
for repl_group in elasticache_client.replication_groups.values():
report = Check_Report_AWS(self.metadata())
report.region = repl_group.region
report.resource_id = repl_group.id
report.resource_arn = repl_group.arn
report.resource_tags = repl_group.tags
report.status = "PASS"
report.status_extended = f"Elasticache Redis cache cluster {repl_group.id} does have automated minor version upgrades enabled."
if not repl_group.auto_minor_version_upgrade:
report.status = "FAIL"
report.status_extended = f"Elasticache Redis cache cluster {repl_group.id} does not have automated minor version upgrades enabled."
findings.append(report)
return findings
@@ -7,19 +7,18 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
################################ Elasticache
class ElastiCache(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.clusters = {}
self.replication_groups = {}
self.__threading_call__(self.__describe_cache_clusters__)
self.__threading_call__(self.__describe_cache_subnet_groups__)
self.__threading_call__(self.__describe_replication_groups__)
self.__list_tags_for_resource__()
self.__threading_call__(self._describe_cache_clusters)
self.__threading_call__(self._describe_cache_subnet_groups)
self.__threading_call__(self._describe_replication_groups)
self._list_tags_for_resource()
def __describe_cache_clusters__(self, regional_client):
def _describe_cache_clusters(self, regional_client):
# Memcached Clusters and Redis Nodes
logger.info("Elasticache - Describing Cache Clusters...")
try:
@@ -39,6 +38,9 @@ class ElastiCache(AWSService):
cache_subnet_group_id=cache_cluster.get(
"CacheSubnetGroupName", None
),
auto_minor_version_upgrade=cache_cluster.get(
"AutoMinorVersionUpgrade", False
),
)
except Exception as error:
logger.error(
@@ -49,7 +51,7 @@ class ElastiCache(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __describe_cache_subnet_groups__(self, regional_client):
def _describe_cache_subnet_groups(self, regional_client):
logger.info("Elasticache - Describing Cache Subnet Groups...")
try:
for cluster in self.clusters.values():
@@ -76,7 +78,7 @@ class ElastiCache(AWSService):
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __describe_replication_groups__(self, regional_client):
def _describe_replication_groups(self, regional_client):
# Redis Clusters
logger.info("Elasticache - Describing Replication Groups...")
try:
@@ -101,6 +103,9 @@ class ElastiCache(AWSService):
"TransitEncryptionEnabled", False
),
multi_az=repl_group.get("MultiAZ", "disabled"),
auto_minor_version_upgrade=repl_group.get(
"AutoMinorVersionUpgrade", False
),
)
except Exception as error:
logger.error(
@@ -111,7 +116,7 @@ class ElastiCache(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __list_tags_for_resource__(self):
def _list_tags_for_resource(self):
logger.info("Elasticache - Listing Tags...")
try:
for cluster in self.clusters.values():
@@ -158,6 +163,7 @@ class Cluster(BaseModel):
cache_subnet_group_id: Optional[str]
subnets: list = []
tags: Optional[list]
auto_minor_version_upgrade: bool = False
class ReplicationGroup(BaseModel):
@@ -170,3 +176,4 @@ class ReplicationGroup(BaseModel):
transit_encryption: bool
multi_az: str
tags: Optional[list]
auto_minor_version_upgrade: bool = False
@@ -0,0 +1,184 @@
from unittest import mock
from mock import MagicMock
from prowler.providers.aws.services.elasticache.elasticache_service import (
ReplicationGroup,
)
from tests.providers.aws.services.elasticache.elasticache_service_test import (
AUTO_MINOR_VERSION_UPGRADE,
REPLICATION_GROUP_ARN,
REPLICATION_GROUP_ENCRYPTION,
REPLICATION_GROUP_ID,
REPLICATION_GROUP_MULTI_AZ,
REPLICATION_GROUP_SNAPSHOT_RETENTION,
REPLICATION_GROUP_STATUS,
REPLICATION_GROUP_TAGS,
REPLICATION_GROUP_TRANSIT_ENCRYPTION,
)
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
VPC_ID = "vpc-12345678901234567"
class Test_elasticache_redis_cluster_auto_minor_version_upgrades:
def test_elasticache_no_clusters(self):
# Mock VPC Service
vpc_client = MagicMock
vpc_client.vpc_subnets = {}
# Mock ElastiCache Service
elasticache_service = MagicMock
elasticache_service.replication_groups = {}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache",
new=elasticache_service,
), mock.patch(
"prowler.providers.aws.services.vpc.vpc_service.VPC",
new=vpc_client,
), mock.patch(
"prowler.providers.aws.services.vpc.vpc_client.vpc_client",
new=vpc_client,
):
from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_auto_minor_version_upgrades.elasticache_redis_cluster_auto_minor_version_upgrades import (
elasticache_redis_cluster_auto_minor_version_upgrades,
)
check = elasticache_redis_cluster_auto_minor_version_upgrades()
result = check.execute()
assert len(result) == 0
def test_elasticache_clusters_auto_minor_version_upgrades_undefined(self):
# Mock ElastiCache Service
elasticache_service = MagicMock
elasticache_service.replication_groups = {}
elasticache_service.replication_groups[REPLICATION_GROUP_ARN] = (
ReplicationGroup(
arn=REPLICATION_GROUP_ARN,
id=REPLICATION_GROUP_ID,
region=AWS_REGION_US_EAST_1,
status=REPLICATION_GROUP_STATUS,
snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION,
encrypted=REPLICATION_GROUP_ENCRYPTION,
transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION,
multi_az=REPLICATION_GROUP_MULTI_AZ,
tags=REPLICATION_GROUP_TAGS,
)
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache",
new=elasticache_service,
):
from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_auto_minor_version_upgrades.elasticache_redis_cluster_auto_minor_version_upgrades import (
elasticache_redis_cluster_auto_minor_version_upgrades,
)
check = elasticache_redis_cluster_auto_minor_version_upgrades()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Elasticache Redis cache cluster {REPLICATION_GROUP_ID} does not have automated minor version upgrades enabled."
)
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_id == REPLICATION_GROUP_ID
assert result[0].resource_arn == REPLICATION_GROUP_ARN
assert result[0].resource_tags == REPLICATION_GROUP_TAGS
def test_elasticache_clusters_auto_minor_version_upgrades_disabled(self):
# Mock ElastiCache Service
elasticache_service = MagicMock
elasticache_service.replication_groups = {}
elasticache_service.replication_groups[REPLICATION_GROUP_ARN] = (
ReplicationGroup(
arn=REPLICATION_GROUP_ARN,
id=REPLICATION_GROUP_ID,
region=AWS_REGION_US_EAST_1,
status=REPLICATION_GROUP_STATUS,
snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION,
encrypted=REPLICATION_GROUP_ENCRYPTION,
transit_encryption=False,
multi_az=REPLICATION_GROUP_MULTI_AZ,
tags=REPLICATION_GROUP_TAGS,
auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE,
)
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache",
new=elasticache_service,
):
from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_auto_minor_version_upgrades.elasticache_redis_cluster_auto_minor_version_upgrades import (
elasticache_redis_cluster_auto_minor_version_upgrades,
)
check = elasticache_redis_cluster_auto_minor_version_upgrades()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Elasticache Redis cache cluster {REPLICATION_GROUP_ID} does not have automated minor version upgrades enabled."
)
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_id == REPLICATION_GROUP_ID
assert result[0].resource_arn == REPLICATION_GROUP_ARN
assert result[0].resource_tags == REPLICATION_GROUP_TAGS
def test_elasticache_clusters_auto_minor_version_upgrades_enabled(self):
# Mock ElastiCache Service
elasticache_service = MagicMock
elasticache_service.replication_groups = {}
elasticache_service.replication_groups[REPLICATION_GROUP_ARN] = (
ReplicationGroup(
arn=REPLICATION_GROUP_ARN,
id=REPLICATION_GROUP_ID,
region=AWS_REGION_US_EAST_1,
status=REPLICATION_GROUP_STATUS,
snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION,
encrypted=REPLICATION_GROUP_ENCRYPTION,
transit_encryption=False,
multi_az=REPLICATION_GROUP_MULTI_AZ,
tags=REPLICATION_GROUP_TAGS,
auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE,
)
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
), mock.patch(
"prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache",
new=elasticache_service,
):
from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_auto_minor_version_upgrades.elasticache_redis_cluster_auto_minor_version_upgrades import (
elasticache_redis_cluster_auto_minor_version_upgrades,
)
check = elasticache_redis_cluster_auto_minor_version_upgrades()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Elasticache Redis cache cluster {REPLICATION_GROUP_ID} does have automated minor version upgrades enabled."
)
assert result[0].region == AWS_REGION_US_EAST_1
assert result[0].resource_id == REPLICATION_GROUP_ID
assert result[0].resource_arn == REPLICATION_GROUP_ARN
assert result[0].resource_tags == REPLICATION_GROUP_TAGS
@@ -37,6 +37,7 @@ REPLICATION_GROUP_MULTI_AZ = "enabled"
REPLICATION_GROUP_TAGS = [
{"Key": "environment", "Value": "test"},
]
AUTO_MINOR_VERSION_UPGRADE = True
# Mocking Access Analyzer Calls
@@ -60,6 +61,7 @@ def mock_make_api_call(self, operation_name, kwargs):
"ARN": ELASTICACHE_CLUSTER_ARN,
"Engine": ELASTICACHE_ENGINE,
"SecurityGroups": [],
"AutoMinorVersionUpgrade": AUTO_MINOR_VERSION_UPGRADE,
},
]
}
@@ -104,6 +106,7 @@ def mock_make_api_call(self, operation_name, kwargs):
"TransitEncryptionEnabled": REPLICATION_GROUP_TRANSIT_ENCRYPTION,
"AtRestEncryptionEnabled": REPLICATION_GROUP_ENCRYPTION,
"ARN": REPLICATION_GROUP_ARN,
"AutoMinorVersionUpgrade": AUTO_MINOR_VERSION_UPGRADE,
},
]
}
@@ -166,6 +169,7 @@ class Test_ElastiCache_Service:
cache_subnet_group_id=SUBNET_GROUP_NAME,
subnets=[SUBNET_1, SUBNET_2],
tags=ELASTICACHE_CLUSTER_TAGS,
auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE,
)
# Test Elasticache Redis cache clusters
@@ -187,4 +191,5 @@ class Test_ElastiCache_Service:
transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION,
multi_az=REPLICATION_GROUP_MULTI_AZ,
tags=REPLICATION_GROUP_TAGS,
auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE,
)