From 0b566f96662d6fee68540ec7bdf5ccb783079940 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:19:57 +0200 Subject: [PATCH] feat(dynamodb): add new check `dynamodb_table_deletion_protection_enabled` (#5148) --- .../aws/services/dynamodb/dynamodb_service.py | 5 + .../__init__.py | 0 ..._deletion_protection_enabled.metadata.json | 32 +++++ ...amodb_table_deletion_protection_enabled.py | 25 ++++ .../dynamodb/dynamodb_service_test.py | 2 + ..._table_deletion_protection_enabled_test.py | 133 ++++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/__init__.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py create mode 100644 tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_service.py b/prowler/providers/aws/services/dynamodb/dynamodb_service.py index ba40f87a5c..27861b2244 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_service.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_service.py @@ -54,6 +54,10 @@ class DynamoDB(AWSService): table.encryption_type = properties["SSEDescription"]["SSEType"] if table.encryption_type == "KMS": table.kms_arn = properties["SSEDescription"]["KMSMasterKeyArn"] + + table.deletion_protection = properties.get( + "DeletionProtectionEnabled", False + ) except Exception as error: logger.error( f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -219,6 +223,7 @@ class Table(BaseModel): policy: Optional[dict] = None region: str tags: Optional[list] = [] + deletion_protection: bool = False class Cluster(BaseModel): diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/__init__.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json new file mode 100644 index 0000000000..5516e61c76 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "dynamodb_table_deletion_protection_enabled", + "CheckTitle": "Check if DynamoDB tables have deletion protection enabled.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "dynamodb", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:dynamodb:region:account-id:table/table-name", + "Severity": "medium", + "ResourceType": "AwsDynamoDbTable", + "Description": "This control checks whether an Amazon DynamoDB table has deletion protection enabled to prevent accidental deletion during regular table management operations.", + "Risk": "If deletion protection is not enabled, a DynamoDB table could be accidentally deleted, leading to data loss and potential disruption of business operations.", + "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.Basics.html#WorkingWithTables.Basics.DeletionProtection", + "Remediation": { + "Code": { + "CLI": "aws dynamodb update-table --table-name --deletion-protection-enabled", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable deletion protection for your DynamoDB tables to prevent accidental deletion.", + "Url": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.Basics.html#WorkingWithTables.Basics.DeletionProtection" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py new file mode 100644 index 0000000000..e1e79caba8 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_client + + +class dynamodb_table_deletion_protection_enabled(Check): + def execute(self): + findings = [] + for table_arn, table in dynamodb_client.tables.items(): + report = Check_Report_AWS(self.metadata()) + report.region = table.region + report.resource_id = table.name + report.resource_arn = table_arn + report.resource_tags = table.tags + report.status = "FAIL" + report.status_extended = f"DynamoDB table {table.name} does not have deletion protection enabled." + + if table.deletion_protection: + report.status = "PASS" + report.status_extended = ( + f"DynamoDB table {table.name} has deletion protection enabled." + ) + + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/dynamodb/dynamodb_service_test.py b/tests/providers/aws/services/dynamodb/dynamodb_service_test.py index a2ef7d0eae..48ce9c7648 100644 --- a/tests/providers/aws/services/dynamodb/dynamodb_service_test.py +++ b/tests/providers/aws/services/dynamodb/dynamodb_service_test.py @@ -103,6 +103,7 @@ class Test_DynamoDB_Service: Tags=[ {"Key": "test", "Value": "test"}, ], + DeletionProtectionEnabled=True, )["TableDescription"] # DynamoDB client for this test class aws_provider = set_mocked_aws_provider() @@ -115,6 +116,7 @@ class Test_DynamoDB_Service: assert tables.tags == [ {"Key": "test", "Value": "test"}, ] + assert tables.deletion_protection # Test DynamoDB Describe Continuous Backups @mock_aws diff --git a/tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py b/tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py new file mode 100644 index 0000000000..8020816aca --- /dev/null +++ b/tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py @@ -0,0 +1,133 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_dynamodb_table_deletion_protection_enabled: + @mock_aws + def test_dynamodb_no_tables(self): + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, 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.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled.dynamodb_client", + new=DynamoDB(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled import ( + dynamodb_table_deletion_protection_enabled, + ) + + check = dynamodb_table_deletion_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_dynamodb_table_with_deletion_protection_enabled(self): + dynamodb_client = client("dynamodb", region_name=AWS_REGION_US_EAST_1) + table = dynamodb_client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + DeletionProtectionEnabled=True, + BillingMode="PAY_PER_REQUEST", + )["TableDescription"] + + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, 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.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled.dynamodb_client", + new=DynamoDB(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled import ( + dynamodb_table_deletion_protection_enabled, + ) + + check = dynamodb_table_deletion_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "DynamoDB table test1 has deletion protection enabled." + ) + assert result[0].resource_id == table["TableName"] + assert result[0].resource_arn == table["TableArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_dynamodb_table_without_deletion_protection_enabled(self): + dynamodb_client = client("dynamodb", region_name=AWS_REGION_US_EAST_1) + table = dynamodb_client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + DeletionProtectionEnabled=False, + BillingMode="PAY_PER_REQUEST", + )["TableDescription"] + + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, 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.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled.dynamodb_client", + new=DynamoDB(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled import ( + dynamodb_table_deletion_protection_enabled, + ) + + check = dynamodb_table_deletion_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "DynamoDB table test1 does not have deletion protection enabled." + ) + assert result[0].resource_id == table["TableName"] + assert result[0].resource_arn == table["TableArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == []