feat(defender): add new check defender_antispam_outbound_policy_configured (#7480)

Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2025-04-22 15:58:07 +02:00
committed by GitHub
parent 2ebf217bb0
commit b07080245d
10 changed files with 533 additions and 15 deletions
+1
View File
@@ -10,6 +10,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add check for unused Service Accounts in GCP [(#7419)](https://github.com/prowler-cloud/prowler/pull/7419).
- Add Powershell to Microsoft365 [(#7331)](https://github.com/prowler-cloud/prowler/pull/7331).
- Add service Defender to Microsoft365 with one check for Common Attachments filter enabled in Malware Policies [(#7425)](https://github.com/prowler-cloud/prowler/pull/7425).
- Add check for Outbound Antispam Policy well configured in service Defender for M365 [(#7480)](https://github.com/prowler-cloud/prowler/pull/7480).
- Add check for Antiphishing Policy well configured in service Defender in M365 [(#7453)](https://github.com/prowler-cloud/prowler/pull/7453).
- Add check for Notifications for Internal users enabled in Malware Policies from service Defender in M365 [(#7435)](https://github.com/prowler-cloud/prowler/pull/7435).
- Support CLOUDSDK_AUTH_ACCESS_TOKEN in GCP [(#7495)](https://github.com/prowler-cloud/prowler/pull/7495).
@@ -188,6 +188,43 @@ class M365PowerShell(PowerShellSession):
"""
return self.execute("Get-MalwareFilterPolicy | ConvertTo-Json")
def get_outbound_spam_filter_policy(self) -> dict:
"""
Get Defender Outbound Spam Filter Policy.
Retrieves the current Defender outbound spam filter policy settings.
Returns:
dict: Outbound spam filter policy settings in JSON format.
Example:
>>> get_outbound_spam_filter_policy()
{
"NotifyOutboundSpam": true,
"BccSuspiciousOutboundMail": true,
"BccSuspiciousOutboundAdditionalRecipients": [],
"NotifyOutboundSpamRecipients": []
}
"""
return self.execute("Get-HostedOutboundSpamFilterPolicy | ConvertTo-Json")
def get_outbound_spam_filter_rule(self) -> dict:
"""
Get Defender Outbound Spam Filter Rule.
Retrieves the current Defender outbound spam filter rule settings.
Returns:
dict: Outbound spam filter rule settings in JSON format.
Example:
>>> get_outbound_spam_filter_rule()
{
"State": "Enabled"
}
"""
return self.execute("Get-HostedOutboundSpamFilterRule | ConvertTo-Json")
def get_antiphishing_policy(self) -> dict:
"""
Get Defender Antiphishing Policy.
@@ -0,0 +1,30 @@
{
"Provider": "m365",
"CheckID": "defender_antispam_outbound_policy_configured",
"CheckTitle": "Ensure Defender Outbound Spam Policies are set to notify administrators.",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "low",
"ResourceType": "Defender Anti-Spam Policy",
"Description": "Ensure that outbound anti-spam policies are configured to notify administrators and copy suspicious outbound messages to designated recipients when a sender is blocked for sending spam emails.",
"Risk": "Without outbound spam notifications and message copies, compromised accounts may go undetected, increasing the risk of reputation damage or data leakage through unauthorized email activity.",
"RelatedUrl": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about",
"Remediation": {
"Code": {
"CLI": "$BccEmailAddress = @(\"<INSERT-EMAIL>\")\n$NotifyEmailAddress = @(\"<INSERT-EMAIL>\")\nSet-HostedOutboundSpamFilterPolicy -Identity Default -BccSuspiciousOutboundAdditionalRecipients $BccEmailAddress -BccSuspiciousOutboundMail $true -NotifyOutboundSpam $true -NotifyOutboundSpamRecipients $NotifyEmailAddress",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft 365 Defender https://security.microsoft.com. 2. Click to expand Email & collaboration and select Policies & rules > Threat policies. 3. Under Policies, select Anti-spam. 4. Click on the Anti-spam outbound policy (default). 5. Select Edit protection settings then under Notifications: 6. Check 'Send a copy of suspicious outbound messages or message that exceed these limits to these users and groups' and enter the email addresses. 7. Check 'Notify these users and groups if a sender is blocked due to sending outbound spam' and enter the desired email addresses. 8. Click Save.",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure Defender outbound spam filter policies to notify administrators and copy suspicious outbound messages when users are blocked for sending spam.",
"Url": "https://learn.microsoft.com/en-us/defender-office-365/outbound-spam-protection-about"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Ensure settings are applied to the highest priority policy if custom policies exist. Default values do not notify or copy outbound spam messages by default."
}
@@ -0,0 +1,54 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.defender.defender_client import defender_client
class defender_antispam_outbound_policy_configured(Check):
"""
Check if the Exchange Online Spam Policies are configured to notify administrators
when a sender is blocked for sending spam emails.
Attributes:
metadata: Metadata associated with the check (inherited from Check).
"""
def execute(self) -> List[CheckReportM365]:
"""
Execute the check to verify if the Exchange Online Spam Policies notify administrators
when a sender is blocked for sending spam emails.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
for policy_name, policy in defender_client.outbound_spam_policies.items():
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name="Defender Outbound Spam Policy",
resource_id=policy_name,
)
report.status = "FAIL"
report.status_extended = (
f"Outbound Spam Policy {policy_name} is not properly configured."
)
if (
not policy.default
and policy_name in defender_client.outbound_spam_rules
and defender_client.outbound_spam_rules[policy_name].state.lower()
== "enabled"
) or policy.default:
if (
policy.notify_limit_exceeded
and policy.notify_sender_blocked
and policy.notify_limit_exceeded_addresses
and policy.notify_sender_blocked_addresses
):
report.status = "PASS"
report.status_extended = f"Outbound Spam Policy {policy_name} is properly configured and enabled."
findings.append(report)
return findings
@@ -1,3 +1,5 @@
from typing import List
from pydantic import BaseModel
from prowler.lib.logger import logger
@@ -10,6 +12,8 @@ class Defender(M365Service):
super().__init__(provider)
self.powershell.connect_exchange_online()
self.malware_policies = self._get_malware_filter_policy()
self.outbound_spam_policies = self._get_outbound_spam_filter_policy()
self.outbound_spam_rules = self._get_outbound_spam_filter_rule()
self.antiphishing_policies = self._get_antiphising_policy()
self.antiphising_rules = self._get_antiphising_rules()
self.powershell.close()
@@ -24,7 +28,7 @@ class Defender(M365Service):
for policy in malware_policy:
if policy:
malware_policies.append(
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=policy.get("EnableFileFilter", True),
identity=policy.get("Identity", ""),
enable_internal_sender_admin_notifications=policy.get(
@@ -89,8 +93,54 @@ class Defender(M365Service):
)
return antiphishing_rules
def _get_outbound_spam_filter_policy(self):
logger.info("Microsoft365 - Getting Defender outbound spam filter policy...")
outbound_spam_policies = {}
try:
outbound_spam_policy = self.powershell.get_outbound_spam_filter_policy()
if isinstance(outbound_spam_policy, dict):
outbound_spam_policy = [outbound_spam_policy]
for policy in outbound_spam_policy:
if policy:
outbound_spam_policies[policy.get("Name", "")] = OutboundSpamPolicy(
notify_sender_blocked=policy.get("NotifyOutboundSpam", True),
notify_limit_exceeded=policy.get(
"BccSuspiciousOutboundMail", True
),
notify_limit_exceeded_addresses=policy.get(
"BccSuspiciousOutboundAdditionalRecipients", []
),
notify_sender_blocked_addresses=policy.get(
"NotifyOutboundSpamRecipients", []
),
default=policy.get("IsDefault", False),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return outbound_spam_policies
class DefenderMalwarePolicy(BaseModel):
def _get_outbound_spam_filter_rule(self):
logger.info("Microsoft365 - Getting Defender outbound spam filter rule...")
outbound_spam_rules = {}
try:
outbound_spam_rule = self.powershell.get_outbound_spam_filter_rule()
if isinstance(outbound_spam_rule, dict):
outbound_spam_rule = [outbound_spam_rule]
for rule in outbound_spam_rule:
if rule:
outbound_spam_rules[rule.get("Name", "")] = OutboundSpamRule(
state=rule.get("State", "Disabled"),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return outbound_spam_rules
class MalwarePolicy(BaseModel):
enable_file_filter: bool
identity: str
enable_internal_sender_admin_notifications: bool
@@ -111,3 +161,15 @@ class AntiphishingPolicy(BaseModel):
class AntiphishingRule(BaseModel):
state: str
class OutboundSpamPolicy(BaseModel):
notify_sender_blocked: bool
notify_limit_exceeded: bool
notify_limit_exceeded_addresses: List[str]
notify_sender_blocked_addresses: List[str]
default: bool
class OutboundSpamRule(BaseModel):
state: str
@@ -0,0 +1,248 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_defender_antispam_outbound_policy_configured:
def test_properly_configured_custom_policy(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
defender_antispam_outbound_policy_configured,
)
from prowler.providers.m365.services.defender.defender_service import (
OutboundSpamPolicy,
OutboundSpamRule,
)
defender_client.outbound_spam_policies = {
"Policy1": OutboundSpamPolicy(
notify_sender_blocked=True,
notify_limit_exceeded=True,
notify_limit_exceeded_addresses=["test@correo.com"],
notify_sender_blocked_addresses=["test@correo.com"],
default=False,
)
}
defender_client.outbound_spam_rules = {
"Policy1": OutboundSpamRule(state="Enabled")
}
check = defender_antispam_outbound_policy_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Outbound Spam Policy Policy1 is properly configured and enabled."
)
assert (
result[0].resource
== defender_client.outbound_spam_policies["Policy1"].dict()
)
assert result[0].resource_name == "Defender Outbound Spam Policy"
assert result[0].resource_id == "Policy1"
assert result[0].location == "global"
def test_not_properly_configured_policy(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
defender_antispam_outbound_policy_configured,
)
from prowler.providers.m365.services.defender.defender_service import (
OutboundSpamPolicy,
OutboundSpamRule,
)
defender_client.outbound_spam_policies = {
"Policy2": OutboundSpamPolicy(
notify_sender_blocked=False,
notify_limit_exceeded=False,
notify_limit_exceeded_addresses=[],
notify_sender_blocked_addresses=[],
default=False,
)
}
defender_client.outbound_spam_rules = {
"Policy2": OutboundSpamRule(state="Enabled")
}
check = defender_antispam_outbound_policy_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Outbound Spam Policy Policy2 is not properly configured."
)
assert (
result[0].resource
== defender_client.outbound_spam_policies["Policy2"].dict()
)
assert result[0].resource_name == "Defender Outbound Spam Policy"
assert result[0].resource_id == "Policy2"
assert result[0].location == "global"
def test_properly_configured_default_policy(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
defender_antispam_outbound_policy_configured,
)
from prowler.providers.m365.services.defender.defender_service import (
OutboundSpamPolicy,
)
defender_client.outbound_spam_policies = {
"Default": OutboundSpamPolicy(
notify_sender_blocked=True,
notify_limit_exceeded=True,
notify_limit_exceeded_addresses=["test@correo.com"],
notify_sender_blocked_addresses=["test@correo.com"],
default=True,
)
}
defender_client.outbound_spam_rules = {}
check = defender_antispam_outbound_policy_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Outbound Spam Policy Default is properly configured and enabled."
)
assert (
result[0].resource
== defender_client.outbound_spam_policies["Default"].dict()
)
assert result[0].resource_name == "Defender Outbound Spam Policy"
assert result[0].resource_id == "Default"
assert result[0].location == "global"
def test_policy_without_rule(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
defender_antispam_outbound_policy_configured,
)
from prowler.providers.m365.services.defender.defender_service import (
OutboundSpamPolicy,
)
defender_client.outbound_spam_policies = {
"PolicyX": OutboundSpamPolicy(
notify_sender_blocked=True,
notify_limit_exceeded=True,
notify_limit_exceeded_addresses=["admin@org.com"],
notify_sender_blocked_addresses=["admin@org.com"],
default=False,
)
}
defender_client.outbound_spam_rules = {}
check = defender_antispam_outbound_policy_configured()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Outbound Spam Policy PolicyX is not properly configured."
)
assert (
result[0].resource
== defender_client.outbound_spam_policies["PolicyX"].dict()
)
assert result[0].resource_name == "Defender Outbound Spam Policy"
assert result[0].resource_id == "PolicyX"
assert result[0].location == "global"
def test_no_outbound_spam_policies(self):
defender_client = mock.MagicMock()
defender_client.audited_tenant = "audited_tenant"
defender_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured.defender_client",
new=defender_client,
),
):
from prowler.providers.m365.services.defender.defender_antispam_outbound_policy_configured.defender_antispam_outbound_policy_configured import (
defender_antispam_outbound_policy_configured,
)
defender_client.outbound_spam_policies = {}
defender_client.outbound_spam_rules = {}
check = defender_antispam_outbound_policy_configured()
result = check.execute()
assert len(result) == 0
@@ -26,11 +26,11 @@ class Test_defender_malware_policy_common_attachments_filter_enabled:
defender_malware_policy_common_attachments_filter_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
DefenderMalwarePolicy,
MalwarePolicy,
)
defender_client.malware_policies = [
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=False,
identity="Policy1",
enable_internal_sender_admin_notifications=False,
@@ -74,11 +74,11 @@ class Test_defender_malware_policy_common_attachments_filter_enabled:
defender_malware_policy_common_attachments_filter_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
DefenderMalwarePolicy,
MalwarePolicy,
)
defender_client.malware_policies = [
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=True,
identity="Policy1",
enable_internal_sender_admin_notifications=False,
@@ -26,11 +26,11 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
defender_malware_policy_notifications_internal_users_malware_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
DefenderMalwarePolicy,
MalwarePolicy,
)
defender_client.malware_policies = [
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=True,
identity="Default",
enable_internal_sender_admin_notifications=False,
@@ -76,11 +76,11 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
defender_malware_policy_notifications_internal_users_malware_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
DefenderMalwarePolicy,
MalwarePolicy,
)
defender_client.malware_policies = [
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=True,
identity="Default",
enable_internal_sender_admin_notifications=True,
@@ -126,11 +126,11 @@ class Test_defender_malware_policy_notifications_internal_users_malware_enabled:
defender_malware_policy_notifications_internal_users_malware_enabled,
)
from prowler.providers.m365.services.defender.defender_service import (
DefenderMalwarePolicy,
MalwarePolicy,
)
defender_client.malware_policies = [
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=True,
identity="Default",
enable_internal_sender_admin_notifications=True,
@@ -6,20 +6,22 @@ from prowler.providers.m365.services.defender.defender_service import (
AntiphishingPolicy,
AntiphishingRule,
Defender,
DefenderMalwarePolicy,
MalwarePolicy,
OutboundSpamPolicy,
OutboundSpamRule,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
def mock_defender_get_malware_filter_policy(_):
return [
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=False,
identity="Policy1",
enable_internal_sender_admin_notifications=False,
internal_sender_admin_address="",
),
DefenderMalwarePolicy(
MalwarePolicy(
enable_file_filter=True,
identity="Policy2",
enable_internal_sender_admin_notifications=True,
@@ -66,6 +68,36 @@ def mock_defender_get_antiphising_rules(_):
}
def mock_defender_get_outbound_spam_filter_policy(_):
return {
"Policy1": OutboundSpamPolicy(
notify_sender_blocked=True,
notify_limit_exceeded=True,
notify_limit_exceeded_addresses=["security@example.com"],
notify_sender_blocked_addresses=["security@example.com"],
default=False,
),
"Policy2": OutboundSpamPolicy(
notify_sender_blocked=False,
notify_limit_exceeded=False,
notify_limit_exceeded_addresses=[],
notify_sender_blocked_addresses=[],
default=True,
),
}
def mock_defender_get_outbound_spam_filter_rule(_):
return {
"Policy1": OutboundSpamRule(
state="Enabled",
),
"Policy2": OutboundSpamRule(
state="Disabled",
),
}
class Test_Defender_Service:
def test_get_client(self):
with (
@@ -177,3 +209,57 @@ class Test_Defender_Service:
assert antiphishing_rules["Policy1"].state == "Enabled"
assert antiphishing_rules["Policy2"].state == "Disabled"
defender_client.powershell.close()
@patch(
"prowler.providers.m365.services.defender.defender_service.Defender._get_outbound_spam_filter_policy",
new=mock_defender_get_outbound_spam_filter_policy,
)
def test_get_outbound_spam_filter_policy(self):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
):
defender_client = Defender(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
outbound_spam_policies = defender_client.outbound_spam_policies
assert outbound_spam_policies["Policy1"].notify_sender_blocked is True
assert outbound_spam_policies["Policy1"].notify_limit_exceeded is True
assert outbound_spam_policies[
"Policy1"
].notify_limit_exceeded_addresses == ["security@example.com"]
assert outbound_spam_policies[
"Policy1"
].notify_sender_blocked_addresses == ["security@example.com"]
assert outbound_spam_policies["Policy1"].default is False
assert outbound_spam_policies["Policy2"].notify_sender_blocked is False
assert outbound_spam_policies["Policy2"].notify_limit_exceeded is False
assert (
outbound_spam_policies["Policy2"].notify_limit_exceeded_addresses == []
)
assert (
outbound_spam_policies["Policy2"].notify_sender_blocked_addresses == []
)
assert outbound_spam_policies["Policy2"].default is True
@patch(
"prowler.providers.m365.services.defender.defender_service.Defender._get_outbound_spam_filter_rule",
new=mock_defender_get_outbound_spam_filter_rule,
)
def test_get_outbound_spam_filter_rule(self):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
):
defender_client = Defender(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
outbound_spam_rules = defender_client.outbound_spam_rules
assert outbound_spam_rules["Policy1"].state == "Enabled"
assert outbound_spam_rules["Policy2"].state == "Disabled"