diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 5c5ba4302d..6cea3faf30 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler SDK** are documented in this file. +## [5.29.0] (Prowler UNRELEASED) + +### 🚀 Added + +- `storage_account_public_network_access_disabled` check for Azure provider and remapped the Azure CIS "Public Network Access is Disabled" requirements to it [(#11334)](https://github.com/prowler-cloud/prowler/pull/11334) + +--- + ## [5.28.0] (Prowler v5.28.0) ### 🚀 Added diff --git a/prowler/compliance/azure/cis_2.1_azure.json b/prowler/compliance/azure/cis_2.1_azure.json index 805c81bbd8..a663e5be18 100644 --- a/prowler/compliance/azure/cis_2.1_azure.json +++ b/prowler/compliance/azure/cis_2.1_azure.json @@ -1383,7 +1383,7 @@ "Id": "3.7", "Description": "Ensure that 'Public Network Access' is `Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_3.0_azure.json b/prowler/compliance/azure/cis_3.0_azure.json index 47344c19ee..107c495d05 100644 --- a/prowler/compliance/azure/cis_3.0_azure.json +++ b/prowler/compliance/azure/cis_3.0_azure.json @@ -1651,7 +1651,7 @@ "Id": "4.6", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_4.0_azure.json b/prowler/compliance/azure/cis_4.0_azure.json index ba15a661ef..c075ff4047 100644 --- a/prowler/compliance/azure/cis_4.0_azure.json +++ b/prowler/compliance/azure/cis_4.0_azure.json @@ -3021,7 +3021,7 @@ "Id": "10.3.2.2", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/cis_5.0_azure.json b/prowler/compliance/azure/cis_5.0_azure.json index 49b18f9326..21785d92b6 100644 --- a/prowler/compliance/azure/cis_5.0_azure.json +++ b/prowler/compliance/azure/cis_5.0_azure.json @@ -3182,7 +3182,7 @@ "Id": "9.3.2.2", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/compliance/azure/prowler_threatscore_azure.json b/prowler/compliance/azure/prowler_threatscore_azure.json index a65a7f60e8..a030bb845b 100644 --- a/prowler/compliance/azure/prowler_threatscore_azure.json +++ b/prowler/compliance/azure/prowler_threatscore_azure.json @@ -459,7 +459,7 @@ "Id": "2.2.6", "Description": "Ensure that 'Public Network Access' is 'Disabled' for storage accounts", "Checks": [ - "storage_blob_public_access_level_is_disabled" + "storage_account_public_network_access_disabled" ], "Attributes": [ { diff --git a/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/__init__.py b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.metadata.json b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.metadata.json new file mode 100644 index 0000000000..142b1082cf --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "azure", + "CheckID": "storage_account_public_network_access_disabled", + "CheckTitle": "Storage account has 'Public Network Access' disabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "microsoft.storage/storageaccounts", + "ResourceGroup": "storage", + "Description": "**Azure Storage accounts** with **public network access** disabled cannot be reached from public networks. Setting `publicNetworkAccess` to `Disabled` overrides the public access settings of individual containers and forces access through private endpoints or trusted services. This is independent from the 'Allow Blob Anonymous Access' setting.", + "Risk": "Leaving **public network access** enabled exposes the storage account endpoints to the **public Internet**, widening the attack surface and undermining **defense in depth**.\n\nThis increases the risk of **unauthorized access**, **data exfiltration**, and reconnaissance against the account, especially when combined with weak network rules or overly permissive access policies.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security?tabs=azure-portal#change-the-default-network-access-rule", + "https://learn.microsoft.com/en-us/azure/storage/common/storage-network-security-set-default-access" + ], + "Remediation": { + "Code": { + "CLI": "az storage account update --name --resource-group --public-network-access Disabled", + "NativeIaC": "```bicep\n// Storage account with public network access disabled\nresource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {\n name: ''\n location: resourceGroup().location\n kind: 'StorageV2'\n sku: { name: 'Standard_LRS' }\n properties: {\n publicNetworkAccess: 'Disabled' // Critical: disables public network access to the account\n }\n}\n```", + "Other": "1. In the Azure portal, go to Storage accounts and select the target account\n2. Under Security + networking, click Networking\n3. Set Public network access to Disabled\n4. Click Save", + "Terraform": "```hcl\nresource \"azurerm_storage_account\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n account_tier = \"Standard\"\n account_replication_type = \"LRS\"\n public_network_access_enabled = false # Critical: disables public network access\n}\n```" + }, + "Recommendation": { + "Text": "Disable **public network access** on the storage account and reach it through **private endpoints** or trusted Azure services only. Combine this with **least privilege** RBAC, short-lived `SAS` tokens, and network restrictions. Validate client connectivity before disabling public access in production.", + "Url": "https://hub.prowler.com/check/storage_account_public_network_access_disabled" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check evaluates the storage account publicNetworkAccess property. It is independent from the 'Allow Blob Anonymous Access' setting evaluated by storage_blob_public_access_level_is_disabled." +} diff --git a/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.py b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.py new file mode 100644 index 0000000000..3db62c5256 --- /dev/null +++ b/prowler/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled.py @@ -0,0 +1,38 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.storage.storage_client import storage_client + + +class storage_account_public_network_access_disabled(Check): + """ + Ensure that 'Public Network Access' is 'Disabled' for storage accounts. + + This check evaluates the storage account's publicNetworkAccess property, which controls + whether the account is reachable from public networks. It is independent from the + 'Allow Blob Anonymous Access' setting (covered by + storage_blob_public_access_level_is_disabled). + - PASS: The storage account has public network access disabled. + - FAIL: The storage account has public network access enabled (or unset, which Azure treats as enabled). + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + for subscription, storage_accounts in storage_client.storage_accounts.items(): + subscription_name = storage_client.subscriptions.get( + subscription, subscription + ) + for storage_account in storage_accounts: + report = Check_Report_Azure( + metadata=self.metadata(), resource=storage_account + ) + report.subscription = subscription + + if storage_account.public_network_access == "Disabled": + report.status = "PASS" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has public network access disabled." + else: + report.status = "FAIL" + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription_name} ({subscription}) has public network access enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json b/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json index 079376899b..1cf3e35569 100644 --- a/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json +++ b/prowler/providers/azure/services/storage/storage_blob_public_access_level_is_disabled/storage_blob_public_access_level_is_disabled.metadata.json @@ -1,7 +1,7 @@ { "Provider": "azure", "CheckID": "storage_blob_public_access_level_is_disabled", - "CheckTitle": "Storage account has 'Allow blob public access' disabled", + "CheckTitle": "Storage account has 'Allow Blob Anonymous Access' disabled", "CheckType": [], "ServiceName": "storage", "SubServiceName": "", @@ -9,7 +9,7 @@ "Severity": "high", "ResourceType": "microsoft.storage/storageaccounts", "ResourceGroup": "storage", - "Description": "**Azure Storage accounts** with **blob public access** disabled prevent containers or blobs from being set to a public access level. Setting `allow blob public access` to `false` enforces no anonymous reads across the account.", + "Description": "**Azure Storage accounts** with **blob anonymous (public) access** disabled prevent containers or blobs from being set to a public access level. Setting `allowBlobPublicAccess` to `false` enforces no anonymous reads across the account. This is independent from the account's 'Public Network Access' setting, which is evaluated by storage_account_public_network_access_disabled.", "Risk": "Allowing public access permits unauthenticated users to read blob data or enumerate container contents when any container is made public, compromising confidentiality.\n\nExposed objects can be scraped at scale, enabling data exfiltration and intelligence gathering without audit attribution.", "RelatedUrl": "", "AdditionalURLs": [ @@ -33,5 +33,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "This check evaluates the 'Allow Blob Anonymous Access' (allowBlobPublicAccess) setting. The account's 'Public Network Access' (publicNetworkAccess) setting is evaluated by storage_account_public_network_access_disabled." } diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index 6ec88248bf..74b8b3da30 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -42,6 +42,9 @@ class Storage(AzureService): enable_https_traffic_only=storage_account.enable_https_traffic_only, infrastructure_encryption=storage_account.encryption.require_infrastructure_encryption, allow_blob_public_access=storage_account.allow_blob_public_access, + public_network_access=getattr( + storage_account, "public_network_access", None + ), network_rule_set=NetworkRuleSet( bypass=getattr( storage_account.network_rule_set, @@ -301,6 +304,7 @@ class Account(BaseModel): enable_https_traffic_only: bool infrastructure_encryption: Optional[bool] = None allow_blob_public_access: bool + public_network_access: Optional[str] = None network_rule_set: NetworkRuleSet encryption_type: str minimum_tls_version: str diff --git a/tests/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled_test.py b/tests/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled_test.py new file mode 100644 index 0000000000..e66338f231 --- /dev/null +++ b/tests/providers/azure/services/storage/storage_account_public_network_access_disabled/storage_account_public_network_access_disabled_test.py @@ -0,0 +1,147 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.storage.storage_service import ( + Account, + NetworkRuleSet, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, + AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, + set_mocked_azure_provider, +) + + +class Test_storage_account_public_network_access_disabled: + def test_no_storage_accounts(self): + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + 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_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 0 + + def _account(self, name, public_network_access): + return Account( + id=str(uuid4()), + name=name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + public_network_access=public_network_access, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + ) + + def test_public_network_access_disabled(self): + storage_account_name = "Test Storage Account" + account = self._account(storage_account_name, "Disabled") + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {AZURE_SUBSCRIPTION_ID: [account]} + + 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_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + 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_DISPLAY} has public network access disabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == account.id + + def test_public_network_access_enabled(self): + storage_account_name = "Test Storage Account" + account = self._account(storage_account_name, "Enabled") + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {AZURE_SUBSCRIPTION_ID: [account]} + + 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_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + 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_DISPLAY} has public network access enabled." + ) + + def test_public_network_access_unset_fails(self): + storage_account_name = "Test Storage Account" + account = self._account(storage_account_name, None) + storage_client = mock.MagicMock() + storage_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + storage_client.storage_accounts = {AZURE_SUBSCRIPTION_ID: [account]} + + 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_account_public_network_access_disabled.storage_account_public_network_access_disabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_account_public_network_access_disabled.storage_account_public_network_access_disabled import ( + storage_account_public_network_access_disabled, + ) + + check = storage_account_public_network_access_disabled() + 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_DISPLAY} has public network access enabled." + ) diff --git a/tests/providers/azure/services/storage/storage_service_test.py b/tests/providers/azure/services/storage/storage_service_test.py index 3d75fa5000..67fba33877 100644 --- a/tests/providers/azure/services/storage/storage_service_test.py +++ b/tests/providers/azure/services/storage/storage_service_test.py @@ -42,6 +42,7 @@ def mock_storage_get_storage_accounts(_): enable_https_traffic_only=False, infrastructure_encryption=False, allow_blob_public_access=False, + public_network_access="Disabled", network_rule_set=NetworkRuleSet( bypass="AzureServices", default_action="Allow" ), @@ -97,6 +98,10 @@ class Test_Storage_Service: storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].allow_blob_public_access is False ) + assert ( + storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].public_network_access + == "Disabled" + ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].network_rule_set is not None