From cc8f6131e6eb9ac828d2ccea9ba9c2baa33284fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Wed, 18 Jun 2025 09:46:38 +0200 Subject: [PATCH] feat(azure): add new check `storage_blob_versioning_is_enabled ` (#7927) Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 2 + .../__init__.py | 0 ...e_blob_versioning_is_enabled.metadata.json | 30 +++ .../storage_blob_versioning_is_enabled.py | 24 ++ .../azure/services/storage/storage_service.py | 5 + ...storage_blob_versioning_is_enabled_test.py | 207 ++++++++++++++++++ 6 files changed, 268 insertions(+) create mode 100644 prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/__init__.py create mode 100644 prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.metadata.json create mode 100644 prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py create mode 100644 tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 3ff7b66c59..0d078ff2a6 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -27,12 +27,14 @@ All notable changes to the **Prowler SDK** are documented in this file. - `storage_ensure_file_shares_soft_delete_is_enabled` check for Azure provider [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966) - Make `validate_mutelist` method static inside `Mutelist` class [(#7811)](https://github.com/prowler-cloud/prowler/pull/7811) - Avoid bypassing IAM check using wildcards [(#7708)](https://github.com/prowler-cloud/prowler/pull/7708) +- `storage_blob_versioning_is_enabled` new check for Azure provider [(#7927)](https://github.com/prowler-cloud/prowler/pull/7927) - New method to authenticate in AppInsights in check `app_function_application_insights_enabled` [(#7763)](https://github.com/prowler-cloud/prowler/pull/7763) - ISO 27001 2022 for M365 provider. [(#7985)](https://github.com/prowler-cloud/prowler/pull/7985) - `codebuild_project_uses_allowed_github_organizations` check for AWS provider [(#7595)](https://github.com/prowler-cloud/prowler/pull/7595) - IaC provider [(#7852)](https://github.com/prowler-cloud/prowler/pull/7852) --- + ## [v5.7.5] (Prowler UNRELEASED) ### Fixed diff --git a/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/__init__.py b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.metadata.json b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.metadata.json new file mode 100644 index 0000000000..d4f91b34f3 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "storage_blob_versioning_is_enabled", + "CheckTitle": "Ensure Blob Versioning is Enabled on Azure Blob Storage Accounts", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AzureStorageAccount", + "Description": "Ensure that blob versioning is enabled on Azure Blob Storage accounts to automatically retain previous versions of objects.", + "Risk": "Without blob versioning, accidental or malicious changes to blobs cannot be easily recovered, leading to potential data loss.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-enable", + "Remediation": { + "Code": { + "CLI": "az storage account blob-service-properties update --resource-group --account-name --enable-versioning true", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/StorageAccounts/enable-versioning-for-blobs.html", + "Terraform": "resource \"azurerm_storage_account\" \"example\" {\n name = \"examplestorageacct\"\n resource_group_name = azurerm_resource_group.example.name\n location = azurerm_resource_group.example.location\n account_tier = \"Standard\"\n account_replication_type = \"LRS\"\n\n blob_properties {\n versioning_enabled = true\n }\n}\n" + }, + "Recommendation": { + "Text": "Enable blob versioning for all Azure Storage accounts that store critical or sensitive data.", + "Url": "https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-enable" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py new file mode 100644 index 0000000000..cf55d6f830 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.storage.storage_client import storage_client + + +class storage_blob_versioning_is_enabled(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + for subscription, storage_accounts in storage_client.storage_accounts.items(): + for storage_account in storage_accounts: + if storage_account.blob_properties: + report = Check_Report_Azure( + metadata=self.metadata(), resource=storage_account + ) + report.subscription = subscription + if getattr( + storage_account.blob_properties, "versioning_enabled", False + ): + report.status = "PASS" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has blob versioning enabled." + else: + report.status = "FAIL" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have blob versioning 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 02533b503e..dd49943400 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -92,6 +92,9 @@ class Storage(AzureService): container_delete_retention_policy = getattr( properties, "container_delete_retention_policy", None ) + versioning_enabled = getattr( + properties, "is_versioning_enabled", False + ) account.blob_properties = BlobProperties( id=properties.id, name=properties.name, @@ -107,6 +110,7 @@ class Storage(AzureService): container_delete_retention_policy, "days", 0 ), ), + versioning_enabled=versioning_enabled, ) except Exception as error: if ( @@ -188,6 +192,7 @@ class BlobProperties: type: str default_service_version: str container_delete_retention_policy: DeleteRetentionPolicy + versioning_enabled: bool = False @dataclass diff --git a/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py b/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py new file mode 100644 index 0000000000..ae0e89716a --- /dev/null +++ b/tests/providers/azure/services/storage/storage_blob_versioning_is_enabled/storage_blob_versioning_is_enabled_test.py @@ -0,0 +1,207 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_storage_blob_versioning_is_enabled: + def test_storage_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_blob_versioning_is_enabled.storage_blob_versioning_is_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_blob_versioning_is_enabled.storage_blob_versioning_is_enabled import ( + storage_blob_versioning_is_enabled, + ) + + check = storage_blob_versioning_is_enabled() + result = check.execute() + assert len(result) == 0 + + def test_storage_no_blob_properties(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock + storage_account_blob_properties = None + 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_blob_versioning_is_enabled.storage_blob_versioning_is_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_service import Account + + 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=None, + network_rule_set=None, + encryption_type="None", + minimum_tls_version=None, + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=None, + blob_properties=storage_account_blob_properties, + ) + ] + } + from prowler.providers.azure.services.storage.storage_blob_versioning_is_enabled.storage_blob_versioning_is_enabled import ( + storage_blob_versioning_is_enabled, + ) + + check = storage_blob_versioning_is_enabled() + result = check.execute() + assert len(result) == 0 + + def test_storage_blob_versioning_is_enabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock + 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_blob_versioning_is_enabled.storage_blob_versioning_is_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_service import ( + Account, + BlobProperties, + DeleteRetentionPolicy, + ) + + storage_account_blob_properties = BlobProperties( + id=None, + name=None, + type=None, + default_service_version=None, + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=False, days=0 + ), + versioning_enabled=True, + ) + 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=None, + network_rule_set=None, + encryption_type="None", + minimum_tls_version=None, + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=None, + blob_properties=storage_account_blob_properties, + ) + ] + } + from prowler.providers.azure.services.storage.storage_blob_versioning_is_enabled.storage_blob_versioning_is_enabled import ( + storage_blob_versioning_is_enabled, + ) + + check = storage_blob_versioning_is_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 blob versioning 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_blob_versioning_is_disabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account" + storage_client = mock.MagicMock + 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_blob_versioning_is_enabled.storage_blob_versioning_is_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_service import ( + Account, + BlobProperties, + DeleteRetentionPolicy, + ) + + storage_account_blob_properties = BlobProperties( + id=None, + name=None, + type=None, + default_service_version=None, + container_delete_retention_policy=DeleteRetentionPolicy( + enabled=False, days=0 + ), + versioning_enabled=False, + ) + 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=None, + network_rule_set=None, + encryption_type="None", + minimum_tls_version=None, + key_expiration_period_in_days=None, + location="westeurope", + private_endpoint_connections=None, + blob_properties=storage_account_blob_properties, + ) + ] + } + from prowler.providers.azure.services.storage.storage_blob_versioning_is_enabled.storage_blob_versioning_is_enabled import ( + storage_blob_versioning_is_enabled, + ) + + check = storage_blob_versioning_is_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 blob versioning 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"