feat(exchange): add new check exchange_mailbox_properties_auditing_e3_enabled (#7642)

Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
Daniel Barranquero
2025-05-02 10:48:30 +02:00
committed by Pepe Fagoaga
parent 5d4191a7fc
commit dbffcedc49
8 changed files with 623 additions and 0 deletions
+1
View File
@@ -38,6 +38,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
- Add new check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
- Add new check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
- Add new check for Auditing Mailbox on E3 users is enabled for Exchange in M365 [(#7642)](https://github.com/prowler-cloud/prowler/pull/7642)
- Add new check for SMTP Auth disabled for Exchange in M365 [(#7640)](https://github.com/prowler-cloud/prowler/pull/7640)
- Add new check for MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637)
@@ -487,6 +487,72 @@ class M365PowerShell(PowerShellSession):
"Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True
)
def get_mailbox_audit_properties(self) -> dict:
"""
Get Mailbox Properties.
Retrieves the properties of all mailboxes in the organization in Exchange Online.
Args:
mailbox (str): The email address or identifier of the mailbox.
Returns:
dict: Mailbox properties in JSON format.
Example:
>>> get_mailbox_properties()
{
"UserPrincipalName": "User1",
"AuditEnabled": "false"
"AuditAdmin": [
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"SendAs",
"SendOnBehalf",
"Create",
"UpdateFolderPermissions",
"UpdateInboxRules",
"UpdateCalendarDelegation",
"ApplyRecord",
"MailItemsAccessed",
"Send"
],
"AuditDelegate": [
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"SendAs",
"SendOnBehalf",
"Create",
"UpdateFolderPermissions",
"UpdateInboxRules",
"ApplyRecord",
"MailItemsAccessed"
],
"AuditOwner": [
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"UpdateFolderPermissions",
"UpdateInboxRules",
"UpdateCalendarDelegation",
"ApplyRecord",
"MailItemsAccessed",
"Send"
],
"AuditLogAgeLimit": "90",
"Identity": "User1",
}
"""
return self.execute(
"Get-EXOMailbox -PropertySets Audit -ResultSize Unlimited | ConvertTo-Json",
json_parse=True,
)
def get_transport_config(self) -> dict:
"""
Get Exchange Online Transport Configuration.
@@ -0,0 +1,30 @@
{
"Provider": "m365",
"CheckID": "exchange_mailbox_properties_auditing_e3_enabled",
"CheckTitle": "Ensure mailbox auditing for E3 users is enabled.",
"CheckType": [],
"ServiceName": "exchange",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Exchange Mailboxes Properties",
"Description": "Ensure mailbox auditing is enabled for all E3-licensed user mailboxes, including the configuration of audit actions for owners, delegates, and admins beyond the Microsoft defaults.",
"Risk": "If auditing is not properly enabled and configured, critical mailbox actions may go unrecorded, reducing the ability to investigate incidents, enforce compliance, or detect malicious behavior.",
"RelatedUrl": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide",
"Remediation": {
"Code": {
"CLI": "$AuditAdmin = @(\"ApplyRecord\", \"Copy\", \"Create\", \"FolderBind\", \"HardDelete\", \"Move\", \"MoveToDeletedItems\", \"SendAs\", \"SendOnBehalf\", \"SoftDelete\", \"Update\", \"UpdateCalendarDelegation\", \"UpdateFolderPermissions\", \"UpdateInboxRules\"); $AuditDelegate = @(\"ApplyRecord\", \"Create\", \"FolderBind\", \"HardDelete\", \"Move\", \"MoveToDeletedItems\", \"SendAs\", \"SendOnBehalf\", \"SoftDelete\", \"Update\", \"UpdateFolderPermissions\", \"UpdateInboxRules\"); $AuditOwner = @(\"ApplyRecord\", \"Create\", \"HardDelete\", \"MailboxLogin\", \"Move\", \"MoveToDeletedItems\", \"SoftDelete\", \"Update\", \"UpdateCalendarDelegation\", \"UpdateFolderPermissions\", \"UpdateInboxRules\"); $MBX = Get-EXOMailbox -ResultSize Unlimited | Where-Object { $_.RecipientTypeDetails -eq \"UserMailbox\" }; $MBX | Set-Mailbox -AuditEnabled $true -AuditLogAgeLimit 90 -AuditAdmin $AuditAdmin -AuditDelegate $AuditDelegate -AuditOwner $AuditOwner",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable mailbox auditing for all E3 user mailboxes and configure auditing for key mailbox actions for owners, delegates, and admins.",
"Url": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,74 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.exchange.exchange_client import exchange_client
from prowler.providers.m365.services.exchange.exchange_service import (
AuditAdmin,
AuditDelegate,
AuditOwner,
)
class exchange_mailbox_properties_auditing_e3_enabled(Check):
"""
Check to ensure that mailbox auditing properties are enabled and properly configured.
Attributes:
metadata: Metadata associated with the check (inherited from Check).
"""
def execute(self) -> List[CheckReportM365]:
"""
Execute the check to validate that mailbox auditing properties are enabled and properly configured.
This method retrieves all mailbox audit properties from the Exchange service and evaluates
whether auditing is enabled and correctly configured for each mailbox. A report is generated
for each mailbox.
Returns:
List[CheckReportM365]: A list of findings with the status of each mailbox.
"""
findings = []
required_admin = {e.value for e in AuditAdmin}
required_delegate = {e.value for e in AuditDelegate}
required_owner = {e.value for e in AuditOwner}
for mailbox in exchange_client.mailbox_audit_properties:
report = CheckReportM365(
metadata=self.metadata(),
resource=mailbox,
resource_name=mailbox.name,
resource_id=mailbox.identity,
)
report.status = "FAIL"
report.status_extended = (
f"Mailbox Audit Properties for Mailbox {mailbox.name} is not enabled."
)
if mailbox.audit_enabled:
audit_admin = set(mailbox.audit_admin or [])
audit_delegate = set(mailbox.audit_delegate or [])
audit_owner = set(mailbox.audit_owner or [])
if (
required_admin.issubset(audit_admin)
and required_delegate.issubset(audit_delegate)
and required_owner.issubset(audit_owner)
):
# The limit for E3 is 90 days, but we check >= 90 because E5 users can set it to more than 90 days
if mailbox.audit_log_age >= 90:
report.status = "PASS"
report.status_extended = f"Mailbox Audit Properties for Mailbox {mailbox.name} is enabled and properly configured."
else:
report.status_extended = f"Mailbox Audit Properties for Mailbox {mailbox.name} is enabled and properly configured but the audit log age is less than 90 days."
else:
report.status_extended = (
f"Mailbox Audit Properties for Mailbox {mailbox.name} is enabled but not properly configured. "
f"Missing audit actions may exist."
)
findings.append(report)
return findings
@@ -1,3 +1,4 @@
from enum import Enum
from typing import Optional
from pydantic import BaseModel
@@ -16,6 +17,7 @@ class Exchange(M365Service):
self.transport_rules = []
self.transport_config = None
self.mailbox_policy = None
self.mailbox_audit_properties = []
if self.powershell:
self.powershell.connect_exchange_online()
@@ -25,6 +27,7 @@ class Exchange(M365Service):
self.transport_rules = self._get_transport_rules()
self.transport_config = self._get_transport_config()
self.mailbox_policy = self._get_mailbox_policy()
self.mailbox_audit_properties = self._get_mailbox_audit_properties()
self.powershell.close()
def _get_organization_config(self):
@@ -163,6 +166,44 @@ class Exchange(M365Service):
)
return mailboxes_policy
def _get_mailbox_audit_properties(self):
logger.info("Microsoft365 - Getting mailbox audit properties...")
mailbox_audit_properties = []
try:
mailbox_audit_properties_info = (
self.powershell.get_mailbox_audit_properties()
)
if not mailbox_audit_properties_info:
return mailbox_audit_properties
if isinstance(mailbox_audit_properties_info, dict):
mailbox_audit_properties_info = [mailbox_audit_properties_info]
for mailbox_audit_property in mailbox_audit_properties_info:
if mailbox_audit_property:
mailbox_audit_properties.append(
MailboxAuditProperties(
name=mailbox_audit_property.get("UserPrincipalName", ""),
audit_enabled=mailbox_audit_property.get(
"AuditEnabled", False
),
audit_admin=mailbox_audit_property.get("AuditAdmin", []),
audit_delegate=mailbox_audit_property.get(
"AuditDelegate", []
),
audit_owner=mailbox_audit_property.get("AuditOwner", []),
audit_log_age=int(
mailbox_audit_property.get(
"AuditLogAgeLimit", "90.00:00:00"
).split(".")[0]
),
identity=mailbox_audit_property.get("Identity", ""),
)
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return mailbox_audit_properties
class Organization(BaseModel):
name: str
@@ -198,3 +239,59 @@ class TransportConfig(BaseModel):
class MailboxPolicy(BaseModel):
id: str
additional_storage_enabled: bool
class MailboxAuditProperties(BaseModel):
name: str
audit_enabled: bool
audit_admin: list[str]
audit_delegate: list[str]
audit_owner: list[str]
audit_log_age: int
identity: str
class AuditAdmin(Enum):
APPLY_RECORD = "ApplyRecord"
COPY = "Copy"
CREATE = "Create"
FOLDER_BIND = "FolderBind"
HARD_DELETE = "HardDelete"
MOVE = "Move"
MOVE_TO_DELETED_ITEMS = "MoveToDeletedItems"
SEND_AS = "SendAs"
SEND_ON_BEHALF = "SendOnBehalf"
SOFT_DELETE = "SoftDelete"
UPDATE = "Update"
UPDATE_CALENDAR_DELEGATION = "UpdateCalendarDelegation"
UPDATE_FOLDER_PERMISSIONS = "UpdateFolderPermissions"
UPDATE_INBOX_RULES = "UpdateInboxRules"
class AuditDelegate(Enum):
APPLY_RECORD = "ApplyRecord"
CREATE = "Create"
FOLDER_BIND = "FolderBind"
HARD_DELETE = "HardDelete"
MOVE = "Move"
MOVE_TO_DELETED_ITEMS = "MoveToDeletedItems"
SEND_AS = "SendAs"
SEND_ON_BEHALF = "SendOnBehalf"
SOFT_DELETE = "SoftDelete"
UPDATE = "Update"
UPDATE_FOLDER_PERMISSIONS = "UpdateFolderPermissions"
UPDATE_INBOX_RULES = "UpdateInboxRules"
class AuditOwner(Enum):
APPLY_RECORD = "ApplyRecord"
CREATE = "Create"
HARD_DELETE = "HardDelete"
MAILBOX_LOGIN = "MailboxLogin"
MOVE = "Move"
MOVE_TO_DELETED_ITEMS = "MoveToDeletedItems"
SOFT_DELETE = "SoftDelete"
UPDATE = "Update"
UPDATE_CALENDAR_DELEGATION = "UpdateCalendarDelegation"
UPDATE_FOLDER_PERMISSIONS = "UpdateFolderPermissions"
UPDATE_INBOX_RULES = "UpdateInboxRules"
@@ -0,0 +1,240 @@
from unittest import mock
from prowler.providers.m365.services.exchange.exchange_service import (
AuditAdmin,
AuditDelegate,
AuditOwner,
MailboxAuditProperties,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_exchange_mailbox_properties_auditing_e3_enabled:
def test_no_auditing_mailboxes(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_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled import (
exchange_mailbox_properties_auditing_e3_enabled,
)
exchange_client.mailbox_audit_properties = []
check = exchange_mailbox_properties_auditing_e3_enabled()
result = check.execute()
assert len(result) == 0
def test_auditing_fully_configured_and_log_age_valid(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_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled import (
exchange_mailbox_properties_auditing_e3_enabled,
)
exchange_client.mailbox_audit_properties = [
MailboxAuditProperties(
name="User1",
audit_enabled=True,
audit_admin=[e.value for e in AuditAdmin],
audit_delegate=[e.value for e in AuditDelegate],
audit_owner=[e.value for e in AuditOwner],
audit_log_age=180,
identity="User1",
)
]
check = exchange_mailbox_properties_auditing_e3_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Mailbox Audit Properties for Mailbox User1 is enabled and properly configured."
)
assert result[0].resource_name == "User1"
assert result[0].resource_id == "User1"
assert result[0].location == "global"
assert (
result[0].resource == exchange_client.mailbox_audit_properties[0].dict()
)
def test_audit_enabled_but_incomplete_configuration(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_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled import (
exchange_mailbox_properties_auditing_e3_enabled,
)
exchange_client.mailbox_audit_properties = [
MailboxAuditProperties(
name="User2",
audit_enabled=True,
audit_admin=["SendAs"],
audit_delegate=["Send"],
audit_owner=["Update"],
audit_log_age=180,
identity="User2",
)
]
check = exchange_mailbox_properties_auditing_e3_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Mailbox Audit Properties for Mailbox User2 is enabled but not properly configured. Missing audit actions may exist."
)
assert result[0].resource_name == "User2"
assert result[0].resource_id == "User2"
assert result[0].location == "global"
assert (
result[0].resource == exchange_client.mailbox_audit_properties[0].dict()
)
def test_audit_not_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_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled import (
exchange_mailbox_properties_auditing_e3_enabled,
)
exchange_client.mailbox_audit_properties = [
MailboxAuditProperties(
name="User3",
audit_enabled=False,
audit_admin=[],
audit_delegate=[],
audit_owner=[],
audit_log_age=0,
identity="User3",
)
]
check = exchange_mailbox_properties_auditing_e3_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Mailbox Audit Properties for Mailbox User3 is not enabled."
)
assert result[0].resource_name == "User3"
assert result[0].resource_id == "User3"
assert result[0].location == "global"
assert (
result[0].resource == exchange_client.mailbox_audit_properties[0].dict()
)
def test_audit_enabled_but_log_age_too_low(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_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_mailbox_properties_auditing_e3_enabled.exchange_mailbox_properties_auditing_e3_enabled import (
exchange_mailbox_properties_auditing_e3_enabled,
)
exchange_client.mailbox_audit_properties = [
MailboxAuditProperties(
name="User4",
audit_enabled=True,
audit_admin=[e.value for e in AuditAdmin],
audit_delegate=[e.value for e in AuditDelegate],
audit_owner=[e.value for e in AuditOwner],
audit_log_age=30,
identity="User4",
)
]
check = exchange_mailbox_properties_auditing_e3_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Mailbox Audit Properties for Mailbox User4 is enabled and properly configured but the audit log age is less than 90 days."
)
assert result[0].resource_name == "User4"
assert result[0].resource_id == "User4"
assert result[0].location == "global"
assert (
result[0].resource == exchange_client.mailbox_audit_properties[0].dict()
)
@@ -6,6 +6,7 @@ from prowler.providers.m365.services.exchange.exchange_service import (
Exchange,
ExternalMailConfig,
MailboxAuditConfig,
MailboxAuditProperties,
MailboxPolicy,
Organization,
TransportConfig,
@@ -74,6 +75,57 @@ def mock_exchange_get_mailbox_policy(_):
)
def mock_exchange_get_mailbox_audit_properties(_):
return [
MailboxAuditProperties(
name="User1",
audit_enabled=False,
audit_admin=[
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"SendAs",
"SendOnBehalf",
"Create",
"UpdateFolderPermissions",
"UpdateInboxRules",
"UpdateCalendarDelegation",
"ApplyRecord",
"MailItemsAccessed",
"Send",
],
audit_delegate=[
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"SendAs",
"SendOnBehalf",
"Create",
"UpdateFolderPermissions",
"UpdateInboxRules",
"ApplyRecord",
"MailItemsAccessed",
],
audit_owner=[
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"UpdateFolderPermissions",
"UpdateInboxRules",
"UpdateCalendarDelegation",
"ApplyRecord",
"MailItemsAccessed",
"Send",
],
audit_log_age=90,
identity="test",
)
]
class Test_Exchange_Service:
def test_get_client(self):
with (
@@ -230,3 +282,66 @@ class Test_Exchange_Service:
assert transport_config.smtp_auth_disabled is True
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_mailbox_audit_properties",
new=mock_exchange_get_mailbox_audit_properties,
)
def test_get_mailbox_audit_properties(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)
)
)
mailbox_audit_properties = exchange_client.mailbox_audit_properties
assert len(mailbox_audit_properties) == 1
assert mailbox_audit_properties[0].name == "User1"
assert mailbox_audit_properties[0].audit_enabled is False
assert mailbox_audit_properties[0].audit_admin == [
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"SendAs",
"SendOnBehalf",
"Create",
"UpdateFolderPermissions",
"UpdateInboxRules",
"UpdateCalendarDelegation",
"ApplyRecord",
"MailItemsAccessed",
"Send",
]
assert mailbox_audit_properties[0].audit_delegate == [
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"SendAs",
"SendOnBehalf",
"Create",
"UpdateFolderPermissions",
"UpdateInboxRules",
"ApplyRecord",
"MailItemsAccessed",
]
assert mailbox_audit_properties[0].audit_owner == [
"Update",
"MoveToDeletedItems",
"SoftDelete",
"HardDelete",
"UpdateFolderPermissions",
"UpdateInboxRules",
"UpdateCalendarDelegation",
"ApplyRecord",
"MailItemsAccessed",
"Send",
]
assert mailbox_audit_properties[0].audit_log_age == 90
assert mailbox_audit_properties[0].identity == "test"
exchange_client.powershell.close()