feat(azure/defender): add new check defender_attack_path_notifications_properly_configured (#8245)

This commit is contained in:
Rubén De la Torre Vico
2025-07-17 12:40:26 +02:00
committed by GitHub
parent c4a9280ebb
commit 1211fe706e
10 changed files with 458 additions and 0 deletions

View File

@@ -78,6 +78,7 @@ The following list includes all the Azure checks with configurable variables tha
| `app_ensure_python_version_is_latest` | `python_latest_version` | String |
| `app_ensure_java_version_is_latest` | `java_latest_version` | String |
| `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings |
| `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String |
## GCP

View File

@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149)
- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168)
- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181)
- `defender_attack_path_notifications_properly_configured` check for Azure provider [(#8245)](https://github.com/prowler-cloud/prowler/pull/8245)
- `entra_intune_enrollment_sign_in_frequency_every_time` check for M365 provider [(#8223)](https://github.com/prowler-cloud/prowler/pull/8223)
- Support for remote repository scanning in IaC provider [(#8193)](https://github.com/prowler-cloud/prowler/pull/8193)
- Add `test_connection` method to GitHub provider [(#8248)](https://github.com/prowler-cloud/prowler/pull/8248)

View File

@@ -430,6 +430,10 @@ azure:
# TODO: create common config
shodan_api_key: null
# Configurable minimal risk level for attack path notifications
# azure.defender_attack_path_notifications_properly_configured
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.2"

View File

@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "defender_attack_path_notifications_properly_configured",
"CheckTitle": "Ensure that email notifications for attack paths are enabled with minimal risk level",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AzureEmailNotifications",
"Description": "Ensure that Microsoft Defender for Cloud is configured to send email notifications for attack paths identified in the Azure subscription with an appropriate minimal risk level.",
"Risk": "If attack path notifications are not enabled, security teams may not be promptly informed about exploitable attack sequences, increasing the risk of delayed mitigation and potential breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable attack path email notifications in Microsoft Defender for Cloud to ensure that security teams are notified when potential attack paths are identified. Configure the minimal risk level as appropriate for your organization.",
"Url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/configure-email-notifications"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}

View File

@@ -0,0 +1,51 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.defender.defender_client import defender_client
class defender_attack_path_notifications_properly_configured(Check):
"""
Ensure that email notifications for attack paths are enabled.
This check evaluates whether Microsoft Defender for Cloud is configured to send email notifications for attack paths in each Azure subscription.
- PASS: Notifications are enabled for attack paths with a risk level set (not None) and equal or higher than the configured minimum.
- FAIL: Notifications are not enabled for attack paths in the subscription or the risk level is too low.
"""
def execute(self) -> list[Check_Report_Azure]:
findings = []
# Get the minimal risk level from config, default to 'High'
risk_levels = ["Low", "Medium", "High", "Critical"]
min_risk_level = defender_client.audit_config.get(
"defender_attack_path_minimal_risk_level", "High"
)
if min_risk_level not in risk_levels:
min_risk_level = "High"
min_risk_index = risk_levels.index(min_risk_level)
for (
subscription_name,
security_contact_configurations,
) in defender_client.security_contact_configurations.items():
for contact_configuration in security_contact_configurations.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=contact_configuration
)
report.subscription = subscription_name
actual_risk_level = getattr(
contact_configuration, "attack_path_minimal_risk_level", None
)
if not actual_risk_level or actual_risk_level not in risk_levels:
report.status = "FAIL"
report.status_extended = f"Attack path notifications are not enabled in subscription {subscription_name} for security contact {contact_configuration.name}."
else:
actual_risk_index = risk_levels.index(actual_risk_level)
if actual_risk_index <= min_risk_index:
report.status = "PASS"
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
else:
report.status = "FAIL"
report.status_extended = f"Attack path notifications are enabled with minimal risk level {actual_risk_level} in subscription {subscription_name} for security contact {contact_configuration.name}."
findings.append(report)
return findings

View File

@@ -321,6 +321,7 @@ config_azure = {
"python_latest_version": "3.12",
"java_latest_version": "17",
"recommended_minimal_tls_versions": ["1.2", "1.3"],
"defender_attack_path_minimal_risk_level": "High",
}
config_gcp = {"shodan_api_key": None, "max_unused_account_days": 30}

View File

@@ -376,6 +376,8 @@ azure:
# azure.network_public_ip_shodan
# TODO: create common config
shodan_api_key: null
# Configurable minimal risk level for attack path notifications
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# azure.app_ensure_php_version_is_latest

View File

@@ -85,6 +85,7 @@ class TestAzureProvider:
"python_latest_version": "3.12",
"java_latest_version": "17",
"recommended_minimal_tls_versions": ["1.2", "1.3"],
"defender_attack_path_minimal_risk_level": "High",
}
def test_azure_provider_not_auth_methods(self):

View File

@@ -0,0 +1,367 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.azure.services.defender.defender_service import (
NotificationsByRole,
SecurityContactConfiguration,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
class Test_defender_attack_path_notifications_properly_configured:
def test_no_subscriptions(self):
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {}
defender_client.audit_config = {}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 0
def test_attack_path_notifications_none(self):
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level=None,
)
}
}
defender_client.audit_config = {}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == (
f"Attack path notifications are not enabled in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id
def test_attack_path_notifications_custom_config(self):
# Configured minimal risk level is Medium
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level="Medium",
)
}
}
defender_client.audit_config = {
"defender_attack_path_minimal_risk_level": "Medium"
}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id
def test_attack_path_notifications_invalid_config(self):
# Configured minimal risk level is invalid, should default to High
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level="Medium",
)
}
}
defender_client.audit_config = {
"defender_attack_path_minimal_risk_level": "INVALID"
}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id
def test_attack_path_notifications_low_default_high(self):
# Low risk level, default config (High) -> PASS
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level="Low",
)
}
}
defender_client.audit_config = {}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
f"Attack path notifications are enabled with minimal risk level Low in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id
def test_attack_path_notifications_medium_default_high(self):
# Medium risk level, default config (High) -> PASS
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level="Medium",
)
}
}
defender_client.audit_config = {}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
f"Attack path notifications are enabled with minimal risk level Medium in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id
def test_attack_path_notifications_high_default_high(self):
# High risk level, default config (High) -> PASS
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level="High",
)
}
}
defender_client.audit_config = {}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
f"Attack path notifications are enabled with minimal risk level High in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id
def test_attack_path_notifications_critical_default_high(self):
# Critical risk level, default config (High) -> FAIL
resource_id = str(uuid4())
contact_name = "default"
defender_client = mock.MagicMock()
defender_client.security_contact_configurations = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContactConfiguration(
id=resource_id,
name=contact_name,
enabled=True,
emails=[""],
phone="",
notifications_by_role=NotificationsByRole(
state=True, roles=["Owner"]
),
alert_minimal_severity="High",
attack_path_minimal_risk_level="Critical",
)
}
}
defender_client.audit_config = {}
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_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_attack_path_notifications_properly_configured.defender_attack_path_notifications_properly_configured import (
defender_attack_path_notifications_properly_configured,
)
check = defender_attack_path_notifications_properly_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == (
f"Attack path notifications are enabled with minimal risk level Critical in subscription {AZURE_SUBSCRIPTION_ID} for security contact {contact_name}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == contact_name
assert result[0].resource_id == resource_id