feat(azure): add storage_account_public_network_access_disabled and fix CIS storage mapping (#11334)

This commit is contained in:
Hugo Pereira Brito
2026-05-25 18:17:41 +02:00
committed by GitHub
parent 546eb2d85a
commit 6ca8e726f7
13 changed files with 247 additions and 8 deletions
+8
View File
@@ -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
+1 -1
View File
@@ -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": [
{
+1 -1
View File
@@ -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": [
{
+1 -1
View File
@@ -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": [
{
+1 -1
View File
@@ -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": [
{
@@ -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": [
{
@@ -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 <storage-account> --resource-group <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: '<example_resource_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\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_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."
}
@@ -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
@@ -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."
}
@@ -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
@@ -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."
)
@@ -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