mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(azure): add databricks_workspace_public_network_access_disabled check (#11035)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com> Co-authored-by: Hugo P.Brito <hugopbrito@Hugos-MacBook-Pro.local>
This commit is contained in:
@@ -16,6 +16,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `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)
|
||||
- `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)
|
||||
- `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)
|
||||
|
||||
@@ -1411,6 +1411,7 @@
|
||||
"containerregistry_not_publicly_accessible",
|
||||
"cosmosdb_account_firewall_use_selected_networks",
|
||||
"cosmosdb_account_public_network_access_disabled",
|
||||
"databricks_workspace_public_network_access_disabled",
|
||||
"network_bastion_host_exists",
|
||||
"network_flow_log_captured_sent",
|
||||
"network_flow_log_more_than_90_days",
|
||||
|
||||
@@ -66,6 +66,9 @@ class Databricks(AzureService):
|
||||
id=workspace.id,
|
||||
name=workspace.name,
|
||||
location=workspace.location,
|
||||
public_network_access=getattr(
|
||||
workspace, "public_network_access", None
|
||||
),
|
||||
custom_managed_vnet_id=(
|
||||
getattr(
|
||||
workspace_parameters, "custom_virtual_network_id", None
|
||||
@@ -107,6 +110,7 @@ class DatabricksWorkspace(BaseModel):
|
||||
id: The unique identifier of the workspace.
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
@@ -114,5 +118,6 @@ class DatabricksWorkspace(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
public_network_access: Optional[str] = None
|
||||
custom_managed_vnet_id: Optional[str] = None
|
||||
managed_disk_encryption: Optional[ManagedDiskEncryption] = None
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "databricks_workspace_public_network_access_disabled",
|
||||
"CheckTitle": "Databricks workspace has public network access disabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "databricks",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "microsoft.databricks/workspaces",
|
||||
"ResourceGroup": "ai_ml",
|
||||
"Description": "**Azure Databricks workspaces** are evaluated for **public network access**. Disabling public network access ensures the workspace is only reachable through **private endpoints**, reducing the attack surface and preventing direct exposure of the control plane and data plane to the internet.",
|
||||
"Risk": "Allowing **public network access** exposes the Databricks workspace control plane and data plane to the internet. Combined with leaked **credentials**, **personal access tokens**, or unpatched vulnerabilities, this enables **unauthorized access**, **brute force**, and **data exfiltration** from any source on the internet.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link",
|
||||
"https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link#step-4-disable-public-network-access",
|
||||
"https://learn.microsoft.com/en-us/azure/templates/microsoft.databricks/workspaces"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az databricks workspace update --name <workspace_name> --resource-group <resource_group_name> --public-network-access Disabled",
|
||||
"NativeIaC": "```bicep\n// Bicep: Disable public network access on a Databricks workspace\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 publicNetworkAccess: 'Disabled' // CRITICAL: Blocks all public-internet traffic to the workspace\n }\n}\n```",
|
||||
"Other": "1. Sign in to the Azure portal and open your Databricks workspace\n2. In the left menu, select Networking\n3. Under Public network access, select Disabled\n4. Configure Azure Private Link private endpoints before saving so clients retain connectivity\n5. Click Save",
|
||||
"Terraform": "```hcl\n# Terraform: Disable public network access on a Databricks workspace\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 public_network_access_enabled = false # CRITICAL: Blocks all public-internet traffic to the workspace\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Disable **public network access** on Databricks workspaces and require connectivity via **Azure Private Link** private endpoints. Before enforcement, validate that all application workloads have private-network connectivity, and pair this control with **VNet injection**, **secure cluster connectivity (no public IP)**, and least-privilege access to uphold **defense in depth**.",
|
||||
"Url": "https://hub.prowler.com/check/databricks_workspace_public_network_access_disabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.databricks.databricks_client import (
|
||||
databricks_client,
|
||||
)
|
||||
|
||||
|
||||
class databricks_workspace_public_network_access_disabled(Check):
|
||||
"""
|
||||
Ensure Azure Databricks workspaces have public network access disabled.
|
||||
|
||||
This check evaluates whether each Azure Databricks workspace in the subscription restricts connectivity to private endpoints by disabling public network access.
|
||||
|
||||
- PASS: The workspace has public network access disabled (public_network_access is "Disabled").
|
||||
- FAIL: The workspace has public network access enabled (or the value is not set).
|
||||
"""
|
||||
|
||||
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.public_network_access == "Disabled":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) has public network access disabled."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Databricks workspace {workspace.name} in subscription {subscription_name} ({subscription}) has public network access enabled."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -18,6 +18,7 @@ def mock_databricks_get_workspaces(_):
|
||||
id="test-workspace-id",
|
||||
name="test-workspace",
|
||||
location="eastus",
|
||||
public_network_access="Disabled",
|
||||
custom_managed_vnet_id="test-vnet-id",
|
||||
managed_disk_encryption=ManagedDiskEncryption(
|
||||
key_name="test-key",
|
||||
@@ -53,6 +54,7 @@ class Test_Databricks_Service:
|
||||
assert workspace.id == "test-workspace-id"
|
||||
assert workspace.name == "test-workspace"
|
||||
assert workspace.location == "eastus"
|
||||
assert workspace.public_network_access == "Disabled"
|
||||
assert workspace.custom_managed_vnet_id == "test-vnet-id"
|
||||
assert (
|
||||
workspace.managed_disk_encryption.__class__.__name__
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
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_public_network_access_disabled:
|
||||
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_public_network_access_disabled.databricks_workspace_public_network_access_disabled.databricks_client",
|
||||
new=databricks_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.databricks.databricks_workspace_public_network_access_disabled.databricks_workspace_public_network_access_disabled import (
|
||||
databricks_workspace_public_network_access_disabled,
|
||||
)
|
||||
|
||||
check = databricks_workspace_public_network_access_disabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_databricks_workspace_public_network_access_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",
|
||||
public_network_access="Enabled",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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_public_network_access_disabled.databricks_workspace_public_network_access_disabled.databricks_client",
|
||||
new=databricks_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.databricks.databricks_workspace_public_network_access_disabled.databricks_workspace_public_network_access_disabled import (
|
||||
databricks_workspace_public_network_access_disabled,
|
||||
)
|
||||
|
||||
check = databricks_workspace_public_network_access_disabled()
|
||||
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} has public network access 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_public_network_access_not_set(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",
|
||||
public_network_access=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_public_network_access_disabled.databricks_workspace_public_network_access_disabled.databricks_client",
|
||||
new=databricks_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.databricks.databricks_workspace_public_network_access_disabled.databricks_workspace_public_network_access_disabled import (
|
||||
databricks_workspace_public_network_access_disabled,
|
||||
)
|
||||
|
||||
check = databricks_workspace_public_network_access_disabled()
|
||||
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} has public network access enabled."
|
||||
)
|
||||
|
||||
def test_databricks_workspace_public_network_access_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",
|
||||
public_network_access="Disabled",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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_public_network_access_disabled.databricks_workspace_public_network_access_disabled.databricks_client",
|
||||
new=databricks_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.databricks.databricks_workspace_public_network_access_disabled.databricks_workspace_public_network_access_disabled import (
|
||||
databricks_workspace_public_network_access_disabled,
|
||||
)
|
||||
|
||||
check = databricks_workspace_public_network_access_disabled()
|
||||
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 public network access disabled."
|
||||
)
|
||||
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"
|
||||
Reference in New Issue
Block a user