From ddbf3405a03316be862540c97a8721a389de3aee Mon Sep 17 00:00:00 2001 From: s1ns3nz0 Date: Thu, 18 Jun 2026 17:05:02 +0900 Subject: [PATCH] feat(azure): add defender_ensure_defender_cspm_is_on check (#11037) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 1 + prowler/compliance/azure/cis_5.0_azure.json | 4 +- .../compliance/azure/iso27001_2022_azure.json | 1 + .../__init__.py | 0 ...r_ensure_defender_cspm_is_on.metadata.json | 37 ++++++ .../defender_ensure_defender_cspm_is_on.py | 35 ++++++ ...efender_ensure_defender_cspm_is_on_test.py | 117 ++++++++++++++++++ 7 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/__init__.py create mode 100644 prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.metadata.json create mode 100644 prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.py create mode 100644 tests/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 7ba3afbe3d..db0987dea6 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `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) +- `defender_ensure_defender_cspm_is_on` check for Azure provider, verifying Microsoft Defender Cloud Security Posture Management (CSPM) is enabled on the Standard tier [(#11037)](https://github.com/prowler-cloud/prowler/pull/11037) - `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) diff --git a/prowler/compliance/azure/cis_5.0_azure.json b/prowler/compliance/azure/cis_5.0_azure.json index 21785d92b6..086320fecd 100644 --- a/prowler/compliance/azure/cis_5.0_azure.json +++ b/prowler/compliance/azure/cis_5.0_azure.json @@ -2094,7 +2094,9 @@ { "Id": "8.1.1.1", "Description": "Ensure Microsoft Defender CSPM is set to 'On'", - "Checks": [], + "Checks": [ + "defender_ensure_defender_cspm_is_on" + ], "Attributes": [ { "Section": "8 Security Services", diff --git a/prowler/compliance/azure/iso27001_2022_azure.json b/prowler/compliance/azure/iso27001_2022_azure.json index 4172a9449a..719f358de5 100644 --- a/prowler/compliance/azure/iso27001_2022_azure.json +++ b/prowler/compliance/azure/iso27001_2022_azure.json @@ -1340,6 +1340,7 @@ } ], "Checks": [ + "defender_ensure_defender_cspm_is_on", "monitor_alert_create_policy_assignment", "monitor_alert_create_update_nsg", "monitor_alert_create_update_public_ip_address_rule", diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/__init__.py b/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.metadata.json b/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.metadata.json new file mode 100644 index 0000000000..cae92490af --- /dev/null +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "azure", + "CheckID": "defender_ensure_defender_cspm_is_on", + "CheckTitle": "Microsoft Defender CSPM is set to On", + "CheckType": [], + "ServiceName": "defender", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.security/pricings", + "ResourceGroup": "security", + "Description": "**Microsoft Defender for Cloud** Cloud Security Posture Management (CSPM) plan is evaluated for **standard tier** activation. Defender CSPM provides advanced posture management capabilities including attack path analysis, cloud security explorer, agentless scanning, and governance rules.", + "Risk": "Without Defender CSPM, the subscription relies on **foundational CSPM** (free tier) which lacks attack path analysis, agentless vulnerability scanning, and security governance. Advanced threats exploiting misconfiguration chains go undetected.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/defender-for-cloud/concept-cloud-security-posture-management", + "https://learn.microsoft.com/en-us/azure/defender-for-cloud/enable-enhanced-security" + ], + "Remediation": { + "Code": { + "CLI": "az security pricing create -n CloudPosture --tier Standard", + "NativeIaC": "```bicep\ntargetScope = 'subscription'\n\nresource defenderCSPM 'Microsoft.Security/pricings@2024-01-01' = {\n name: 'CloudPosture'\n properties: {\n pricingTier: 'Standard' // Critical: enables Defender CSPM\n }\n}\n```", + "Other": "1. Sign in to Azure portal\n2. Go to Microsoft Defender for Cloud\n3. Select Environment Settings\n4. Click on the subscription\n5. Set Cloud Security Posture Management (CSPM) to On\n6. Click Save", + "Terraform": "```hcl\nresource \"azurerm_security_center_subscription_pricing\" \"\" {\n tier = \"Standard\" # Critical: enables Defender CSPM\n resource_type = \"CloudPosture\"\n}\n```" + }, + "Recommendation": { + "Text": "Enable **Defender CSPM** standard tier for advanced cloud security posture management. Evaluate the cost against the security benefits \u2014 CSPM provides attack path analysis and agentless scanning.", + "Url": "https://hub.prowler.com/check/defender_ensure_defender_cspm_is_on" + } + }, + "Categories": [ + "threat-detection" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.py b/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.py new file mode 100644 index 0000000000..cf7957400c --- /dev/null +++ b/prowler/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on.py @@ -0,0 +1,35 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.defender.defender_client import defender_client + + +class defender_ensure_defender_cspm_is_on(Check): + """ + Ensure Microsoft Defender Cloud Security Posture Management (CSPM) is set to On. + + This check evaluates whether the Defender CSPM plan (CloudPosture pricing) is enabled with the Standard tier for each subscription. + + - PASS: The CloudPosture pricing tier is "Standard" (Defender CSPM is on). + - FAIL: The CloudPosture pricing tier is not "Standard" (Defender CSPM is off). + """ + + def execute(self) -> Check_Report_Azure: + findings = [] + for subscription, pricings in defender_client.pricings.items(): + subscription_name = defender_client.subscriptions.get( + subscription, subscription + ) + if "CloudPosture" in pricings: + report = Check_Report_Azure( + metadata=self.metadata(), + resource=pricings["CloudPosture"], + ) + report.subscription = subscription + report.resource_name = "Defender plan CSPM" + report.status = "PASS" + report.status_extended = f"Defender plan CSPM from subscription {subscription_name} ({subscription}) is set to ON (pricing tier standard)." + if pricings["CloudPosture"].pricing_tier != "Standard": + report.status = "FAIL" + report.status_extended = f"Defender plan CSPM from subscription {subscription_name} ({subscription}) is set to OFF (pricing tier not standard)." + + findings.append(report) + return findings diff --git a/tests/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on_test.py b/tests/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on_test.py new file mode 100644 index 0000000000..b81d10d31d --- /dev/null +++ b/tests/providers/azure/services/defender/defender_ensure_defender_cspm_is_on/defender_ensure_defender_cspm_is_on_test.py @@ -0,0 +1,117 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.defender.defender_service import Pricing +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, + AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, + set_mocked_azure_provider, +) + + +class Test_defender_ensure_defender_cspm_is_on: + def test_defender_no_cspm(self): + defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + defender_client.pricings = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.defender.defender_ensure_defender_cspm_is_on.defender_ensure_defender_cspm_is_on.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.defender.defender_ensure_defender_cspm_is_on.defender_ensure_defender_cspm_is_on import ( + defender_ensure_defender_cspm_is_on, + ) + + check = defender_ensure_defender_cspm_is_on() + result = check.execute() + assert len(result) == 0 + + def test_defender_cspm_pricing_tier_not_standard(self): + resource_id = str(uuid4()) + defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + defender_client.pricings = { + AZURE_SUBSCRIPTION_ID: { + "CloudPosture": Pricing( + resource_id=resource_id, + resource_name="Defender plan CSPM", + pricing_tier="Free", + free_trial_remaining_time=0, + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.defender.defender_ensure_defender_cspm_is_on.defender_ensure_defender_cspm_is_on.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.defender.defender_ensure_defender_cspm_is_on.defender_ensure_defender_cspm_is_on import ( + defender_ensure_defender_cspm_is_on, + ) + + check = defender_ensure_defender_cspm_is_on() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Defender plan CSPM from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to OFF (pricing tier not standard)." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "Defender plan CSPM" + assert result[0].resource_id == resource_id + + def test_defender_cspm_pricing_tier_standard(self): + resource_id = str(uuid4()) + defender_client = mock.MagicMock + defender_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME} + defender_client.pricings = { + AZURE_SUBSCRIPTION_ID: { + "CloudPosture": Pricing( + resource_id=resource_id, + resource_name="Defender plan CSPM", + pricing_tier="Standard", + free_trial_remaining_time=0, + ) + } + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.defender.defender_ensure_defender_cspm_is_on.defender_ensure_defender_cspm_is_on.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.defender.defender_ensure_defender_cspm_is_on.defender_ensure_defender_cspm_is_on import ( + defender_ensure_defender_cspm_is_on, + ) + + check = defender_ensure_defender_cspm_is_on() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Defender plan CSPM from subscription {AZURE_SUBSCRIPTION_DISPLAY} is set to ON (pricing tier standard)." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "Defender plan CSPM" + assert result[0].resource_id == resource_id