feat(exchange): add service and new check exchange_organization_mailbox_auditing_enabled (#7408)

Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2025-04-16 18:19:06 +02:00
committed by Pepe Fagoaga
parent 86b6732013
commit 311c9a41ff
10 changed files with 301 additions and 0 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).
- Support CLOUDSDK_AUTH_ACCESS_TOKEN in GCP [(#7495)](https://github.com/prowler-cloud/prowler/pull/7495).
- Add Powershell to Microsoft365 [(#7331)](https://github.com/prowler-cloud/prowler/pull/7331)
- Add service Exchange to Microsoft365 with one check for Organizations Mailbox Auditing enabled [(#7408)](https://github.com/prowler-cloud/prowler/pull/7408)
- Add new check `teams_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533)
### Fixed
@@ -169,3 +169,22 @@ class M365PowerShell(PowerShellSession):
return self.execute(
"Get-AdminAuditLogConfig | Select-Object UnifiedAuditLogIngestionEnabled | ConvertTo-Json"
)
def get_organization_config(self) -> dict:
"""
Get Exchange Online Organization Configuration.
Retrieves the current Exchange Online organization configuration settings.
Returns:
dict: Organization configuration settings in JSON format.
Example:
>>> get_organization_config()
{
"Name": "MyOrganization",
"Guid": "12345678-1234-1234-1234-123456789012"
"AuditDisabled": false
}
"""
return self.execute("Get-OrganizationConfig | ConvertTo-Json")
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.m365.services.exchange.exchange_service import Exchange
exchange_client = Exchange(Provider.get_global_provider())
@@ -0,0 +1,30 @@
{
"Provider": "m365",
"CheckID": "exchange_organization_mailbox_auditing_enabled",
"CheckTitle": "Ensure AuditDisabled organizationally is set to False.",
"CheckType": [],
"ServiceName": "exchange",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Exchange Organization Configuration",
"Description": "Ensure that the AuditDisabled property is set to False at the organizational level in Exchange Online. This enables mailbox auditing by default for all mailboxes and overrides individual mailbox settings.",
"Risk": "If mailbox auditing is disabled at the organization level, no mailbox actions are audited, limiting forensic investigation capabilities and exposing the organization to undetected malicious activity.",
"RelatedUrl": "https://learn.microsoft.com/en-us/purview/audit-mailboxes?view=o365-worldwide",
"Remediation": {
"Code": {
"CLI": "Set-OrganizationConfig -AuditDisabled $false",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Set AuditDisabled to False at the organization level to ensure mailbox auditing is always enforced.",
"Url": "https://learn.microsoft.com/en-us/powershell/module/exchange/set-organizationconfig?view=exchange-ps#-auditdisabled"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,44 @@
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_mailbox_auditing_enabled(Check):
"""Check if Exchange mailbox auditing is enabled.
Attributes:
metadata: Metadata associated with the check (inherited from Check).
"""
def execute(self) -> List[CheckReportM365]:
"""Execute the check for Exchange mailbox auditing.
This method checks if mailbox auditing is enabled in the Exchange organization configuration.
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 = (
"Exchange mailbox auditing is not enabled on your organization."
)
if not organization_config.audit_disabled:
report.status = "PASS"
report.status_extended = (
"Exchange mailbox auditing is enabled on your organization."
)
findings.append(report)
return findings
@@ -0,0 +1,35 @@
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.m365.lib.service.service import M365Service
from prowler.providers.m365.m365_provider import M365Provider
class Exchange(M365Service):
def __init__(self, provider: M365Provider):
super().__init__(provider)
self.powershell.connect_exchange_online()
self.organization_config = self._get_organization_config()
self.powershell.close()
def _get_organization_config(self):
logger.info("Microsoft365 - Getting Exchange Organization configuration...")
organization_config = None
try:
organization_configuration = self.powershell.get_organization_config()
organization_config = Organization(
name=organization_configuration.get("Name", ""),
guid=organization_configuration.get("Guid", ""),
audit_disabled=organization_configuration.get("AuditDisabled", False),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return organization_config
class Organization(BaseModel):
name: str
guid: str
audit_disabled: bool
@@ -0,0 +1,116 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_exchange_organization_mailbox_auditing_enabled:
def test_no_organization(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_mailbox_auditing_enabled.exchange_organization_mailbox_auditing_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_mailbox_auditing_enabled.exchange_organization_mailbox_auditing_enabled import (
exchange_organization_mailbox_auditing_enabled,
)
check = exchange_organization_mailbox_auditing_enabled()
result = check.execute()
assert len(result) == 0
def test_audit_log_search_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_mailbox_auditing_enabled.exchange_organization_mailbox_auditing_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_mailbox_auditing_enabled.exchange_organization_mailbox_auditing_enabled import (
exchange_organization_mailbox_auditing_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
audit_disabled=True, name="test", guid="test"
)
check = exchange_organization_mailbox_auditing_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Exchange mailbox auditing is not enabled on your organization."
)
assert result[0].resource == exchange_client.organization_config.dict()
assert result[0].resource_name == "test"
assert result[0].resource_id == "test"
assert result[0].location == "global"
def test_audit_log_search_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_mailbox_auditing_enabled.exchange_organization_mailbox_auditing_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_mailbox_auditing_enabled.exchange_organization_mailbox_auditing_enabled import (
exchange_organization_mailbox_auditing_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
audit_disabled=False, name="test", guid="test"
)
check = exchange_organization_mailbox_auditing_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Exchange mailbox auditing is enabled on your organization."
)
assert result[0].resource == exchange_client.organization_config.dict()
assert result[0].resource_name == "test"
assert result[0].resource_id == "test"
assert result[0].location == "global"
@@ -0,0 +1,52 @@
from unittest import mock
from unittest.mock import patch
from prowler.providers.m365.models import M365IdentityInfo
from prowler.providers.m365.services.exchange.exchange_service import (
Exchange,
Organization,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
def mock_exchange_get_organization_config(_):
return Organization(audit_disabled=True, name="test", guid="test")
class Test_Exchange_Service:
def test_get_client(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)
)
)
assert exchange_client.client.__class__.__name__ == "GraphServiceClient"
assert exchange_client.powershell.__class__.__name__ == "M365PowerShell"
exchange_client.powershell.close()
@patch(
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_organization_config",
new=mock_exchange_get_organization_config,
)
def test_get_organization_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)
)
)
organization_config = exchange_client.organization_config
assert organization_config.name == "test"
assert organization_config.guid == "test"
assert organization_config.audit_disabled is True
exchange_client.powershell.close()