feat(m365): add CIS M365 v7.0.0 exchange checks (#12149)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Pedro Martín
2026-08-04 13:37:28 +02:00
committed by GitHub
co-authored by Daniel Barranquero
parent 5cf49805a2
commit 90905dcc9f
14 changed files with 766 additions and 3 deletions
@@ -0,0 +1 @@
Spurious error log output from `Get-ApplicationAccessPolicy` on M365 tenants without application access policies
@@ -0,0 +1 @@
`exchange_owa_mailbox_policy_personal_accounts_disabled` and `exchange_organization_reject_direct_send_enabled` checks for M365 provider, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 controls 6.3.2 and 6.5.5
+6 -2
View File
@@ -2577,7 +2577,9 @@
{
"Id": "6.3.2",
"Description": "Outlook on the web (OWA) mailbox policies include two settings that control personal account integration in Outlook. PersonalAccountsEnabled controls whether users can add personal email accounts (e.g., Outlook.com, Gmail, Yahoo) in the new Outlook for Windows. PersonalAccountCalendarsEnabled controls whether users can connect personal Outlook.com or Google calendars in Outlook on the web. Neither setting applies to classic Outlook for Windows, Outlook for Mac, or Outlook mobile apps. The recommended state for the default OWA Mailbox Policy is: - PersonalAccountsEnabled is set to False - PersonalAccountCalendarsEnabled is set to False",
"Checks": [],
"Checks": [
"exchange_owa_mailbox_policy_personal_accounts_disabled"
],
"Attributes": [
{
"Section": "6 Exchange admin center",
@@ -2698,7 +2700,9 @@
{
"Id": "6.5.5",
"Description": "Direct Send is a method used to send emails directly to an Exchange Online customer's hosted mailboxes from on-premises devices, applications, or third-party cloud services using the customer's own accepted domain. This method does not require any form of authentication because, by its nature, it mimics incoming anonymous emails from the internet, apart from the sender domain. The recommended state is to configure RejectDirectSend to True.",
"Checks": [],
"Checks": [
"exchange_organization_reject_direct_send_enabled"
],
"Attributes": [
{
"Section": "6 Exchange admin center",
@@ -1055,8 +1055,11 @@ class M365PowerShell(PowerShellSession):
}
]
"""
# -ErrorAction SilentlyContinue: tenants with no application access
# policies raise a localized "object not found" error instead of
# returning an empty result; the error output never carries data.
return self.execute(
"Get-ApplicationAccessPolicy | ConvertTo-Json -Depth 10",
"Get-ApplicationAccessPolicy -ErrorAction SilentlyContinue | ConvertTo-Json -Depth 10",
json_parse=True,
)
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "exchange_organization_reject_direct_send_enabled",
"CheckTitle": "Direct Send is rejected for the Exchange Online organization",
"CheckType": [],
"ServiceName": "exchange",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "The Exchange Online organization configuration should have **RejectDirectSend** enabled. Direct Send allows on-premises devices, applications, or third-party cloud services to send email to the tenant's hosted mailboxes using one of the tenant's accepted domains without authentication, which can be abused to spoof internal senders.",
"Risk": "When **Direct Send** is allowed, anyone who knows a valid recipient and accepted domain can send unauthenticated email that appears to originate from inside the organization, enabling **phishing** and **business email compromise** against employees.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365#option-3-configure-a-connector-to-send-mail-using-microsoft-365-or-office-365-smtp-relay"
],
"Remediation": {
"Code": {
"CLI": "Set-OrganizationConfig -RejectDirectSend $true",
"NativeIaC": "",
"Other": "1. Connect to Exchange Online PowerShell using Connect-ExchangeOnline\n2. Run: Set-OrganizationConfig -RejectDirectSend $true\n3. Ensure legitimate senders use an authenticated connector or SMTP AUTH before enabling",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable RejectDirectSend so that unauthenticated Direct Send email to the tenant's accepted domains is rejected. Migrate any legitimate on-premises or application senders to authenticated connectors first.",
"Url": "https://hub.prowler.com/check/exchange_organization_reject_direct_send_enabled"
}
},
"Categories": [
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,47 @@
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_organization_reject_direct_send_enabled(Check):
"""Check if Direct Send is rejected in the Exchange Online organization.
Direct Send lets on-premises devices, applications, or third-party services
send email to the tenant's hosted mailboxes using an accepted domain without
authentication. Rejecting Direct Send reduces the risk of spoofed internal
email.
- PASS: RejectDirectSend is enabled for the organization.
- FAIL: RejectDirectSend is disabled for the organization.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for the Direct Send organization setting.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
organization_config = exchange_client.organization_config
if organization_config:
report = CheckReportM365(
metadata=self.metadata(),
resource=organization_config,
resource_name=organization_config.name,
resource_id=organization_config.guid,
)
report.status = "FAIL"
report.status_extended = (
"Direct Send is not rejected for the Exchange Online organization."
)
if organization_config.reject_direct_send:
report.status = "PASS"
report.status_extended = (
"Direct Send is rejected for the Exchange Online organization."
)
findings.append(report)
return findings
@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "exchange_owa_mailbox_policy_personal_accounts_disabled",
"CheckTitle": "Default OWA mailbox policy disables personal account integration",
"CheckType": [],
"ServiceName": "exchange",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "The default Outlook on the web (OWA) mailbox policy should have **PersonalAccountsEnabled** and **PersonalAccountCalendarsEnabled** set to false. The former controls adding personal email accounts (e.g., Outlook.com, Gmail) in the new Outlook for Windows; the latter controls connecting personal Outlook.com or Google calendars in Outlook on the web.",
"Risk": "Allowing personal account integration lets corporate and personal data mix within the same client, increasing the risk of data leakage and reducing the organization's ability to govern and audit access to corporate mail and calendars.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/powershell/module/exchange/set-owamailboxpolicy"
],
"Remediation": {
"Code": {
"CLI": "Set-OwaMailboxPolicy -Identity OwaMailboxPolicy-Default -PersonalAccountsEnabled $false -PersonalAccountCalendarsEnabled $false",
"NativeIaC": "",
"Other": "1. Connect to Exchange Online PowerShell using Connect-ExchangeOnline\n2. Run: Set-OwaMailboxPolicy -Identity OwaMailboxPolicy-Default -PersonalAccountsEnabled $false -PersonalAccountCalendarsEnabled $false",
"Terraform": ""
},
"Recommendation": {
"Text": "Disable PersonalAccountsEnabled and PersonalAccountCalendarsEnabled on the default OWA mailbox policy to prevent users from adding personal email accounts in the new Outlook for Windows and connecting personal calendars in Outlook on the web.",
"Url": "https://hub.prowler.com/check/exchange_owa_mailbox_policy_personal_accounts_disabled"
}
},
"Categories": [
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,54 @@
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_owa_mailbox_policy_personal_accounts_disabled(Check):
"""Check if the default OWA mailbox policy disables personal account integration.
Outlook on the web mailbox policies expose PersonalAccountsEnabled, which
controls whether users can add personal email accounts in the new Outlook for
Windows, and PersonalAccountCalendarsEnabled, which controls whether users can
connect personal Outlook.com or Google calendars in Outlook on the web. Only
the default OWA mailbox policy is required for compliance with this control.
- PASS: The default OWA mailbox policy disables personal accounts and personal
account calendars.
- FAIL: The default OWA mailbox policy allows personal accounts and/or personal
account calendars.
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for the default OWA mailbox policy personal account settings.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
for mailbox_policy in exchange_client.mailbox_policies:
if not mailbox_policy or not mailbox_policy.is_default:
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=mailbox_policy,
resource_name=f"Exchange Mailbox Policy - {mailbox_policy.id}",
resource_id=mailbox_policy.id,
)
report.status = "PASS"
report.status_extended = f"Default OWA mailbox policy '{mailbox_policy.id}' disables personal account integration."
allowed_settings = []
if mailbox_policy.personal_accounts_enabled:
allowed_settings.append("personal accounts")
if mailbox_policy.personal_account_calendars_enabled:
allowed_settings.append("personal account calendars")
if allowed_settings:
report.status = "FAIL"
report.status_extended = f"Default OWA mailbox policy '{mailbox_policy.id}' allows {' and '.join(allowed_settings)}."
findings.append(report)
return findings
@@ -108,6 +108,16 @@ class Exchange(M365Service):
return None
def _get_organization_config(self):
"""Retrieve the Exchange Online organization configuration.
Reads Get-OrganizationConfig via Exchange Online PowerShell. Boolean
properties that can come back null (never configured) are normalized to
their platform defaults, e.g. RejectDirectSend to False.
Returns:
Optional[Organization]: The parsed organization configuration, or
None when unavailable or on error.
"""
logger.info("Microsoft365 - Getting Exchange Organization configuration...")
organization_config = None
try:
@@ -137,6 +147,12 @@ class Exchange(M365Service):
delayed_delicensing_enabled=organization_configuration.get(
"DelayedDelicensingEnabled", False
),
# Can be null on tenants where the setting was never
# configured; null keeps the platform default (disabled).
reject_direct_send=organization_configuration.get(
"RejectDirectSend"
)
is True,
)
except Exception as error:
logger.error(
@@ -241,6 +257,15 @@ class Exchange(M365Service):
return transport_config
def _get_mailbox_policy(self):
"""Retrieve the OWA mailbox policies.
Reads Get-OwaMailboxPolicy via Exchange Online PowerShell. The personal
account properties can come back null (never configured) and are
normalized to their platform defaults (enabled).
Returns:
List[MailboxPolicy]: The parsed OWA mailbox policies, empty on error.
"""
logger.info("Microsoft365 - Getting mailbox policy configuration...")
mailbox_policies = []
try:
@@ -256,6 +281,18 @@ class Exchange(M365Service):
additional_storage_enabled=policy.get(
"AdditionalStorageProvidersAvailable", True
),
# These properties can be null on tenants where the
# setting was never configured; null keeps the
# platform default.
is_default=policy.get("IsDefault") is True,
personal_accounts_enabled=policy.get(
"PersonalAccountsEnabled"
)
is not False,
personal_account_calendars_enabled=policy.get(
"PersonalAccountCalendarsEnabled"
)
is not False,
)
)
except Exception as error:
@@ -489,6 +526,7 @@ class Organization(BaseModel):
mailtips_large_audience_threshold: int
delayed_delicensing_enabled: bool = False
total_paid_licenses: Optional[int] = None
reject_direct_send: bool = False
class MailboxAuditConfig(BaseModel):
@@ -516,6 +554,9 @@ class TransportConfig(BaseModel):
class MailboxPolicy(BaseModel):
id: str
additional_storage_enabled: bool
is_default: bool = False
personal_accounts_enabled: bool = True
personal_account_calendars_enabled: bool = True
class RoleAssignmentPolicy(BaseModel):
@@ -0,0 +1,132 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_exchange_organization_reject_direct_send_enabled:
def test_exchange_no_organization_config(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
exchange_client.organization_config = 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_organization_reject_direct_send_enabled.exchange_organization_reject_direct_send_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_reject_direct_send_enabled.exchange_organization_reject_direct_send_enabled import (
exchange_organization_reject_direct_send_enabled,
)
check = exchange_organization_reject_direct_send_enabled()
result = check.execute()
assert len(result) == 0
def test_exchange_reject_direct_send_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_organization_reject_direct_send_enabled.exchange_organization_reject_direct_send_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_reject_direct_send_enabled.exchange_organization_reject_direct_send_enabled import (
exchange_organization_reject_direct_send_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
name="test-org",
guid="org-guid",
audit_disabled=False,
oauth_enabled=True,
mailtips_enabled=True,
mailtips_external_recipient_enabled=True,
mailtips_group_metrics_enabled=True,
mailtips_large_audience_threshold=25,
reject_direct_send=True,
)
check = exchange_organization_reject_direct_send_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Direct Send is rejected for the Exchange Online organization."
)
assert result[0].resource == exchange_client.organization_config.dict()
assert result[0].resource_name == "test-org"
assert result[0].resource_id == "org-guid"
assert result[0].location == "global"
def test_exchange_reject_direct_send_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_organization_reject_direct_send_enabled.exchange_organization_reject_direct_send_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_reject_direct_send_enabled.exchange_organization_reject_direct_send_enabled import (
exchange_organization_reject_direct_send_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
name="test-org",
guid="org-guid",
audit_disabled=False,
oauth_enabled=True,
mailtips_enabled=True,
mailtips_external_recipient_enabled=True,
mailtips_group_metrics_enabled=True,
mailtips_large_audience_threshold=25,
reject_direct_send=False,
)
check = exchange_organization_reject_direct_send_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Direct Send is not rejected for the Exchange Online organization."
)
assert result[0].resource == exchange_client.organization_config.dict()
assert result[0].resource_name == "test-org"
assert result[0].resource_id == "org-guid"
assert result[0].location == "global"
@@ -0,0 +1,311 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_exchange_owa_mailbox_policy_personal_accounts_disabled:
def test_exchange_no_mailbox_policies(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
exchange_client.mailbox_policies = []
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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 0
def test_exchange_non_default_policy_ignored(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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
MailboxPolicy,
)
# A non-default policy that is non-compliant must be ignored.
exchange_client.mailbox_policies = [
MailboxPolicy(
id="OwaMailboxPolicy-Custom",
additional_storage_enabled=False,
is_default=False,
personal_accounts_enabled=True,
personal_account_calendars_enabled=True,
)
]
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 0
def test_exchange_default_policy_personal_accounts_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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
MailboxPolicy,
)
exchange_client.mailbox_policies = [
MailboxPolicy(
id="OwaMailboxPolicy-Default",
additional_storage_enabled=False,
is_default=True,
personal_accounts_enabled=False,
personal_account_calendars_enabled=False,
)
]
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Default OWA mailbox policy 'OwaMailboxPolicy-Default' disables personal account integration."
)
assert result[0].resource == exchange_client.mailbox_policies[0].dict()
assert (
result[0].resource_name
== "Exchange Mailbox Policy - OwaMailboxPolicy-Default"
)
assert result[0].resource_id == "OwaMailboxPolicy-Default"
assert result[0].location == "global"
def test_exchange_default_policy_personal_accounts_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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
MailboxPolicy,
)
exchange_client.mailbox_policies = [
MailboxPolicy(
id="OwaMailboxPolicy-Default",
additional_storage_enabled=False,
is_default=True,
personal_accounts_enabled=True,
personal_account_calendars_enabled=True,
)
]
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Default OWA mailbox policy 'OwaMailboxPolicy-Default' allows personal accounts and personal account calendars."
)
assert result[0].resource == exchange_client.mailbox_policies[0].dict()
assert (
result[0].resource_name
== "Exchange Mailbox Policy - OwaMailboxPolicy-Default"
)
assert result[0].resource_id == "OwaMailboxPolicy-Default"
assert result[0].location == "global"
def test_exchange_default_policy_only_personal_accounts_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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
MailboxPolicy,
)
exchange_client.mailbox_policies = [
MailboxPolicy(
id="OwaMailboxPolicy-Default",
additional_storage_enabled=False,
is_default=True,
personal_accounts_enabled=True,
personal_account_calendars_enabled=False,
)
]
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Default OWA mailbox policy 'OwaMailboxPolicy-Default' allows personal accounts."
)
def test_exchange_default_policy_only_personal_calendars_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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
MailboxPolicy,
)
exchange_client.mailbox_policies = [
MailboxPolicy(
id="OwaMailboxPolicy-Default",
additional_storage_enabled=False,
is_default=True,
personal_accounts_enabled=False,
personal_account_calendars_enabled=True,
)
]
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Default OWA mailbox policy 'OwaMailboxPolicy-Default' allows personal account calendars."
)
def test_exchange_default_and_custom_policies(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_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_owa_mailbox_policy_personal_accounts_disabled.exchange_owa_mailbox_policy_personal_accounts_disabled import (
exchange_owa_mailbox_policy_personal_accounts_disabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
MailboxPolicy,
)
# Only the default policy must produce a finding.
exchange_client.mailbox_policies = [
MailboxPolicy(
id="OwaMailboxPolicy-Custom",
additional_storage_enabled=False,
is_default=False,
personal_accounts_enabled=True,
personal_account_calendars_enabled=True,
),
MailboxPolicy(
id="OwaMailboxPolicy-Default",
additional_storage_enabled=False,
is_default=True,
personal_accounts_enabled=False,
personal_account_calendars_enabled=False,
),
]
check = exchange_owa_mailbox_policy_personal_accounts_disabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_id == "OwaMailboxPolicy-Default"
@@ -26,6 +26,7 @@ def mock_exchange_get_organization_config(_):
mailtips_external_recipient_enabled=False,
mailtips_group_metrics_enabled=True,
mailtips_large_audience_threshold=25,
reject_direct_send=True,
)
@@ -214,9 +215,62 @@ class Test_Exchange_Service:
assert organization_config.mailtips_group_metrics_enabled is True
assert organization_config.mailtips_large_audience_threshold == 25
assert organization_config.total_paid_licenses == 6000
assert organization_config.reject_direct_send is True
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_organization_config",
return_value={
"Name": "test-org",
"Guid": "org-guid",
"RejectDirectSend": True,
},
)
def test_get_organization_config_reject_direct_send(
self, _mock_get_organization_config
):
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.organization_config.reject_direct_send is True
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_organization_config",
return_value={
"Name": "test-org",
"Guid": "org-guid",
"RejectDirectSend": None,
},
)
def test_get_organization_config_reject_direct_send_null(
self, _mock_get_organization_config
):
# Null means the setting was never configured; it must keep the
# platform default (disabled).
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.organization_config.reject_direct_send is False
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_mailbox_audit_config",
new=mock_exchange_get_mailbox_audit_config,
@@ -300,6 +354,9 @@ class Test_Exchange_Service:
{
"Id": "test",
"AdditionalStorageProvidersAvailable": True,
"IsDefault": True,
"PersonalAccountsEnabled": False,
"PersonalAccountCalendarsEnabled": False,
}
],
)
@@ -319,6 +376,9 @@ class Test_Exchange_Service:
assert len(mailbox_policies) == 1
assert mailbox_policies[0].id == "test"
assert mailbox_policies[0].additional_storage_enabled is True
assert mailbox_policies[0].is_default is True
assert mailbox_policies[0].personal_accounts_enabled is False
assert mailbox_policies[0].personal_account_calendars_enabled is False
exchange_client.powershell.close()
@patch(
@@ -344,6 +404,41 @@ class Test_Exchange_Service:
assert len(mailbox_policies) == 1
assert mailbox_policies[0].id == "test_single"
assert mailbox_policies[0].additional_storage_enabled is False
assert mailbox_policies[0].is_default is False
assert mailbox_policies[0].personal_accounts_enabled is True
assert mailbox_policies[0].personal_account_calendars_enabled is True
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.get_mailbox_policy",
return_value={
"Id": "OwaMailboxPolicy-Default",
"AdditionalStorageProvidersAvailable": True,
"IsDefault": True,
"PersonalAccountsEnabled": None,
"PersonalAccountCalendarsEnabled": None,
},
)
def test_get_mailbox_policy_null_personal_accounts(self, _mock_get_mailbox_policy):
# Tenants where the personal accounts settings were never configured
# return null; null must keep the platform default (enabled).
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)
)
)
mailbox_policies = exchange_client.mailbox_policies
assert len(mailbox_policies) == 1
assert mailbox_policies[0].id == "OwaMailboxPolicy-Default"
assert mailbox_policies[0].is_default is True
assert mailbox_policies[0].personal_accounts_enabled is True
assert mailbox_policies[0].personal_account_calendars_enabled is True
exchange_client.powershell.close()
@patch(