diff --git a/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.py b/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.py index 4d21002d..97a5dfe2 100644 --- a/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.py +++ b/prowler/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection.py @@ -10,16 +10,23 @@ class rds_instance_deletion_protection(Check): report.region = db_instance.region report.resource_id = db_instance.id report.resource_tags = db_instance.tags - if db_instance.deletion_protection: - report.status = "PASS" - report.status_extended = ( - f"RDS Instance {db_instance.id} deletion protection is enabled." - ) + # Check if is member of a cluster + if db_instance.cluster_id: + if rds_client.db_clusters[db_instance.cluster_id].deletion_protection: + report.status = "PASS" + report.status_extended = f"RDS Instance {db_instance.id} deletion protection is enabled at cluster {db_instance.cluster_id} level." + else: + report.status = "FAIL" + report.status_extended = f"RDS Instance {db_instance.id} deletion protection is not enabled at cluster {db_instance.cluster_id} level." else: - report.status = "FAIL" - report.status_extended = ( - f"RDS Instance {db_instance.id} deletion protection is not enabled." - ) + if db_instance.deletion_protection: + report.status = "PASS" + report.status_extended = ( + f"RDS Instance {db_instance.id} deletion protection is enabled." + ) + else: + report.status = "FAIL" + report.status_extended = f"RDS Instance {db_instance.id} deletion protection is not enabled." findings.append(report) diff --git a/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.py b/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.py index 004b0891..56ed3ba8 100644 --- a/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.py +++ b/prowler/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az.py @@ -10,16 +10,25 @@ class rds_instance_multi_az(Check): report.region = db_instance.region report.resource_id = db_instance.id report.resource_tags = db_instance.tags - if db_instance.multi_az: - report.status = "PASS" - report.status_extended = ( - f"RDS Instance {db_instance.id} has multi-AZ enabled." - ) + # Check if is member of a cluster + if db_instance.cluster_id: + if rds_client.db_clusters[db_instance.cluster_id].multi_az: + report.status = "PASS" + report.status_extended = f"RDS Instance {db_instance.id} has multi-AZ enabled at cluster {db_instance.cluster_id} level." + else: + report.status = "FAIL" + report.status_extended = f"RDS Instance {db_instance.id} does not have multi-AZ enabled at cluster {db_instance.cluster_id} level." else: - report.status = "FAIL" - report.status_extended = ( - f"RDS Instance {db_instance.id} does not have multi-AZ enabled." - ) + if db_instance.multi_az: + report.status = "PASS" + report.status_extended = ( + f"RDS Instance {db_instance.id} has multi-AZ enabled." + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"RDS Instance {db_instance.id} does not have multi-AZ enabled." + ) findings.append(report) diff --git a/prowler/providers/aws/services/rds/rds_service.py b/prowler/providers/aws/services/rds/rds_service.py index b3cf9db4..9b872d6e 100644 --- a/prowler/providers/aws/services/rds/rds_service.py +++ b/prowler/providers/aws/services/rds/rds_service.py @@ -18,12 +18,14 @@ class RDS: self.audit_resources = audit_info.audit_resources self.regional_clients = generate_regional_clients(self.service, audit_info) self.db_instances = [] + self.db_clusters = {} self.db_snapshots = [] self.db_cluster_snapshots = [] self.__threading_call__(self.__describe_db_instances__) self.__threading_call__(self.__describe_db_parameters__) self.__threading_call__(self.__describe_db_snapshots__) self.__threading_call__(self.__describe_db_snapshot_attributes__) + self.__threading_call__(self.__describe_db_clusters__) self.__threading_call__(self.__describe_db_cluster_snapshots__) self.__threading_call__(self.__describe_db_cluster_snapshot_attributes__) @@ -79,6 +81,7 @@ class RDS: for item in instance["DBParameterGroups"] ], multi_az=instance["MultiAZ"], + cluster_id=instance.get("DBClusterIdentifier"), region=regional_client.region, tags=instance.get("TagList"), ) @@ -157,6 +160,50 @@ class RDS: f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def __describe_db_clusters__(self, regional_client): + logger.info("RDS - Describe Clusters...") + try: + describe_db_clusters_paginator = regional_client.get_paginator( + "describe_db_clusters" + ) + for page in describe_db_clusters_paginator.paginate(): + for cluster in page["DBClusters"]: + if not self.audit_resources or ( + is_resource_filtered( + cluster["DBClusterIdentifier"], self.audit_resources + ) + ): + if cluster["Engine"] != "docdb": + db_cluster = DBCluster( + id=cluster["DBClusterIdentifier"], + endpoint=cluster.get("Endpoint"), + engine=cluster["Engine"], + status=cluster["Status"], + public=cluster.get("PubliclyAccessible", False), + encrypted=cluster["StorageEncrypted"], + auto_minor_version_upgrade=cluster.get( + "AutoMinorVersionUpgrade", False + ), + backup_retention_period=cluster.get( + "BackupRetentionPeriod" + ), + cloudwatch_logs=cluster.get( + "EnabledCloudwatchLogsExports" + ), + deletion_protection=cluster["DeletionProtection"], + parameter_group=cluster["DBClusterParameterGroup"], + multi_az=cluster["MultiAZ"], + region=regional_client.region, + tags=cluster.get("TagList"), + ) + self.db_clusters[ + cluster["DBClusterIdentifier"] + ] = db_cluster + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def __describe_db_cluster_snapshots__(self, regional_client): logger.info("RDS - Describe Cluster Snapshots...") try: @@ -218,6 +265,24 @@ class DBInstance(BaseModel): multi_az: bool parameter_groups: list[str] = [] parameters: list[dict] = [] + cluster_id: Optional[str] + region: str + tags: Optional[list] = [] + + +class DBCluster(BaseModel): + id: str + endpoint: Optional[str] + engine: str + status: str + public: bool + encrypted: bool + backup_retention_period: int = 0 + cloudwatch_logs: Optional[list] + deletion_protection: bool + auto_minor_version_upgrade: bool + multi_az: bool + parameter_group: Optional[str] region: str tags: Optional[list] = [] diff --git a/tests/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection_test.py b/tests/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection_test.py index f6d69d22..3729cfa1 100644 --- a/tests/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection_test.py +++ b/tests/providers/aws/services/rds/rds_instance_deletion_protection/rds_instance_deletion_protection_test.py @@ -98,7 +98,7 @@ class Test_rds_instance_deletion_protection: assert result[0].resource_id == "db-master-1" @mock_rds - def test_rds_instance_with_encryption(self): + def test_rds_instance_with_deletion_protection(self): conn = client("rds", region_name=AWS_REGION) conn.create_db_instance( DBInstanceIdentifier="db-master-1", @@ -136,3 +136,107 @@ class Test_rds_instance_deletion_protection: result[0].status_extended, ) assert result[0].resource_id == "db-master-1" + + @mock_rds + def test_rds_instance_without_cluster_deletion_protection(self): + conn = client("rds", region_name=AWS_REGION) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + AllocatedStorage=10, + Engine="postgres", + DatabaseName="staging-postgres", + DeletionProtection=False, + MasterUsername="test", + MasterUserPassword="password", + Tags=[ + {"Key": "test", "Value": "test"}, + ], + ) + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + DBClusterIdentifier="db-cluster-1", + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + audit_info = self.set_mocked_audit_info() + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=audit_info, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_deletion_protection.rds_instance_deletion_protection.rds_client", + new=RDS(audit_info), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_deletion_protection.rds_instance_deletion_protection import ( + rds_instance_deletion_protection, + ) + + check = rds_instance_deletion_protection() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + "deletion protection is not enabled at cluster", + result[0].status_extended, + ) + assert result[0].resource_id == "db-master-1" + + @mock_rds + def test_rds_instance_with_cluster_deletion_protection(self): + conn = client("rds", region_name=AWS_REGION) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + AllocatedStorage=10, + Engine="postgres", + DatabaseName="staging-postgres", + DeletionProtection=True, + MasterUsername="test", + MasterUserPassword="password", + Tags=[ + {"Key": "test", "Value": "test"}, + ], + ) + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + DBClusterIdentifier="db-cluster-1", + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + audit_info = self.set_mocked_audit_info() + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=audit_info, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_deletion_protection.rds_instance_deletion_protection.rds_client", + new=RDS(audit_info), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_deletion_protection.rds_instance_deletion_protection import ( + rds_instance_deletion_protection, + ) + + check = rds_instance_deletion_protection() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "deletion protection is enabled at cluster", + result[0].status_extended, + ) + assert result[0].resource_id == "db-master-1" diff --git a/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py b/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py index 475d3d0e..0f9859aa 100644 --- a/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py +++ b/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py @@ -5,6 +5,7 @@ from boto3 import client, session from moto import mock_rds from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info +from prowler.providers.aws.services.rds.rds_service import DBCluster, DBInstance AWS_ACCOUNT_NUMBER = "123456789012" AWS_REGION = "us-east-1" @@ -136,3 +137,123 @@ class Test_rds_instance_multi_az: result[0].status_extended, ) assert result[0].resource_id == "db-master-1" + + def test_rds_instance_in_cluster_multi_az(self): + rds_client = mock.MagicMock + rds_client.db_clusters = { + "test-cluster": DBCluster( + id="test-cluster", + endpoint="", + engine="aurora", + status="available", + public=False, + encrypted=False, + auto_minor_version_upgrade=False, + backup_retention_period=0, + cloudwatch_logs=[], + deletion_protection=False, + parameter_group="", + multi_az=True, + region=AWS_REGION, + tags=[], + ) + } + rds_client.db_instances = [ + DBInstance( + id="test-instance", + endpoint="", + engine="aurora", + status="available", + public=False, + encrypted=False, + auto_minor_version_upgrade=False, + backup_retention_period=0, + cloudwatch_logs=[], + deletion_protection=False, + parameter_group=[], + multi_az=False, + cluster_id="test-cluster", + region=AWS_REGION, + tags=[], + ) + ] + + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_multi_az.rds_instance_multi_az.rds_client", + new=rds_client, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_multi_az.rds_instance_multi_az import ( + rds_instance_multi_az, + ) + + check = rds_instance_multi_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "has multi-AZ enabled at cluster", + result[0].status_extended, + ) + assert result[0].resource_id == "test-instance" + + def test_rds_instance_in_cluster_without_multi_az(self): + rds_client = mock.MagicMock + rds_client.db_clusters = { + "test-cluster": DBCluster( + id="test-cluster", + endpoint="", + engine="aurora", + status="available", + public=False, + encrypted=False, + auto_minor_version_upgrade=False, + backup_retention_period=0, + cloudwatch_logs=[], + deletion_protection=False, + parameter_group="", + multi_az=False, + region=AWS_REGION, + tags=[], + ) + } + rds_client.db_instances = [ + DBInstance( + id="test-instance", + endpoint="", + engine="aurora", + status="available", + public=False, + encrypted=False, + auto_minor_version_upgrade=False, + backup_retention_period=0, + cloudwatch_logs=[], + deletion_protection=False, + parameter_group=[], + multi_az=False, + cluster_id="test-cluster", + region=AWS_REGION, + tags=[], + ) + ] + + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_multi_az.rds_instance_multi_az.rds_client", + new=rds_client, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_multi_az.rds_instance_multi_az import ( + rds_instance_multi_az, + ) + + check = rds_instance_multi_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + "does not have multi-AZ enabled at cluster", + result[0].status_extended, + ) + assert result[0].resource_id == "test-instance" diff --git a/tests/providers/aws/services/rds/rds_service_test.py b/tests/providers/aws/services/rds/rds_service_test.py index d19403ff..22e9524f 100644 --- a/tests/providers/aws/services/rds/rds_service_test.py +++ b/tests/providers/aws/services/rds/rds_service_test.py @@ -176,6 +176,55 @@ class Test_RDS_Service: assert rds.db_snapshots[0].region == AWS_REGION assert not rds.db_snapshots[0].public + # Test RDS Describe DB Clusters + @mock_rds + def test__describe_db_clusters__(self): + conn = client("rds", region_name=AWS_REGION) + cluster_id = "db-master-1" + conn.create_db_parameter_group( + DBParameterGroupName="test", + DBParameterGroupFamily="default.postgres9.3", + Description="test parameter group", + ) + conn.create_db_cluster( + DBClusterIdentifier=cluster_id, + AllocatedStorage=10, + Engine="postgres", + DatabaseName="staging-postgres", + StorageEncrypted=False, + DeletionProtection=True, + PubliclyAccessible=False, + AutoMinorVersionUpgrade=False, + BackupRetentionPeriod=1, + MasterUsername="test", + MasterUserPassword="password", + EnableCloudwatchLogsExports=["audit", "error"], + DBClusterParameterGroupName="test", + Tags=[ + {"Key": "test", "Value": "test"}, + ], + ) + # RDS client for this test class + audit_info = self.set_mocked_audit_info() + rds = RDS(audit_info) + assert len(rds.db_clusters) == 1 + assert rds.db_clusters[cluster_id].id == "db-master-1" + assert rds.db_clusters[cluster_id].engine == "postgres" + assert rds.db_clusters[cluster_id].region == AWS_REGION + assert f"{AWS_REGION}.rds.amazonaws.com" in rds.db_clusters[cluster_id].endpoint + assert rds.db_clusters[cluster_id].status == "available" + assert not rds.db_clusters[cluster_id].public + assert not rds.db_clusters[cluster_id].encrypted + assert rds.db_clusters[cluster_id].backup_retention_period == 1 + assert rds.db_clusters[cluster_id].cloudwatch_logs == ["audit", "error"] + assert rds.db_clusters[cluster_id].deletion_protection + assert not rds.db_clusters[cluster_id].auto_minor_version_upgrade + assert not rds.db_clusters[cluster_id].multi_az + assert rds.db_clusters[cluster_id].tags == [ + {"Key": "test", "Value": "test"}, + ] + assert rds.db_clusters[cluster_id].parameter_group == "test" + # Test RDS Describe DB Cluster Snapshots @mock_rds def test__describe_db_cluster_snapshots__(self):