feat(exchange): add new check exchange_external_email_tagging_enabled (#7580)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2025-04-23 20:11:39 +02:00
committed by GitHub
parent 12c2439196
commit 8e3c856a14
10 changed files with 337 additions and 6 deletions
+1
View File
@@ -18,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add check for Bypass Disable in every Mailbox for service Defender in M365 [(#7418)](https://github.com/prowler-cloud/prowler/pull/7418)
- Add new check `teams_external_domains_restricted` [(#7557)](https://github.com/prowler-cloud/prowler/pull/7557)
- Add new check `teams_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533)
- Add new check for External Mails Tagged for service Exchange in M365 [(#7580)](https://github.com/prowler-cloud/prowler/pull/7580)
- Add new check for WhiteList not used in Transport Rules for service Defender in M365 [(#7569)](https://github.com/prowler-cloud/prowler/pull/7569)
- Add check for Inbound Antispam Policy with no allowed domains from service Defender in M365 [(#7500)](https://github.com/prowler-cloud/prowler/pull/7500)
- Add new check `teams_meeting_anonymous_user_join_disabled` [(#7565)](https://github.com/prowler-cloud/prowler/pull/7565)
@@ -342,6 +342,24 @@ class M365PowerShell(PowerShellSession):
"""
return self.execute("Get-MailboxAuditBypassAssociation | ConvertTo-Json")
def get_external_mail_config(self) -> dict:
"""
Get Exchange Online External Mail Configuration.
Retrieves the current external mail configuration settings for Exchange Online.
Returns:
dict: External mail configuration settings in JSON format.
Example:
>>> get_external_mail_config()
{
"Identity": "MyExternalMail",
"ExternalMailTagEnabled": true
}
"""
return self.execute("Get-ExternalInOutlook | ConvertTo-Json")
def get_transport_rules(self) -> dict:
"""
Get Exchange Online Transport Rules.
@@ -0,0 +1,30 @@
{
"Provider": "m365",
"CheckID": "exchange_external_email_tagging_enabled",
"CheckTitle": "Ensure email from external senders is identified.",
"CheckType": [],
"ServiceName": "exchange",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Exchange External Mail Tagging",
"Description": "Ensure that emails from external senders are identified using the native External tag experience in Outlook clients, which helps users recognize messages originating outside the organization.",
"Risk": "If external email tagging is not enabled, users may be unable to quickly identify emails coming from outside the organization, increasing the risk of phishing or social engineering attacks.",
"RelatedUrl": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-externalinoutlook?view=exchange-ps",
"Remediation": {
"Code": {
"CLI": "Set-ExternalInOutlook -Enabled $true",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable the External tag for Outlook to help users visually identify emails from outside the organization.",
"Url": "https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,41 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.exchange.exchange_client import exchange_client
class exchange_external_email_tagging_enabled(Check):
"""Ensure email from external senders is identified.
This check verifies that the native "External" sender tag feature is enabled
in Exchange so that messages from outside the organization are automatically marked.
"""
def execute(self) -> List[CheckReportM365]:
"""Run the check to validate that external sender tagging is enabled.
Iterates through the external mail configuration to determine if the
ExternalInOutlook setting is turned on and generates a report accordingly.
Returns:
List[CheckReportM365]: A list of reports for each organization identity.
"""
findings = []
for mail_config in exchange_client.external_mail_config:
report = CheckReportM365(
metadata=self.metadata(),
resource=mail_config,
resource_name=mail_config.identity,
resource_id=mail_config.identity,
)
report.status = "FAIL"
report.status_extended = f"External sender tagging is disabled for Exchange identity {mail_config.identity}."
if mail_config.external_mail_tag_enabled:
report.status = "PASS"
report.status_extended = f"External sender tagging is enabled for Exchange identity {mail_config.identity}."
findings.append(report)
return findings
@@ -13,6 +13,7 @@ class Exchange(M365Service):
self.powershell.connect_exchange_online()
self.organization_config = self._get_organization_config()
self.mailboxes_config = self._get_mailbox_audit_config()
self.external_mail_config = self._get_external_mail_config()
self.transport_rules = self._get_transport_rules()
self.powershell.close()
@@ -56,6 +57,31 @@ class Exchange(M365Service):
)
return mailboxes_config
def _get_external_mail_config(self):
logger.info("Microsoft365 - Getting external mail configuration...")
external_mail_config = []
try:
external_mail_configuration = self.powershell.get_external_mail_config()
if not external_mail_configuration:
return external_mail_config
if isinstance(external_mail_configuration, dict):
external_mail_configuration = [external_mail_configuration]
for external_mail in external_mail_configuration:
if external_mail:
external_mail_config.append(
ExternalMailConfig(
identity=external_mail.get("Identity", ""),
external_mail_tag_enabled=external_mail.get(
"Enabled", False
),
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return external_mail_config
def _get_transport_rules(self):
logger.info("Microsoft365 - Getting transport rules configuration...")
transport_rules = []
@@ -93,6 +119,11 @@ class MailboxAuditConfig(BaseModel):
audit_bypass_enabled: bool
class ExternalMailConfig(BaseModel):
identity: str
external_mail_tag_enabled: bool
class TransportRule(BaseModel):
name: str
scl: Optional[int]
@@ -35,12 +35,12 @@ class exchange_transport_rules_whitelist_disabled(Check):
report.status = "PASS"
report.status_extended = (
f"Transport rule '{rule.name}' does not whitelist any domains."
f"Transport rule {rule.name} does not whitelist any domains."
)
if rule.sender_domain_is and rule.scl == -1:
report.status = "FAIL"
report.status_extended = f"Transport rule '{rule.name}' whitelists domains: {', '.join(rule.sender_domain_is)}."
report.status_extended = f"Transport rule {rule.name} whitelists domains: {', '.join(rule.sender_domain_is)}."
findings.append(report)
@@ -0,0 +1,173 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_exchange_external_email_tagging_enabled:
def test_external_tagging_enabled(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_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.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import (
exchange_external_email_tagging_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
ExternalMailConfig,
)
exchange_client.external_mail_config = [
ExternalMailConfig(identity="Org1", external_mail_tag_enabled=True)
]
check = exchange_external_email_tagging_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "External sender tagging is enabled for Exchange identity Org1."
)
assert result[0].resource == exchange_client.external_mail_config[0].dict()
assert result[0].resource_name == "Org1"
assert result[0].resource_id == "Org1"
assert result[0].location == "global"
def test_external_tagging_disabled(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_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.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import (
exchange_external_email_tagging_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
ExternalMailConfig,
)
exchange_client.external_mail_config = [
ExternalMailConfig(identity="Org2", external_mail_tag_enabled=False)
]
check = exchange_external_email_tagging_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "External sender tagging is disabled for Exchange identity Org2."
)
assert result[0].resource == exchange_client.external_mail_config[0].dict()
assert result[0].resource_name == "Org2"
assert result[0].resource_id == "Org2"
assert result[0].location == "global"
def test_multiple_configs_mixed_status(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_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.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import (
exchange_external_email_tagging_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
ExternalMailConfig,
)
exchange_client.external_mail_config = [
ExternalMailConfig(
identity="OrgEnabled", external_mail_tag_enabled=True
),
ExternalMailConfig(
identity="OrgDisabled", external_mail_tag_enabled=False
),
]
check = exchange_external_email_tagging_enabled()
result = check.execute()
assert len(result) == 2
assert result[0].status == "PASS"
assert result[0].resource_name == "OrgEnabled"
assert (
result[0].status_extended
== "External sender tagging is enabled for Exchange identity OrgEnabled."
)
assert result[1].status == "FAIL"
assert result[1].resource_name == "OrgDisabled"
assert (
result[1].status_extended
== "External sender tagging is disabled for Exchange identity OrgDisabled."
)
def test_no_mail_configs(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_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.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_external_email_tagging_enabled.exchange_external_email_tagging_enabled import (
exchange_external_email_tagging_enabled,
)
exchange_client.external_mail_config = []
check = exchange_external_email_tagging_enabled()
result = check.execute()
assert len(result) == 0
@@ -4,6 +4,7 @@ from unittest.mock import patch
from prowler.providers.m365.models import M365IdentityInfo
from prowler.providers.m365.services.exchange.exchange_service import (
Exchange,
ExternalMailConfig,
MailboxAuditConfig,
Organization,
TransportRule,
@@ -22,6 +23,19 @@ def mock_exchange_get_mailbox_audit_config(_):
]
def mock_exchange_get_external_mail_config(_):
return [
ExternalMailConfig(
identity="test",
external_mail_tag_enabled=True,
),
ExternalMailConfig(
identity="test2",
external_mail_tag_enabled=False,
),
]
def mock_exchange_get_transport_rules(_):
return [
TransportRule(
@@ -101,6 +115,29 @@ class Test_Exchange_Service:
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_external_mail_config",
new=mock_exchange_get_external_mail_config,
)
def test_get_external_mail_config(self):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
):
exchange_client = Exchange(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
external_mail_config = exchange_client.external_mail_config
assert len(external_mail_config) == 2
assert external_mail_config[0].identity == "test"
assert external_mail_config[0].external_mail_tag_enabled is True
assert external_mail_config[1].identity == "test2"
assert external_mail_config[1].external_mail_tag_enabled is False
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_transport_rules",
new=mock_exchange_get_transport_rules,
@@ -41,14 +41,14 @@ class Test_exchange_transport_rules_whitelist_disabled:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Transport rule 'Rule1' does not whitelist any domains."
== "Transport rule Rule1 does not whitelist any domains."
)
assert result[0].resource_name == "Rule1"
assert result[0].resource_id == "ExchangeTransportRule"
assert result[1].status == "PASS"
assert (
result[1].status_extended
== "Transport rule 'Rule2' does not whitelist any domains."
== "Transport rule Rule2 does not whitelist any domains."
)
assert result[1].resource_name == "Rule2"
@@ -91,7 +91,7 @@ class Test_exchange_transport_rules_whitelist_disabled:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Transport rule 'WhitelistRule' whitelists domains: whitelist.com."
== "Transport rule WhitelistRule whitelists domains: whitelist.com."
)
assert result[0].resource_name == "WhitelistRule"
assert result[0].resource_id == "ExchangeTransportRule"
@@ -99,7 +99,7 @@ class Test_exchange_transport_rules_whitelist_disabled:
assert result[1].status == "PASS"
assert (
result[1].status_extended
== "Transport rule 'NoWhitelistRule' does not whitelist any domains."
== "Transport rule NoWhitelistRule does not whitelist any domains."
)
assert result[1].resource_name == "NoWhitelistRule"
assert result[1].resource_id == "ExchangeTransportRule"