diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index d4ed312987..a380b07115 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031) - `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032) - `cosmosdb_account_minimum_tls_version` check for Azure provider, verifying Cosmos DB accounts enforce TLS 1.2 or higher for client connections [(#11033)](https://github.com/prowler-cloud/prowler/pull/11033) +- `cosmosdb_account_public_network_access_disabled` check for Azure provider, verifying Cosmos DB accounts have public network access disabled so connectivity is restricted to private endpoints or VNet service endpoints [(#11034)](https://github.com/prowler-cloud/prowler/pull/11034) - `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027) - Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602) - TLS certificate verification in the `codepipeline_project_repo_private` check, which previously used an unverified SSL context, leaving the repository-visibility probe open to MITM tampering [(#11603)](https://github.com/prowler-cloud/prowler/pull/11603) diff --git a/prowler/compliance/azure/iso27001_2022_azure.json b/prowler/compliance/azure/iso27001_2022_azure.json index ef9dc6ec23..d137938fd7 100644 --- a/prowler/compliance/azure/iso27001_2022_azure.json +++ b/prowler/compliance/azure/iso27001_2022_azure.json @@ -1410,6 +1410,7 @@ "aks_network_policy_enabled", "containerregistry_not_publicly_accessible", "cosmosdb_account_firewall_use_selected_networks", + "cosmosdb_account_public_network_access_disabled", "network_bastion_host_exists", "network_flow_log_captured_sent", "network_flow_log_more_than_90_days", diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/__init__.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled.metadata.json b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled.metadata.json new file mode 100644 index 0000000000..2d8a61a54e --- /dev/null +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "azure", + "CheckID": "cosmosdb_account_public_network_access_disabled", + "CheckTitle": "Cosmos DB account has public network access disabled", + "CheckType": [], + "ServiceName": "cosmosdb", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.documentdb/databaseaccounts", + "ResourceGroup": "database", + "Description": "**Azure Cosmos DB accounts** are evaluated for **public network access**. Disabling public network access ensures the account is only reachable through **private endpoints** or **VNet service endpoints**, reducing the attack surface and preventing direct exposure of the data plane to the internet.", + "Risk": "Allowing **public network access** exposes the Cosmos DB data plane to the internet. Combined with leaked **connection strings**, weak **firewall rules**, or compromised **AAD tokens**, this enables **unauthorized data access**, **enumeration**, **brute force**, and **data exfiltration** from any source on the internet.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-configure-private-endpoints", + "https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-configure-firewall", + "https://learn.microsoft.com/en-us/azure/templates/microsoft.documentdb/databaseaccounts" + ], + "Remediation": { + "Code": { + "CLI": "az cosmosdb update --name --resource-group --public-network-access Disabled", + "NativeIaC": "```bicep\n// Bicep: Disable public network access on a Cosmos DB account\nresource account 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = {\n name: ''\n location: resourceGroup().location\n kind: 'GlobalDocumentDB'\n properties: {\n databaseAccountOfferType: 'Standard'\n locations: [{ locationName: resourceGroup().location }]\n publicNetworkAccess: 'Disabled' // Critical: Blocks all public-internet traffic to the data plane\n }\n}\n```", + "Other": "1. Sign in to the Azure portal and open your Cosmos DB account\n2. In the left menu, select Networking\n3. Under Public network access, select Disabled\n4. Configure private endpoints or VNet service endpoints before saving so clients retain connectivity\n5. Click Save", + "Terraform": "```hcl\n# Terraform: Disable public network access on a Cosmos DB account\nresource \"azurerm_cosmosdb_account\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n offer_type = \"Standard\"\n kind = \"GlobalDocumentDB\"\n\n geo_location {\n location = \"\"\n failover_priority = 0\n }\n\n public_network_access_enabled = false # Critical: Blocks all public-internet traffic to the data plane\n}\n```" + }, + "Recommendation": { + "Text": "Disable **public network access** on Cosmos DB accounts and require connectivity via **private endpoints** or **VNet service endpoints**. Before enforcement, validate that all application workloads have private-network connectivity, and pair this control with **AAD/RBAC authentication**, **TLS 1.2+**, and **firewall rules** restricted to the minimum trusted ranges to uphold **defense in depth**.", + "Url": "https://hub.prowler.com/check/cosmosdb_account_public_network_access_disabled" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled.py new file mode 100644 index 0000000000..ec2d29728d --- /dev/null +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled.py @@ -0,0 +1,31 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.cosmosdb.cosmosdb_client import cosmosdb_client + + +class cosmosdb_account_public_network_access_disabled(Check): + """Ensure that Cosmos DB accounts have public network access disabled.""" + + def execute(self) -> Check_Report_Azure: + """Execute the Cosmos DB public network access check. + + Iterates over every Cosmos DB account fetched by the service and reports + PASS when `publicNetworkAccess` is `Disabled` or `SecuredByPerimeter` + (Microsoft Network Security Perimeter), FAIL otherwise (including when + the property is missing or set to `Enabled`). + + Returns: + A list of Check_Report_Azure with one report per Cosmos DB account. + """ + findings = [] + for subscription, accounts in cosmosdb_client.accounts.items(): + for account in accounts: + report = Check_Report_Azure(metadata=self.metadata(), resource=account) + report.subscription = subscription + report.status = "FAIL" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} does not have public network access disabled (current value: {account.public_network_access!r})." + if account.public_network_access in {"Disabled", "SecuredByPerimeter"}: + report.status = "PASS" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} has public network access disabled." + findings.append(report) + + return findings diff --git a/tests/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled_test.py b/tests/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled_test.py new file mode 100644 index 0000000000..60297d6138 --- /dev/null +++ b/tests/providers/azure/services/cosmosdb/cosmosdb_account_public_network_access_disabled/cosmosdb_account_public_network_access_disabled_test.py @@ -0,0 +1,210 @@ +from unittest import mock + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_cosmosdb_account_public_network_access_disabled: + def test_no_subscriptions(self): + cosmosdb_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.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled import ( + cosmosdb_account_public_network_access_disabled, + ) + + cosmosdb_client.accounts = {} + + check = cosmosdb_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 0 + + def test_pass_disabled(self): + cosmosdb_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.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled import ( + cosmosdb_account_public_network_access_disabled, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + public_network_access="Disabled", + ) + ] + } + + check = cosmosdb_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + f"CosmosDB account test-account from subscription " + f"{AZURE_SUBSCRIPTION_ID} has public network access disabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + + def test_pass_secured_by_perimeter(self): + cosmosdb_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.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled import ( + cosmosdb_account_public_network_access_disabled, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + public_network_access="SecuredByPerimeter", + ) + ] + } + + check = cosmosdb_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + + def test_fail_enabled(self): + cosmosdb_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.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled import ( + cosmosdb_account_public_network_access_disabled, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + public_network_access="Enabled", + ) + ] + } + + check = cosmosdb_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + f"CosmosDB account test-account from subscription " + f"{AZURE_SUBSCRIPTION_ID} does not have public network access " + f"disabled (current value: 'Enabled')." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + + def test_fail_no_public_network_access(self): + cosmosdb_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.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_public_network_access_disabled.cosmosdb_account_public_network_access_disabled import ( + cosmosdb_account_public_network_access_disabled, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + public_network_access=None, + ) + ] + } + + check = cosmosdb_account_public_network_access_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL"