diff --git a/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/__init__.py b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json new file mode 100644 index 0000000000..1739076a70 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "rds_cluster_protected_by_backup_plan", + "CheckTitle": "Check if RDS clusters are protected by a backup plan.", + "CheckType": [ + "Software and Configuration Checks, AWS Security Best Practices" + ], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "Severity": "medium", + "ResourceType": "AwsRdsDbInstance", + "Description": "Check if RDS clusters are protected by a backup plan.", + "Risk": "Without a backup plan, RDS clusters are vulnerable to data loss, accidental deletion, or corruption. This could lead to significant operational disruptions or loss of critical data.", + "RelatedUrl": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html", + "Remediation": { + "Code": { + "CLI": "aws backup create-backup-plan --backup-plan , aws backup tag-resource --resource-arn --tags Key=backup,Value=true", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-26", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create a backup plan for the RDS cluster to protect it from data loss, accidental deletion, or corruption.", + "Url": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.py b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.py new file mode 100644 index 0000000000..e1b81db62a --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.backup.backup_client import backup_client +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_cluster_protected_by_backup_plan(Check): + def execute(self): + findings = [] + for db_cluster_arn, db_cluster in rds_client.db_clusters.items(): + report = Check_Report_AWS(self.metadata()) + report.region = db_cluster.region + report.resource_id = db_cluster.id + report.resource_arn = db_cluster_arn + report.resource_tags = db_cluster.tags + report.status = "FAIL" + report.status_extended = ( + f"RDS Cluster {db_cluster.id} is not protected by a backup plan." + ) + + if ( + db_cluster_arn in backup_client.protected_resources + or f"arn:{rds_client.audited_partition}:rds:*:*:cluster:*" + in backup_client.protected_resources + or "*" in backup_client.protected_resources + ): + report.status = "PASS" + report.status_extended = ( + f"RDS Cluster {db_cluster.id} is protected by a backup plan." + ) + + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan_test.py b/tests/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan_test.py new file mode 100644 index 0000000000..8f293c6e91 --- /dev/null +++ b/tests/providers/aws/services/rds/rds_cluster_protected_by_backup_plan/rds_cluster_protected_by_backup_plan_test.py @@ -0,0 +1,416 @@ +from unittest import mock + +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_rds_cluster_protected_by_backup_plan: + @mock_aws + def test_rds_no_clusters(self): + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.rds.rds_service import RDS + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.rds_client", + new=RDS(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan import ( + rds_cluster_protected_by_backup_plan, + ) + + check = rds_cluster_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_cluster_no_existing_backup_plans(self): + cluster = mock.MagicMock() + backup = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBCluster + + arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + cluster.db_clusters = { + arn: DBCluster( + id="db-cluster-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1", + endpoint="db-cluster-1.c9akciq32.rds.amazonaws.com", + backtrack=1, + parameter_group="test", + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-cluster-1", + region="us-east-1", + cluster_class="db.m1.small", + engine="aurora-postgres", + allocated_storage=10, + tags=[], + ) + } + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan import ( + rds_cluster_protected_by_backup_plan, + ) + + check = rds_cluster_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is not protected by a backup plan." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + ) + assert result[0].resource_tags == [] + + def test_rds_cluster_without_backup_plan(self): + cluster = mock.MagicMock() + backup = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBCluster + + arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + cluster.db_clusters = { + arn: DBCluster( + id="db-cluster-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1", + endpoint="db-cluster-1.c9akciq32.rds.amazonaws.com", + backtrack=1, + parameter_group="test", + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-cluster-1", + region="us-east-1", + cluster_class="db.m1.small", + engine="aurora-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup.protected_resources = [ + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-2" + ] + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan import ( + rds_cluster_protected_by_backup_plan, + ) + + check = rds_cluster_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is not protected by a backup plan." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + ) + assert result[0].resource_tags == [] + + def test_rds_cluster_with_backup_plan(self): + cluster = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBCluster + + arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + cluster.db_clusters = { + arn: DBCluster( + id="db-cluster-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1", + endpoint="db-cluster-1.c9akciq32.rds.amazonaws.com", + backtrack=1, + parameter_group="test", + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-cluster-1", + region="us-east-1", + cluster_class="db.m1.small", + engine="aurora-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup = mock.MagicMock() + backup.protected_resources = [arn] + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan import ( + rds_cluster_protected_by_backup_plan, + ) + + check = rds_cluster_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is protected by a backup plan." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + ) + assert result[0].resource_tags == [] + + def test_rds_cluster_with_backup_plan_via_cluster_wildcard(self): + cluster = mock.MagicMock() + cluster.audited_partition = "aws" + + from prowler.providers.aws.services.rds.rds_service import DBCluster + + arn = "arn:aws:rds:*:*:cluster:*" + cluster.db_clusters = { + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1": DBCluster( + id="db-cluster-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1", + endpoint="db-cluster-1.c9akciq32.rds.amazonaws.com", + backtrack=1, + parameter_group="test", + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-cluster-1", + region="us-east-1", + cluster_class="db.m1.small", + engine="aurora-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup = mock.MagicMock() + backup.protected_resources = [arn] + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan import ( + rds_cluster_protected_by_backup_plan, + ) + + check = rds_cluster_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is protected by a backup plan." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + ) + assert result[0].resource_tags == [] + + def test_rds_cluster_with_backup_plan_via_all_wildcard(self): + cluster = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBCluster + + arn = "*" + cluster.db_clusters = { + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1": DBCluster( + id="db-cluster-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1", + endpoint="db-cluster-1.c9akciq32.rds.amazonaws.com", + backtrack=1, + parameter_group="test", + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-cluster-1", + region="us-east-1", + cluster_class="db.m1.small", + engine="aurora-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup = mock.MagicMock() + backup.protected_resources = [arn] + + 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, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=cluster, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_protected_by_backup_plan.rds_cluster_protected_by_backup_plan import ( + rds_cluster_protected_by_backup_plan, + ) + + check = rds_cluster_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is protected by a backup plan." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-cluster-1" + ) + assert result[0].resource_tags == []