diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 9a73fdf02f..aeb3470893 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -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 diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 49326cd38b..560e9b3194 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -188,3 +188,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") diff --git a/prowler/providers/m365/services/exchange/__init__.py b/prowler/providers/m365/services/exchange/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_client.py b/prowler/providers/m365/services/exchange/exchange_client.py new file mode 100644 index 0000000000..5be172a2a3 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_client.py @@ -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()) diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json new file mode 100644 index 0000000000..abe968f4e0 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.metadata.json @@ -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": "" +} diff --git a/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.py b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.py new file mode 100644 index 0000000000..2e4cdaedea --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled.py @@ -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 diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py new file mode 100644 index 0000000000..5ae7171882 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -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 diff --git a/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py b/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py new file mode 100644 index 0000000000..104b050541 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py @@ -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" diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py new file mode 100644 index 0000000000..92cd275365 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -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()