From facc0627d7dac866cd1c7b022578a1ffad72f3be Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:10:02 +0200 Subject: [PATCH] feat(azure): add new check `storage_geo_redundant_enabled` (#7980) Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 2 +- .../storage_geo_redundant_enabled/__init__.py | 0 ...torage_geo_redundant_enabled.metadata.json | 30 ++++ .../storage_geo_redundant_enabled.py | 41 ++++++ .../azure/services/storage/storage_service.py | 15 ++ .../storage_geo_redundant_enabled_test.py | 137 ++++++++++++++++++ .../services/storage/storage_service_test.py | 6 + 7 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/azure/services/storage/storage_geo_redundant_enabled/__init__.py create mode 100644 prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.metadata.json create mode 100644 prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py create mode 100644 tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 7db3191276..86f05ee925 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ## [v5.8.0] (Prowler UNRELEASED) ### Added - +- Add `storage_geo_redundant_enabled` check for Azure provider. [(#7980)](https://github.com/prowler-cloud/prowler/pull/7980) - Add `storage_cross_tenant_replication_disabled` check for Azure provider. [(#7977)](https://github.com/prowler-cloud/prowler/pull/7977) - CIS 1.11 compliance framework for Kubernetes [(#7790)](https://github.com/prowler-cloud/prowler/pull/7790) - Support `HTTPS_PROXY` and `K8S_SKIP_TLS_VERIFY` in Kubernetes [(#7720)](https://github.com/prowler-cloud/prowler/pull/7720) diff --git a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/__init__.py b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.metadata.json b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.metadata.json new file mode 100644 index 0000000000..35dc796130 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "storage_geo_redundant_enabled", + "CheckTitle": "Ensure geo-redundant storage (GRS) is enabled on critical Azure Storage Accounts", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "AzureStorageAccount", + "Description": "Geo-redundant storage (GRS) must be enabled on critical Azure Storage Accounts to ensure data durability and availability in the event of a regional outage. GRS replicates data within the primary region and asynchronously to a secondary region, offering enhanced resilience and supporting disaster recovery strategies.", + "Risk": "Without GRS, critical data may be lost or become unavailable during a regional outage, compromising data durability and disaster recovery efforts.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy", + "Remediation": { + "Code": { + "CLI": "az storage account update --name --resource-group --sku Standard_GRS", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/StorageAccounts/enable-geo-redundant-storage.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable geo-redundant storage (GRS) for critical Azure Storage Accounts to ensure data durability and availability across regional failures.", + "Url": "https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py new file mode 100644 index 0000000000..971b440b0e --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py @@ -0,0 +1,41 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.storage.storage_client import storage_client +from prowler.providers.azure.services.storage.storage_service import ReplicationSettings + + +class storage_geo_redundant_enabled(Check): + """Check if geo-redundant storage (GRS) is enabled on critical Azure Storage Accounts. + + Attributes: + metadata: Metadata associated with the check (inherited from Check). + """ + + def execute(self) -> Check_Report_Azure: + """Execute the check for geo-redundant storage (GRS). + + This method checks if geo-redundant storage (GRS) is enabled on critical Azure Storage Accounts. + + Returns: + Check_Report_Azure: A report containing the result of the check. + """ + findings = [] + for subscription, storage_accounts in storage_client.storage_accounts.items(): + for storage_account in storage_accounts: + report = Check_Report_Azure( + metadata=self.metadata(), resource=storage_account + ) + report.subscription = subscription + + if ( + storage_account.replication_settings + == ReplicationSettings.STANDARD_GRS + ): + report.status = "PASS" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has Geo-redundant storage (GRS) enabled." + else: + report.status = "FAIL" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have Geo-redundant storage (GRS) enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index f6df697ff1..026742631b 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from enum import Enum from typing import List, Optional from azure.mgmt.storage import StorageManagementClient @@ -34,6 +35,7 @@ class Storage(AzureService): key_expiration_period_in_days = ( storage_account.key_policy.key_expiration_period_in_days ) + replication_settings = ReplicationSettings(storage_account.sku.name) storage_accounts[subscription].append( Account( id=storage_account.id, @@ -68,6 +70,7 @@ class Storage(AzureService): ], key_expiration_period_in_days=key_expiration_period_in_days, location=storage_account.location, + replication_settings=replication_settings, allow_cross_tenant_replication=getattr( storage_account, "allow_cross_tenant_replication", True ), @@ -211,6 +214,17 @@ class PrivateEndpointConnection: type: str +class ReplicationSettings(Enum): + STANDARD_LRS = "Standard_LRS" + STANDARD_GRS = "Standard_GRS" + STANDARD_RAGRS = "Standard_RAGRS" + STANDARD_ZRS = "Standard_ZRS" + PREMIUM_LRS = "Premium_LRS" + PREMIUM_ZRS = "Premium_ZRS" + STANDARD_GZRS = "Standard_GZRS" + STANDARD_RAGZRS = "Standard_RAGZRS" + + @dataclass class Account: id: str @@ -225,6 +239,7 @@ class Account: private_endpoint_connections: List[PrivateEndpointConnection] key_expiration_period_in_days: str location: str + replication_settings: ReplicationSettings = ReplicationSettings.STANDARD_LRS allow_cross_tenant_replication: bool = True allow_shared_key_access: bool = True blob_properties: Optional[BlobProperties] = None diff --git a/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py b/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py new file mode 100644 index 0000000000..ce17cb80ed --- /dev/null +++ b/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py @@ -0,0 +1,137 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.storage.storage_service import ( + Account, + ReplicationSettings, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_storage_geo_redundant_enabled: + def test_no_storage_accounts(self): + storage_client = mock.MagicMock() + storage_client.storage_accounts = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 0 + + def test_storage_geo_redundant_enabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account GRS" + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name=None, + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=None, + encryption_type=None, + minimum_tls_version=None, + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=None, + replication_settings=ReplicationSettings.STANDARD_GRS, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage (GRS) enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_geo_redundant_disabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account LRS" + storage_client = mock.MagicMock() + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name=None, + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=None, + encryption_type=None, + minimum_tls_version=None, + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=None, + replication_settings=ReplicationSettings.STANDARD_LRS, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage (GRS) enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" diff --git a/tests/providers/azure/services/storage/storage_service_test.py b/tests/providers/azure/services/storage/storage_service_test.py index 0b42f88e33..9175ca9876 100644 --- a/tests/providers/azure/services/storage/storage_service_test.py +++ b/tests/providers/azure/services/storage/storage_service_test.py @@ -4,6 +4,7 @@ from prowler.providers.azure.services.storage.storage_service import ( Account, BlobProperties, FileShare, + ReplicationSettings, Storage, ) from tests.providers.azure.azure_fixtures import ( @@ -40,6 +41,7 @@ def mock_storage_get_storage_accounts(_): private_endpoint_connections=None, location="westeurope", blob_properties=blob_properties, + replication_settings=ReplicationSettings.STANDARD_LRS, allow_cross_tenant_replication=True, allow_shared_key_access=True, file_shares=file_shares, @@ -118,6 +120,10 @@ class Test_Storage_Service: default_service_version=None, container_delete_retention_policy=None, ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].replication_settings + == ReplicationSettings.STANDARD_LRS + ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ 0