feat(azure): add databricks_workspace_no_public_ip_enabled check (#11036)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
s1ns3nz0
2026-06-18 16:06:25 +09:00
committed by GitHub
parent c0ae8b9739
commit 3c68a121e5
8 changed files with 267 additions and 0 deletions
+1
View File
@@ -18,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `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)
- `databricks_workspace_public_network_access_disabled` check for Azure provider, verifying Databricks workspaces have public network access disabled so connectivity is restricted to Azure Private Link private endpoints [(#11035)](https://github.com/prowler-cloud/prowler/pull/11035)
- `databricks_workspace_no_public_ip_enabled` check for Azure provider, verifying Databricks workspaces use secure cluster connectivity (no public IP) so compute nodes are not assigned public IP addresses [(#11036)](https://github.com/prowler-cloud/prowler/pull/11036)
- `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027)
- Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398)
- 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)
@@ -1411,6 +1411,7 @@
"containerregistry_not_publicly_accessible",
"cosmosdb_account_firewall_use_selected_networks",
"cosmosdb_account_public_network_access_disabled",
"databricks_workspace_no_public_ip_enabled",
"databricks_workspace_public_network_access_disabled",
"network_bastion_host_exists",
"network_flow_log_captured_sent",
@@ -62,6 +62,9 @@ class Databricks(AzureService):
else:
managed_disk_encryption = None
enable_no_public_ip = getattr(
workspace_parameters, "enable_no_public_ip", None
)
workspaces[subscription][workspace.id] = DatabricksWorkspace(
id=workspace.id,
name=workspace.name,
@@ -69,6 +72,11 @@ class Databricks(AzureService):
public_network_access=getattr(
workspace, "public_network_access", None
),
no_public_ip_enabled=(
getattr(enable_no_public_ip, "value", None)
if enable_no_public_ip
else None
),
custom_managed_vnet_id=(
getattr(
workspace_parameters, "custom_virtual_network_id", None
@@ -111,6 +119,7 @@ class DatabricksWorkspace(BaseModel):
name: The name of the workspace.
location: The Azure region where the workspace is deployed.
public_network_access: Whether public network access is "Enabled" or "Disabled", if configured.
no_public_ip_enabled: Whether secure cluster connectivity (no public IP) is enabled. None when the workspace does not expose this classic-compute setting (e.g. serverless workspaces).
custom_managed_vnet_id: The ID of the custom managed virtual network, if configured.
managed_disk_encryption: The encryption settings for the workspace's managed disks.
"""
@@ -119,5 +128,6 @@ class DatabricksWorkspace(BaseModel):
name: str
location: str
public_network_access: Optional[str] = None
no_public_ip_enabled: Optional[bool] = None
custom_managed_vnet_id: Optional[str] = None
managed_disk_encryption: Optional[ManagedDiskEncryption] = None
@@ -0,0 +1,38 @@
{
"Provider": "azure",
"CheckID": "databricks_workspace_no_public_ip_enabled",
"CheckTitle": "Databricks workspace has secure cluster connectivity (no public IP)",
"CheckType": [],
"ServiceName": "databricks",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "microsoft.databricks/workspaces",
"ResourceGroup": "ai_ml",
"Description": "**Azure Databricks workspaces** are evaluated for **secure cluster connectivity** (No Public IP / NPIP). When enabled, compute (cluster) nodes are deployed without public IP addresses and communicate with the control plane through a secure relay, reducing the workspace's exposure to the internet.",
"Risk": "Without **secure cluster connectivity**, Databricks compute nodes are assigned **public IP addresses** and are directly reachable from the internet. This enables **port scanning**, **lateral movement** from a compromised node, and **data exfiltration** that bypasses private-network controls.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/secure-cluster-connectivity",
"https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/vnet-inject",
"https://learn.microsoft.com/en-us/azure/templates/microsoft.databricks/workspaces"
],
"Remediation": {
"Code": {
"CLI": "az databricks workspace create --name <workspace_name> --resource-group <resource_group_name> --location <region> --sku premium --enable-no-public-ip",
"NativeIaC": "```bicep\n// Bicep: Deploy a Databricks workspace with secure cluster connectivity (No Public IP)\nresource workspace 'Microsoft.Databricks/workspaces@2023-02-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n sku: {\n name: 'premium'\n }\n properties: {\n managedResourceGroupId: '/subscriptions/<example_resource_id>/resourceGroups/<example_resource_name>'\n parameters: {\n enableNoPublicIp: {\n value: true // CRITICAL: Deploys cluster nodes without public IP addresses\n }\n }\n }\n}\n```",
"Other": "1. Sign in to the Azure portal and start creating a new Azure Databricks workspace (No Public IP must be set at creation; it cannot be enabled on an existing workspace)\n2. On the Networking tab, set Deploy Azure Databricks workspace with Secure Cluster Connectivity (No Public IP) to Yes\n3. Complete the VNet injection configuration if required\n4. Review + create the workspace\n5. Migrate workloads to the new workspace and decommission the old one",
"Terraform": "```hcl\n# Terraform: Databricks workspace with secure cluster connectivity (No Public IP)\nresource \"azurerm_databricks_workspace\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<region>\"\n sku = \"premium\"\n\n custom_parameters {\n no_public_ip = true # CRITICAL: Deploys cluster nodes without public IP addresses\n }\n}\n```"
},
"Recommendation": {
"Text": "Deploy Databricks workspaces with **secure cluster connectivity (No Public IP)** so compute nodes have no public IP addresses. Because this is set at creation, plan a migration for existing workspaces, and pair it with **VNet injection**, **private endpoints / disabled public network access**, and least-privilege NSG rules to uphold **defense in depth**.",
"Url": "https://hub.prowler.com/check/databricks_workspace_no_public_ip_enabled"
}
},
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Secure cluster connectivity (No Public IP) applies to classic compute and is set at workspace creation. Serverless workspaces do not expose this setting (they have no customer-managed cluster nodes with public IPs) and are reported as MANUAL for verification."
}
@@ -0,0 +1,41 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.databricks.databricks_client import (
databricks_client,
)
class databricks_workspace_no_public_ip_enabled(Check):
"""
Ensure Azure Databricks workspaces have secure cluster connectivity (no public IP) enabled.
This check evaluates whether each Azure Databricks workspace in the subscription is deployed with secure cluster connectivity (No Public IP / NPIP), so cluster nodes are not assigned public IP addresses.
Secure cluster connectivity is a classic-compute setting. Serverless workspaces do not expose it (they have no customer-managed cluster nodes with public IPs), so the workspace is reported as MANUAL for verification rather than failed.
- PASS: The workspace has secure cluster connectivity enabled (no_public_ip_enabled is True).
- FAIL: The workspace has secure cluster connectivity disabled (no_public_ip_enabled is False).
- MANUAL: The workspace does not expose the setting (no_public_ip_enabled is None, e.g. serverless workspaces).
"""
def execute(self):
findings = []
for subscription, workspaces in databricks_client.workspaces.items():
subscription_name = databricks_client.subscriptions.get(
subscription, subscription
)
for workspace in workspaces.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=workspace
)
report.subscription = subscription
if workspace.no_public_ip_enabled is None:
report.status = "MANUAL"
report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) does not expose secure cluster connectivity (no public IP) settings (for example, serverless workspaces have no public-IP cluster nodes); verify the network configuration manually."
elif workspace.no_public_ip_enabled:
report.status = "PASS"
report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) has secure cluster connectivity (no public IP) enabled."
else:
report.status = "FAIL"
report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) does not have secure cluster connectivity (no public IP) enabled."
findings.append(report)
return findings
@@ -19,6 +19,7 @@ def mock_databricks_get_workspaces(_):
name="test-workspace",
location="eastus",
public_network_access="Disabled",
no_public_ip_enabled=True,
custom_managed_vnet_id="test-vnet-id",
managed_disk_encryption=ManagedDiskEncryption(
key_name="test-key",
@@ -55,6 +56,7 @@ class Test_Databricks_Service:
assert workspace.name == "test-workspace"
assert workspace.location == "eastus"
assert workspace.public_network_access == "Disabled"
assert workspace.no_public_ip_enabled is True
assert workspace.custom_managed_vnet_id == "test-vnet-id"
assert (
workspace.managed_disk_encryption.__class__.__name__
@@ -0,0 +1,174 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.azure.services.databricks.databricks_service import (
DatabricksWorkspace,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
AZURE_SUBSCRIPTION_NAME,
set_mocked_azure_provider,
)
class Test_databricks_workspace_no_public_ip_enabled:
def test_databricks_no_workspaces(self):
databricks_client = mock.MagicMock
databricks_client.subscriptions = {
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
}
databricks_client.workspaces = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled.databricks_client",
new=databricks_client,
),
):
from prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled import (
databricks_workspace_no_public_ip_enabled,
)
check = databricks_workspace_no_public_ip_enabled()
result = check.execute()
assert len(result) == 0
def test_databricks_workspace_no_public_ip_disabled(self):
workspace_id = str(uuid4())
workspace_name = "test-workspace"
databricks_client = mock.MagicMock
databricks_client.subscriptions = {
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
}
databricks_client.workspaces = {
AZURE_SUBSCRIPTION_ID: {
workspace_id: DatabricksWorkspace(
id=workspace_id,
name=workspace_name,
location="eastus",
no_public_ip_enabled=False,
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled.databricks_client",
new=databricks_client,
),
):
from prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled import (
databricks_workspace_no_public_ip_enabled,
)
check = databricks_workspace_no_public_ip_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have secure cluster connectivity (no public IP) enabled."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == workspace_name
assert result[0].resource_id == workspace_id
assert result[0].location == "eastus"
def test_databricks_workspace_no_public_ip_enabled(self):
workspace_id = str(uuid4())
workspace_name = "test-workspace"
databricks_client = mock.MagicMock
databricks_client.subscriptions = {
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
}
databricks_client.workspaces = {
AZURE_SUBSCRIPTION_ID: {
workspace_id: DatabricksWorkspace(
id=workspace_id,
name=workspace_name,
location="eastus",
no_public_ip_enabled=True,
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled.databricks_client",
new=databricks_client,
),
):
from prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled import (
databricks_workspace_no_public_ip_enabled,
)
check = databricks_workspace_no_public_ip_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} has secure cluster connectivity (no public IP) enabled."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == workspace_name
assert result[0].resource_id == workspace_id
assert result[0].location == "eastus"
def test_databricks_workspace_no_public_ip_not_reported(self):
workspace_id = str(uuid4())
workspace_name = "test-workspace"
databricks_client = mock.MagicMock
databricks_client.subscriptions = {
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
}
databricks_client.workspaces = {
AZURE_SUBSCRIPTION_ID: {
workspace_id: DatabricksWorkspace(
id=workspace_id,
name=workspace_name,
location="eastus",
no_public_ip_enabled=None,
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled.databricks_client",
new=databricks_client,
),
):
from prowler.providers.azure.services.databricks.databricks_workspace_no_public_ip_enabled.databricks_workspace_no_public_ip_enabled import (
databricks_workspace_no_public_ip_enabled,
)
check = databricks_workspace_no_public_ip_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"Databricks workspace {workspace_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY} does not expose secure cluster connectivity (no public IP) settings (for example, serverless workspaces have no public-IP cluster nodes); verify the network configuration manually."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == workspace_name
assert result[0].resource_id == workspace_id
assert result[0].location == "eastus"