From cbf2a28bac9133f10ca7ecd8422190cf5c1071b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Tue, 24 Jun 2025 16:04:02 +0200 Subject: [PATCH] feat(azure): add new check `keyvault_access_only_through_private_endpoints` (#8072) Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...ly_through_private_endpoints.metadata.json | 30 +++ ...t_access_only_through_private_endpoints.py | 37 ++++ .../services/keyvault/keyvault_service.py | 9 + ...ess_only_through_private_endpoints_test.py | 193 ++++++++++++++++++ 6 files changed, 270 insertions(+) create mode 100644 prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/__init__.py create mode 100644 prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json create mode 100644 prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py create mode 100644 tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index ec77a9392e..168efa0652 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -37,6 +37,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Azure Databricks service integration for Azure provider, including the `databricks_workspace_vnet_injection_enabled` check [(#8008)](https://github.com/prowler-cloud/prowler/pull/8008) - Azure Databricks check `databricks_workspace_cmk_encryption_enabled` to ensure workspaces use customer-managed keys (CMK) for encryption at rest [(#8017)](https://github.com/prowler-cloud/prowler/pull/8017) - Add `storage_account_default_to_entra_authorization_enabled` check for Azure provider. [(#7981)](https://github.com/prowler-cloud/prowler/pull/7981) +- `keyvault_ensure_public_network_access_disabled` check for Azure provider. [(#8072)](https://github.com/prowler-cloud/prowler/pull/8072) - New check `monitor_alert_service_health_exists` for Azure provider [(#8067)](https://github.com/prowler-cloud/prowler/pull/8067) - Replace `Domain.Read.All` with `Directory.Read.All` in Azure and M365 docs [(#8075)](https://github.com/prowler-cloud/prowler/pull/8075) diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/__init__.py b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json new file mode 100644 index 0000000000..2e1fe0e59c --- /dev/null +++ b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "keyvault_access_only_through_private_endpoints", + "CheckTitle": "Ensure that public network access when using private endpoint is disabled.", + "CheckType": [], + "ServiceName": "keyvault", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.KeyVault/vaults/{vault_name}", + "Severity": "high", + "ResourceType": "KeyVault", + "Description": "Checks if Key Vaults with private endpoints have public network access disabled.", + "Risk": "Allowing public network access to Key Vault when using private endpoint can expose sensitive data to unauthorized access.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security", + "Remediation": { + "Code": { + "CLI": "az keyvault update --resource-group --name --public-network-access disabled", + "NativeIaC": "{\n \"type\": \"Microsoft.KeyVault/vaults\",\n \"apiVersion\": \"2022-07-01\",\n \"properties\": {\n \"publicNetworkAccess\": \"disabled\"\n }\n}", + "Terraform": "resource \"azurerm_key_vault\" \"example\" {\n # ... other configuration ...\n\n public_network_access_enabled = false\n}", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/use-private-endpoints.html" + }, + "Recommendation": { + "Text": "Disable public network access for Key Vaults that use private endpoint to ensure network traffic only flows through the private endpoint.", + "Url": "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py new file mode 100644 index 0000000000..1a363f2d61 --- /dev/null +++ b/prowler/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints.py @@ -0,0 +1,37 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.keyvault.keyvault_client import keyvault_client + + +class keyvault_access_only_through_private_endpoints(Check): + """ + Ensure that Public Network Access when using Private Endpoint is disabled. + + This check evaluates whether Azure Key Vaults with private endpoints configured have + public network access disabled. Disabling public network access enhances security by + isolating the Key Vault from the public internet, thereby reducing its exposure. + + - PASS: The Key Vault has private endpoints and public network access is disabled. + - FAIL: The Key Vault has private endpoints and public network access is enabled. + """ + + def execute(self) -> Check_Report_Azure: + findings = [] + for subscription, key_vaults in keyvault_client.key_vaults.items(): + for keyvault in key_vaults: + if ( + keyvault.properties + and keyvault.properties.private_endpoint_connections + ): + report = Check_Report_Azure( + metadata=self.metadata(), resource=keyvault + ) + report.subscription = subscription + + if keyvault.properties.public_network_access_disabled: + report.status = "PASS" + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has public network access disabled and is using private endpoints." + else: + report.status = "FAIL" + report.status_extended = f"Keyvault {keyvault.name} from subscription {subscription} has public network access enabled while using private endpoints." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/keyvault/keyvault_service.py b/prowler/providers/azure/services/keyvault/keyvault_service.py index ccee275282..e63e799017 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_service.py +++ b/prowler/providers/azure/services/keyvault/keyvault_service.py @@ -70,6 +70,14 @@ class KeyVault(AzureService): "enable_purge_protection", False, ), + public_network_access_disabled=( + getattr( + keyvault_properties, + "public_network_access", + "Enabled", + ) + == "Disabled" + ), ), keys=keys, secrets=secrets, @@ -247,6 +255,7 @@ class VaultProperties: private_endpoint_connections: List[PrivateEndpointConnection] enable_soft_delete: bool enable_purge_protection: bool + public_network_access_disabled: bool = False @dataclass diff --git a/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py b/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py new file mode 100644 index 0000000000..793b3b4912 --- /dev/null +++ b/tests/providers/azure/services/keyvault/keyvault_access_only_through_private_endpoints/keyvault_access_only_through_private_endpoints_test.py @@ -0,0 +1,193 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_keyvault_access_only_through_private_endpoints: + def test_no_key_vaults(self): + keyvault_client = mock.MagicMock + keyvault_client.key_vaults = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 0 + + def test_key_vaults_no_private_endpoints(self): + keyvault_client = mock.MagicMock + keyvault_name = "Keyvault Name" + keyvault_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVaultInfo, + VaultProperties, + ) + + keyvault_client.key_vaults = { + AZURE_SUBSCRIPTION_ID: [ + KeyVaultInfo( + id=keyvault_id, + name=keyvault_name, + location="westeurope", + resource_group="resource_group", + properties=VaultProperties( + tenant_id="tenantid", + enable_rbac_authorization=False, + private_endpoint_connections=[], + enable_soft_delete=True, + enable_purge_protection=True, + public_network_access_disabled=False, + ), + ) + ] + } + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 0 + + def test_key_vaults_with_private_endpoints_public_access_enabled(self): + keyvault_client = mock.MagicMock + keyvault_name = "Keyvault Name" + keyvault_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVaultInfo, + PrivateEndpointConnection, + VaultProperties, + ) + + keyvault_client.key_vaults = { + AZURE_SUBSCRIPTION_ID: [ + KeyVaultInfo( + id=keyvault_id, + name=keyvault_name, + location="westeurope", + resource_group="resource_group", + properties=VaultProperties( + tenant_id="tenantid", + enable_rbac_authorization=True, + private_endpoint_connections=[ + PrivateEndpointConnection(id="id1") + ], + enable_soft_delete=True, + enable_purge_protection=True, + public_network_access_disabled=False, + ), + ) + ] + } + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has public network access enabled while using private endpoints." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == keyvault_name + assert result[0].resource_id == keyvault_id + assert result[0].location == "westeurope" + + def test_key_vaults_with_private_endpoints_public_access_disabled(self): + keyvault_client = mock.MagicMock + keyvault_name = "Keyvault Name" + keyvault_id = str(uuid4()) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints.keyvault_client", + new=keyvault_client, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_access_only_through_private_endpoints.keyvault_access_only_through_private_endpoints import ( + keyvault_access_only_through_private_endpoints, + ) + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVaultInfo, + PrivateEndpointConnection, + VaultProperties, + ) + + keyvault_client.key_vaults = { + AZURE_SUBSCRIPTION_ID: [ + KeyVaultInfo( + id=keyvault_id, + name=keyvault_name, + location="westeurope", + resource_group="resource_group", + properties=VaultProperties( + tenant_id="tenantid", + enable_rbac_authorization=True, + private_endpoint_connections=[ + PrivateEndpointConnection(id="id1") + ], + enable_soft_delete=True, + enable_purge_protection=True, + public_network_access_disabled=True, + ), + ) + ] + } + + check = keyvault_access_only_through_private_endpoints() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Keyvault {keyvault_name} from subscription {AZURE_SUBSCRIPTION_ID} has public network access disabled and is using private endpoints." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == keyvault_name + assert result[0].resource_id == keyvault_id + assert result[0].location == "westeurope"