feat(m365): add exchange_mailbox_primary_smtp_custom_domain check (#11215)

Co-authored-by: Jasmine Sullivan <20147180@tafe.wa.edu.au>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Jasmine
2026-06-09 22:24:25 +08:00
committed by GitHub
parent b3caee88e4
commit e710ebff1c
8 changed files with 617 additions and 0 deletions
@@ -0,0 +1,300 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_exchange_mailbox_primary_smtp_uses_custom_domain:
def test_powershell_unavailable_manual(self):
"""MANUAL: Exchange Online PowerShell unavailable (mailboxes is None)."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
exchange_client.mailboxes = None
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert "PowerShell" in result[0].status_extended
assert result[0].resource_name == "Exchange Online Mailboxes"
assert result[0].resource_id == "exchange_mailboxes"
def test_empty_tenant_no_findings(self):
"""Empty tenant (no mailboxes) produces zero findings, not MANUAL."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
exchange_client.mailboxes = []
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert result == []
def test_custom_domain_passes(self):
"""PASS: Mailbox primary SMTP uses a custom domain."""
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Mailbox,
)
exchange_client.mailboxes = [
Mailbox(
identity="user1@contoso.com",
name="User One",
primary_smtp_address="user1@contoso.com",
recipient_type_details="UserMailbox",
)
]
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "custom domain" in result[0].status_extended
assert result[0].resource_name == "User One"
assert result[0].resource_id == "user1@contoso.com"
assert result[0].location == "global"
def test_onmicrosoft_domain_fails(self):
"""FAIL: Mailbox primary SMTP uses .onmicrosoft.com."""
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Mailbox,
)
exchange_client.mailboxes = [
Mailbox(
identity="user1@contoso.onmicrosoft.com",
name="User One",
primary_smtp_address="user1@contoso.onmicrosoft.com",
recipient_type_details="UserMailbox",
)
]
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert ".onmicrosoft.com" in result[0].status_extended
assert result[0].resource_name == "User One"
assert result[0].resource_id == "user1@contoso.onmicrosoft.com"
assert result[0].location == "global"
def test_mixed_mailboxes(self):
"""Test multiple mailboxes with mixed domain status."""
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Mailbox,
)
exchange_client.mailboxes = [
Mailbox(
identity="user1@contoso.com",
name="User One",
primary_smtp_address="user1@contoso.com",
recipient_type_details="UserMailbox",
),
Mailbox(
identity="shared@contoso.onmicrosoft.com",
name="Shared Mailbox",
primary_smtp_address="shared@contoso.onmicrosoft.com",
recipient_type_details="SharedMailbox",
),
]
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert len(result) == 2
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Mailbox user1@contoso.com (UserMailbox) has primary SMTP address user1@contoso.com using a custom domain."
)
assert result[1].status == "FAIL"
assert (
result[1].status_extended
== "Mailbox shared@contoso.onmicrosoft.com (SharedMailbox) has primary SMTP address shared@contoso.onmicrosoft.com using the .onmicrosoft.com domain instead of a custom domain."
)
def test_room_mailbox_custom_domain(self):
"""PASS: Room mailbox using a custom domain."""
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Mailbox,
)
exchange_client.mailboxes = [
Mailbox(
identity="boardroom@contoso.com",
name="Board Room",
primary_smtp_address="boardroom@contoso.com",
recipient_type_details="RoomMailbox",
)
]
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_id == "boardroom@contoso.com"
assert result[0].location == "global"
def test_equipment_mailbox_onmicrosoft(self):
"""FAIL: Equipment mailbox using .onmicrosoft.com."""
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_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_primary_smtp_uses_custom_domain.exchange_mailbox_primary_smtp_uses_custom_domain import (
exchange_mailbox_primary_smtp_uses_custom_domain,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Mailbox,
)
exchange_client.mailboxes = [
Mailbox(
identity="projector@contoso.onmicrosoft.com",
name="Projector",
primary_smtp_address="projector@contoso.onmicrosoft.com",
recipient_type_details="EquipmentMailbox",
)
]
check = exchange_mailbox_primary_smtp_uses_custom_domain()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_id == "projector@contoso.onmicrosoft.com"
assert result[0].location == "global"
@@ -495,6 +495,104 @@ class Test_Exchange_Service:
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_mailboxes",
return_value=[
{
"Identity": "user1@contoso.com",
"DisplayName": "User One",
"PrimarySmtpAddress": "user1@contoso.com",
"RecipientTypeDetails": "UserMailbox",
},
{
"Identity": "room@contoso.com",
"DisplayName": "Boardroom",
"PrimarySmtpAddress": "room@contoso.com",
"RecipientTypeDetails": "RoomMailbox",
},
{
"Identity": "DiscoverySearchMailbox{D919BA05}",
"DisplayName": "Discovery Search Mailbox",
"PrimarySmtpAddress": "DiscoverySearchMailbox@contoso.onmicrosoft.com",
"RecipientTypeDetails": "DiscoveryMailbox",
},
{
"Identity": "SystemMailbox{1f05a927}",
"DisplayName": "Microsoft Exchange",
"PrimarySmtpAddress": "SystemMailbox@contoso.onmicrosoft.com",
"RecipientTypeDetails": "SystemMailbox",
},
],
)
def test_get_mailboxes_excludes_system_types(self, _mock_get_mailboxes):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
return_value=True,
),
):
exchange_client = Exchange(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
mailboxes = exchange_client.mailboxes
assert mailboxes is not None
assert len(mailboxes) == 2
identities = {m.identity for m in mailboxes}
assert identities == {"user1@contoso.com", "room@contoso.com"}
assert all(
m.recipient_type_details not in {"DiscoveryMailbox", "SystemMailbox"}
for m in mailboxes
)
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_mailboxes",
return_value={
"Identity": "user1@contoso.com",
"DisplayName": "User One",
"PrimarySmtpAddress": "user1@contoso.com",
"RecipientTypeDetails": "UserMailbox",
},
)
def test_get_mailboxes_single_dict(self, _mock_get_mailboxes):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
return_value=True,
),
):
exchange_client = Exchange(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
mailboxes = exchange_client.mailboxes
assert mailboxes is not None
assert len(mailboxes) == 1
assert mailboxes[0].identity == "user1@contoso.com"
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_mailboxes",
side_effect=Exception("Get-EXOMailbox failed"),
)
def test_get_mailboxes_returns_none_on_exception(self, _mock_get_mailboxes):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
return_value=True,
),
):
exchange_client = Exchange(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
assert exchange_client.mailboxes is None
exchange_client.powershell.close()
def test_get_total_paid_licenses_none(self):
with (
mock.patch(