From bc8f3eba4d0a584c14e8ad1913611b5ea3611866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Wed, 25 Sep 2024 17:02:53 +0200 Subject: [PATCH] feat(backup): add tags to backup vaults and backup plans (#5194) --- .../backup_plans_exist/backup_plans_exist.py | 2 + .../aws/services/backup/backup_service.py | 16 ++- .../backup_vaults_encrypted.py | 1 + .../backup_vaults_exist.py | 2 + .../backup_plans_exist_test.py | 3 + .../services/backup/backup_service_test.py | 103 ++++++++++++++++-- .../backup_vaults_encrypted_test.py | 4 + .../backup_vaults_exist_test.py | 3 + 8 files changed, 123 insertions(+), 11 deletions(-) diff --git a/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py b/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py index 3823b95a59..4309985757 100644 --- a/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py +++ b/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py @@ -12,6 +12,7 @@ class backup_plans_exist(Check): report.resource_arn = backup_client.backup_plans[0].arn report.resource_id = backup_client.backup_plans[0].name report.region = backup_client.backup_plans[0].region + report.resource_tags = backup_client.backup_plans[0].tags findings.append(report) elif backup_client.backup_vaults: report = Check_Report_AWS(self.metadata()) @@ -20,5 +21,6 @@ class backup_plans_exist(Check): report.resource_arn = backup_client.backup_plan_arn_template report.resource_id = backup_client.audited_account report.region = backup_client.region + report.resource_tags = [] findings.append(report) return findings diff --git a/prowler/providers/aws/services/backup/backup_service.py b/prowler/providers/aws/services/backup/backup_service.py index 712c75e611..9b46d2ea93 100644 --- a/prowler/providers/aws/services/backup/backup_service.py +++ b/prowler/providers/aws/services/backup/backup_service.py @@ -9,7 +9,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## Backup class Backup(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -19,8 +18,10 @@ class Backup(AWSService): self.backup_vault_arn_template = f"arn:{self.audited_partition}:backup:{self.region}:{self.audited_account}:backup-vault" self.backup_vaults = [] self.__threading_call__(self._list_backup_vaults) + self.__threading_call__(self._list_tags, self.backup_vaults) self.backup_plans = [] self.__threading_call__(self._list_backup_plans) + self.__threading_call__(self._list_tags, self.backup_plans) self.backup_report_plans = [] self.__threading_call__(self._list_backup_report_plans) self.protected_resources = {} @@ -167,6 +168,17 @@ class Backup(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource): + try: + tags = self.regional_clients[resource.region].list_tags( + ResourceArn=resource.arn + )["Tags"] + resource.tags = [tags] if tags else [] + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class BackupVault(BaseModel): arn: str @@ -177,6 +189,7 @@ class BackupVault(BaseModel): locked: bool min_retention_days: int = None max_retention_days: int = None + tags: Optional[list] class BackupPlan(BaseModel): @@ -187,6 +200,7 @@ class BackupPlan(BaseModel): version_id: str last_execution_date: Optional[datetime] advanced_settings: list + tags: Optional[list] class BackupReportPlan(BaseModel): diff --git a/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py b/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py index c9f909170d..d53d8e00a0 100644 --- a/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py +++ b/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py @@ -16,6 +16,7 @@ class backup_vaults_encrypted(Check): report.resource_arn = backup_vault.arn report.resource_id = backup_vault.name report.region = backup_vault.region + report.resource_tags = backup_vault.tags # if it is encrypted we only change the status and the status extended if backup_vault.encryption: report.status = "PASS" diff --git a/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py b/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py index c05e9c5ab1..8a59c70dbc 100644 --- a/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py +++ b/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py @@ -12,12 +12,14 @@ class backup_vaults_exist(Check): report.resource_arn = backup_client.backup_vault_arn_template report.resource_id = backup_client.audited_account report.region = backup_client.region + report.resource_tags = [] if backup_client.backup_vaults: report.status = "PASS" report.status_extended = f"At least one backup vault exists: {backup_client.backup_vaults[0].name}." report.resource_arn = backup_client.backup_vaults[0].arn report.resource_id = backup_client.backup_vaults[0].name report.region = backup_client.backup_vaults[0].region + report.resource_tags = backup_client.backup_vaults[0].tags findings.append(report) return findings diff --git a/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py b/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py index 7cae2c73fa..d1fb5afd1c 100644 --- a/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py +++ b/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py @@ -45,6 +45,7 @@ class Test_backup_plans_exist: == f"arn:aws:backup:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:backup-plan" ) assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_no_backup_plans_not_vaults(self): backup_client = mock.MagicMock @@ -88,6 +89,7 @@ class Test_backup_plans_exist: version_id="version_id", last_execution_date=datetime(2015, 1, 1), advanced_settings=[], + tags=[], ) ] with mock.patch( @@ -117,3 +119,4 @@ class Test_backup_plans_exist: == f"arn:aws:backup:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:plan:{backup_plan_id}" ) assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/backup/backup_service_test.py b/tests/providers/aws/services/backup/backup_service_test.py index 2da4cd505f..e0017ec423 100644 --- a/tests/providers/aws/services/backup/backup_service_test.py +++ b/tests/providers/aws/services/backup/backup_service_test.py @@ -2,10 +2,15 @@ from datetime import datetime from unittest.mock import patch import botocore +from boto3 import client from moto import mock_aws from prowler.providers.aws.services.backup.backup_service import Backup -from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) # Mocking Backup Calls make_api_call = botocore.client.BaseClient._make_api_call @@ -74,15 +79,14 @@ def mock_generate_regional_clients(provider, service): return {AWS_REGION_EU_WEST_1: regional_client} -@mock_aws -# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client -@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) -@patch( - "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", - new=mock_generate_regional_clients, -) -class Test_Backup_Service: +class TestBackupService: # Test Backup Client + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_get_client(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -91,18 +95,36 @@ class Test_Backup_Service: ) # Test Backup Session + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test__get_session__(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) access_analyzer = Backup(aws_provider) assert access_analyzer.session.__class__.__name__ == "Session" - # Test Backup Service + # Test Backup ServiceƧ + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test__get_service__(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) access_analyzer = Backup(aws_provider) assert access_analyzer.service == "backup" # Test Backup List Backup Vaults + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_backup_vaults(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -117,6 +139,12 @@ class Test_Backup_Service: assert backup.backup_vaults[0].max_retention_days == 2 # Test Backup List Backup Plans + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_backup_plans(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -130,6 +158,12 @@ class Test_Backup_Service: assert backup.backup_plans[0].advanced_settings == [] # Test Backup List Report Plans + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_backup_report_plans(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -144,6 +178,12 @@ class Test_Backup_Service: 2015, 1, 1 ) + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_protected_resources(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -155,3 +195,46 @@ class Test_Backup_Service: assert protected_resource.resource_type == "RDS" assert protected_resource.region == AWS_REGION_EU_WEST_1 assert protected_resource.last_backup_time == datetime(2015, 1, 1) + + @mock_aws + def test_list_tags(self): + backup_client = client("backup", region_name=AWS_REGION_EU_WEST_1) + + # Create necessary resources and tags + backup_vault = backup_client.create_backup_vault( + BackupVaultName="TestVault", + EncryptionKeyArn=f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/1234abcd-12ab-34cd-56ef-123456789012", + ) + + tags = {"TestKey": "TestValue"} + + backup_client.tag_resource( + ResourceArn=backup_vault["BackupVaultArn"], Tags=tags + ) + + # Create a backup plan + backup_plan = backup_client.create_backup_plan( + BackupPlan={ + "BackupPlanName": "TestPlan", + "Rules": [ + { + "RuleName": "TestRule", + "TargetBackupVaultName": "TestVault", # Match the vault name + "ScheduleExpression": "cron(0 12 * * ? *)", + } + ], + } + ) + + backup_client.tag_resource(ResourceArn=backup_plan["BackupPlanArn"], Tags=tags) + + # Test list_tags + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + backup = Backup(aws_provider) + + assert len(backup.backup_vaults) == 1 + assert len(backup.backup_vaults[0].tags) == 1 + assert backup.backup_vaults[0].tags[0]["TestKey"] == "TestValue" + assert len(backup.backup_plans) == 1 + assert len(backup.backup_plans[0].tags) == 1 + assert backup.backup_plans[0].tags[0]["TestKey"] == "TestValue" diff --git a/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py b/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py index bd37684728..fe0223c4eb 100644 --- a/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py +++ b/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py @@ -40,6 +40,7 @@ class Test_backup_vaults_encrypted: locked=True, min_retention_days=1, max_retention_days=2, + tags=[], ) ] @@ -67,6 +68,7 @@ class Test_backup_vaults_encrypted: assert result[0].resource_id == "MyBackupVault" assert result[0].resource_arn == backup_vault_arn assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_one_backup_vault_encrypted(self): backup_client = mock.MagicMock @@ -81,6 +83,7 @@ class Test_backup_vaults_encrypted: locked=True, min_retention_days=1, max_retention_days=2, + tags=[], ) ] @@ -108,3 +111,4 @@ class Test_backup_vaults_encrypted: assert result[0].resource_id == "MyBackupVault" assert result[0].resource_arn == backup_vault_arn assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py b/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py index 2b682c4be3..8c1c229359 100644 --- a/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py +++ b/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py @@ -42,6 +42,7 @@ class Test_backup_vaults_exist: == f"arn:aws:backup:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:backup-vault" ) assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_one_backup_vault(self): backup_client = mock.MagicMock @@ -64,6 +65,7 @@ class Test_backup_vaults_exist: locked=True, min_retention_days=1, max_retention_days=2, + tags=[], ) ] @@ -91,6 +93,7 @@ class Test_backup_vaults_exist: assert result[0].resource_id == "MyBackupVault" assert result[0].resource_arn == backup_vault_arn assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_access_denied(self): backup_client = mock.MagicMock