From 097e61ab9d75f47aa76abaa66ef969f82c09bd3b Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:28:16 +0200 Subject: [PATCH] feat(elasticache): Ensure Redis Cache Clusters Automatically Install Minor Updates (#4699) --- .../__init__.py | 0 ..._auto_minor_version_upgrades.metadata.json | 30 +++ ...dis_cluster_auto_minor_version_upgrades.py | 25 +++ .../elasticache/elasticache_service.py | 25 ++- ...luster_auto_minor_version_upgrades_test.py | 184 ++++++++++++++++++ .../elasticache/elasticache_service_test.py | 5 + 6 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/__init__.py create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.py create mode 100644 tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/__init__.py b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json new file mode 100644 index 0000000000..80cef26283 --- /dev/null +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json @@ -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 --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": "" +} diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.py b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.py new file mode 100644 index 0000000000..f28fd62462 --- /dev/null +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.py @@ -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 diff --git a/prowler/providers/aws/services/elasticache/elasticache_service.py b/prowler/providers/aws/services/elasticache/elasticache_service.py index 980e3540a1..f594c93da0 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_service.py +++ b/prowler/providers/aws/services/elasticache/elasticache_service.py @@ -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 diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py new file mode 100644 index 0000000000..5193d81aa2 --- /dev/null +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py @@ -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 diff --git a/tests/providers/aws/services/elasticache/elasticache_service_test.py b/tests/providers/aws/services/elasticache/elasticache_service_test.py index c4bb7dcca1..bbebc7925b 100644 --- a/tests/providers/aws/services/elasticache/elasticache_service_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_service_test.py @@ -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, )