From df1abb2152892ddf4d851bfa16a807460fc0999f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Tue, 24 Jun 2025 12:04:20 +0200 Subject: [PATCH] feat(azure): add new check `monitor_alert_service_health_exists` (#8067) Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ..._alert_service_health_exists.metadata.json | 30 ++++ .../monitor_alert_service_health_exists.py | 48 +++++ .../azure/services/monitor/monitor_service.py | 4 +- ...onitor_alert_service_health_exists_test.py | 164 ++++++++++++++++++ 6 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/__init__.py create mode 100644 prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json create mode 100644 prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py create mode 100644 tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index ab222956b1..4ce0326c25 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) +- 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) ### Fixed diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/__init__.py b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json new file mode 100644 index 0000000000..4354005d98 --- /dev/null +++ b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "monitor_alert_service_health_exists", + "CheckTitle": "Ensure that an Activity Log Alert exists for Service Health", + "CheckType": [], + "ServiceName": "monitor", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Monitor", + "Description": "Ensure that an Azure activity log alert is configured to trigger when Service Health events occur within your Microsoft Azure cloud account. The alert should activate when new events match the specified conditions in the alert rule configuration.", + "Risk": "Lack of monitoring for Service Health events may result in missing critical service issues, planned maintenance, security advisories, or other changes that could impact Azure services and regions in use.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/service-health/overview", + "Remediation": { + "Code": { + "CLI": "az monitor activity-log alert create --subscription --resource-group --name --condition category=ServiceHealth and properties.incidentType=Incident --scope /subscriptions/ --action-group ", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActivityLog/service-health-alert.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create an activity log alert for Service Health events and configure an action group to notify appropriate personnel.", + "Url": "https://learn.microsoft.com/en-us/azure/service-health/alerts-activity-log-service-notifications-portal" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "By default, in your Azure subscription there will not be any activity log alerts configured for Service Health events." +} diff --git a/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py new file mode 100644 index 0000000000..94dc9747c3 --- /dev/null +++ b/prowler/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists.py @@ -0,0 +1,48 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.monitor.monitor_client import monitor_client + + +class monitor_alert_service_health_exists(Check): + def execute(self) -> list[Check_Report_Azure]: + findings = [] + + for ( + subscription_name, + activity_log_alerts, + ) in monitor_client.alert_rules.items(): + for alert_rule in activity_log_alerts: + # Check if alert rule is enabled and has required Service Health conditions + if alert_rule.enabled: + has_service_health_category = False + has_incident_type_incident = False + for element in alert_rule.condition.all_of: + if ( + element.field == "category" + and element.equals == "ServiceHealth" + ): + has_service_health_category = True + if ( + element.field == "properties.incidentType" + and element.equals == "Incident" + ): + has_incident_type_incident = True + + if has_service_health_category and has_incident_type_incident: + report = Check_Report_Azure( + metadata=self.metadata(), resource=alert_rule + ) + report.subscription = subscription_name + report.status = "PASS" + report.status_extended = f"There is an activity log alert for Service Health in subscription {subscription_name}." + break + else: + report = Check_Report_Azure(metadata=self.metadata(), resource={}) + report.subscription = subscription_name + report.resource_name = "Monitor" + report.resource_id = "Monitor" + report.status = "FAIL" + report.status_extended = f"There is no activity log alert for Service Health in subscription {subscription_name}." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/monitor/monitor_service.py b/prowler/providers/azure/services/monitor/monitor_service.py index 7f5b9c8232..b4542a7b14 100644 --- a/prowler/providers/azure/services/monitor/monitor_service.py +++ b/prowler/providers/azure/services/monitor/monitor_service.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List +from typing import List, Optional from azure.mgmt.monitor import MonitorManagementClient @@ -131,4 +131,4 @@ class AlertRule: name: str condition: AlertRuleAllOfCondition enabled: bool - description: str + description: Optional[str] diff --git a/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py b/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py new file mode 100644 index 0000000000..1d7dbb12b5 --- /dev/null +++ b/tests/providers/azure/services/monitor/monitor_alert_service_health_exists/monitor_alert_service_health_exists_test.py @@ -0,0 +1,164 @@ +from unittest import mock + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_monitor_alert_service_health_exists: + def test_monitor_alert_service_health_exists_no_subscriptions(self): + monitor_client = mock.MagicMock() + monitor_client.alert_rules = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 0 + + def test_no_alert_rules(self): + monitor_client = mock.MagicMock() + monitor_client.alert_rules = {AZURE_SUBSCRIPTION_ID: []} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "Monitor" + assert result[0].resource_id == "Monitor" + assert ( + result[0].status_extended + == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + ) + + def test_alert_rules_configured(self): + monitor_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.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + from prowler.providers.azure.services.monitor.monitor_service import ( + AlertRule, + AlertRuleAllOfCondition, + AlertRuleAnyOfOrLeafCondition, + ) + + monitor_client.alert_rules = { + AZURE_SUBSCRIPTION_ID: [ + AlertRule( + id="id1", + name="name1", + condition=AlertRuleAllOfCondition( + all_of=[ + AlertRuleAnyOfOrLeafCondition( + field="category", equals="ServiceHealth" + ), + AlertRuleAnyOfOrLeafCondition( + field="properties.incidentType", equals="Incident" + ), + ] + ), + enabled=True, + description="desc1", + ), + ] + } + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "name1" + assert result[0].resource_id == "id1" + assert ( + result[0].status_extended + == f"There is an activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + ) + + def test_alert_rules_configured_but_disabled(self): + monitor_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.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists.monitor_client", + new=monitor_client, + ), + ): + from prowler.providers.azure.services.monitor.monitor_alert_service_health_exists.monitor_alert_service_health_exists import ( + monitor_alert_service_health_exists, + ) + from prowler.providers.azure.services.monitor.monitor_service import ( + AlertRule, + AlertRuleAllOfCondition, + AlertRuleAnyOfOrLeafCondition, + ) + + monitor_client.alert_rules = { + AZURE_SUBSCRIPTION_ID: [ + AlertRule( + id="id1", + name="name1", + condition=AlertRuleAllOfCondition( + all_of=[ + AlertRuleAnyOfOrLeafCondition( + field="category", equals="ServiceHealth" + ), + AlertRuleAnyOfOrLeafCondition( + field="properties.incidentType", equals="Incident" + ), + ] + ), + enabled=False, + description="desc1", + ), + ] + } + check = monitor_alert_service_health_exists() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "Monitor" + assert result[0].resource_id == "Monitor" + assert ( + result[0].status_extended + == f"There is no activity log alert for Service Health in subscription {AZURE_SUBSCRIPTION_ID}." + )