diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 82c884093a..27e03c39ce 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747) - `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840) +- `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828) ### Changed diff --git a/prowler/compliance/m365/cis_4.0_m365.json b/prowler/compliance/m365/cis_4.0_m365.json index 96a7932baf..98404ca88f 100644 --- a/prowler/compliance/m365/cis_4.0_m365.json +++ b/prowler/compliance/m365/cis_4.0_m365.json @@ -121,7 +121,9 @@ { "Id": "1.2.2", "Description": "Shared mailboxes are used when multiple people need access to the same mailbox, such as a company information or support email address, reception desk, or other function that might be shared by multiple people.Users with permissions to the group mailbox can send as or send on behalf of the mailbox email address if the administrator has given that user permissions to do that. This is particularly useful for help and support mailboxes because users can send emails from \"Contoso Support\" or \"Building A Reception Desk.\"Shared mailboxes are created with a corresponding user account using a system generated password that is unknown at the time of creation.The recommended state is `Sign in blocked` for `Shared mailboxes`.", - "Checks": [], + "Checks": [ + "exchange_shared_mailbox_sign_in_disabled" + ], "Attributes": [ { "Section": "1 Microsoft 365 admin center", diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index ea97cf10e6..a8f2a1e95d 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -121,7 +121,9 @@ { "Id": "1.2.2", "Description": "Shared mailboxes are used when multiple people need access to the same mailbox, such as a company information or support email address, reception desk, or other function that might be shared by multiple people. Shared mailboxes are created with a corresponding user account using a system generated password that is unknown at the time of creation. The recommended state is Sign in blocked for Shared mailboxes.", - "Checks": [], + "Checks": [ + "exchange_shared_mailbox_sign_in_disabled" + ], "Attributes": [ { "Section": "1 Microsoft 365 admin center", diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 6abda172f9..512f9f03e2 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -823,6 +823,32 @@ class M365PowerShell(PowerShellSession): "Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True ) + def get_shared_mailboxes(self) -> dict: + """ + Get Exchange Online Shared Mailboxes. + + Retrieves all shared mailboxes from Exchange Online with their external + directory object IDs for cross-referencing with Entra ID user accounts. + + Returns: + dict: Shared mailbox information in JSON format. + + Example: + >>> get_shared_mailboxes() + [ + { + "DisplayName": "Support Mailbox", + "UserPrincipalName": "support@contoso.com", + "ExternalDirectoryObjectId": "12345678-1234-1234-1234-123456789012", + "Identity": "support@contoso.com" + } + ] + """ + return self.execute( + "Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited | Select-Object DisplayName, UserPrincipalName, ExternalDirectoryObjectId, Identity | ConvertTo-Json -Depth 10", + json_parse=True, + ) + def get_user_account_status(self) -> dict: """ Get User Account Status. @@ -833,7 +859,7 @@ class M365PowerShell(PowerShellSession): dict: User account status settings in JSON format. """ return self.execute( - "$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json -Depth 10", + "$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.ExternalDirectoryObjectId] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json -Depth 10", json_parse=True, ) diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index 1c8f1d1e87..978a9b2a10 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -9,7 +9,20 @@ from prowler.providers.m365.m365_provider import M365Provider class Exchange(M365Service): + """ + Exchange Online service for Microsoft 365. + + This service provides access to Exchange Online resources and configurations + including organization settings, mailboxes, transport rules, and policies. + """ + def __init__(self, provider: M365Provider): + """ + Initialize the Exchange service. + + Args: + provider: The M365Provider instance for authentication and configuration. + """ super().__init__(provider) self.organization_config = None self.mailboxes_config = [] @@ -19,6 +32,7 @@ class Exchange(M365Service): self.mailbox_policies = [] self.role_assignment_policies = [] self.mailbox_audit_properties = [] + self.shared_mailboxes = [] if self.powershell: if self.powershell.connect_exchange_online(): @@ -30,6 +44,7 @@ class Exchange(M365Service): self.mailbox_policies = self._get_mailbox_policy() self.role_assignment_policies = self._get_role_assignment_policies() self.mailbox_audit_properties = self._get_mailbox_audit_properties() + self.shared_mailboxes = self._get_shared_mailboxes() self.powershell.close() def _get_organization_config(self): @@ -211,6 +226,12 @@ class Exchange(M365Service): return role_assignment_policies def _get_mailbox_audit_properties(self): + """ + Get mailbox audit properties for all mailboxes. + + Returns: + list[MailboxAuditProperties]: List of mailbox audit property configurations. + """ logger.info("Microsoft365 - Getting mailbox audit properties...") mailbox_audit_properties = [] try: @@ -248,6 +269,44 @@ class Exchange(M365Service): ) return mailbox_audit_properties + def _get_shared_mailboxes(self): + """ + Get all shared mailboxes from Exchange Online. + + Retrieves shared mailboxes with their external directory object IDs + for cross-referencing with Entra ID user accounts. + + Returns: + list[SharedMailbox]: List of shared mailbox configurations. + """ + logger.info("Microsoft365 - Getting shared mailboxes...") + shared_mailboxes = [] + try: + shared_mailboxes_data = self.powershell.get_shared_mailboxes() + if not shared_mailboxes_data: + return shared_mailboxes + if isinstance(shared_mailboxes_data, dict): + shared_mailboxes_data = [shared_mailboxes_data] + for shared_mailbox in shared_mailboxes_data: + if shared_mailbox: + shared_mailboxes.append( + SharedMailbox( + name=shared_mailbox.get("DisplayName", ""), + user_principal_name=shared_mailbox.get( + "UserPrincipalName", "" + ), + external_directory_object_id=shared_mailbox.get( + "ExternalDirectoryObjectId", "" + ), + identity=shared_mailbox.get("Identity", ""), + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return shared_mailboxes + class Organization(BaseModel): name: str @@ -342,6 +401,8 @@ class AuditDelegate(Enum): class AuditOwner(Enum): + """Audit actions for mailbox owner operations.""" + APPLY_RECORD = "ApplyRecord" CREATE = "Create" HARD_DELETE = "HardDelete" @@ -353,3 +414,20 @@ class AuditOwner(Enum): UPDATE_CALENDAR_DELEGATION = "UpdateCalendarDelegation" UPDATE_FOLDER_PERMISSIONS = "UpdateFolderPermissions" UPDATE_INBOX_RULES = "UpdateInboxRules" + + +class SharedMailbox(BaseModel): + """ + Model for Exchange Online shared mailbox. + + Attributes: + name: Display name of the shared mailbox. + user_principal_name: User principal name (email) of the shared mailbox. + external_directory_object_id: The Entra ID object ID for cross-referencing. + identity: Identity of the shared mailbox in Exchange. + """ + + name: str + user_principal_name: str + external_directory_object_id: str + identity: str diff --git a/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/__init__.py b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json new file mode 100644 index 0000000000..118868c158 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "exchange_shared_mailbox_sign_in_disabled", + "CheckTitle": "Shared mailbox has sign-in blocked", + "CheckType": [], + "ServiceName": "exchange", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Shared Mailbox", + "ResourceGroup": "IAM", + "Description": "Shared mailboxes are used for collaboration and should not permit direct sign-in. This check verifies that the **AccountEnabled** property is set to `false` in Entra ID for all shared mailboxes, preventing direct authentication.", + "Risk": "When sign-in is enabled on shared mailboxes, users with the password can bypass delegation controls and access the mailbox directly. This undermines **accountability** since actions cannot be attributed to individual users, and it increases the attack surface for credential-based attacks.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/microsoft-365/admin/email/about-shared-mailboxes", + "https://learn.microsoft.com/en-us/microsoft-365/admin/email/create-a-shared-mailbox#block-sign-in-for-the-shared-mailbox-account" + ], + "Remediation": { + "Code": { + "CLI": "Get-EXOMailbox -RecipientTypeDetails SharedMailbox | ForEach-Object { Update-MgUser -UserId $_.ExternalDirectoryObjectId -AccountEnabled:$false }", + "NativeIaC": "", + "Other": "1. Navigate to Entra admin center (https://entra.microsoft.com/)\n2. Expand Identity > Users and select All users\n3. Search for and select the shared mailbox user account\n4. In the properties pane, go to Account status\n5. Uncheck 'Account enabled' and click Save\n6. Repeat for all shared mailbox accounts", + "Terraform": "" + }, + "Recommendation": { + "Text": "Block sign-in for all shared mailboxes to ensure users can only access them through delegation. This enforces accountability and reduces security risks from shared credentials.", + "Url": "https://hub.prowler.com/check/exchange_shared_mailbox_sign_in_disabled" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.py b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.py new file mode 100644 index 0000000000..849731f7c8 --- /dev/null +++ b/prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled.py @@ -0,0 +1,59 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.exchange.exchange_client import exchange_client + + +class exchange_shared_mailbox_sign_in_disabled(Check): + """ + Verify that sign-in is blocked for all shared mailboxes. + + Shared mailboxes are designed for collaboration and should not permit direct + sign-in. Users should access shared mailboxes through delegation only, which + ensures accountability and proper access controls. + + - PASS: Shared mailbox has sign-in blocked (AccountEnabled = False in Entra ID). + - FAIL: Shared mailbox has sign-in enabled (AccountEnabled = True in Entra ID). + """ + + def execute(self) -> List[CheckReportM365]: + """ + Execute the check to verify shared mailbox sign-in status. + + Cross-references shared mailboxes from Exchange Online with user accounts + in Entra ID to determine if sign-in is blocked. + + Returns: + List[CheckReportM365]: A list of reports with the sign-in status for + each shared mailbox. + """ + findings = [] + + for shared_mailbox in exchange_client.shared_mailboxes: + report = CheckReportM365( + metadata=self.metadata(), + resource=shared_mailbox, + resource_name=shared_mailbox.name or shared_mailbox.user_principal_name, + resource_id=shared_mailbox.external_directory_object_id + or shared_mailbox.identity, + ) + + # Look up the user in Entra ID by their external directory object ID + entra_user = entra_client.users.get( + shared_mailbox.external_directory_object_id + ) + + if not entra_user: + report.status = "FAIL" + report.status_extended = f"Shared mailbox {shared_mailbox.user_principal_name} could not be found in Entra ID for verification." + elif entra_user.account_enabled: + report.status = "FAIL" + report.status_extended = f"Shared mailbox {shared_mailbox.user_principal_name} has sign-in enabled." + else: + report.status = "PASS" + report.status_extended = f"Shared mailbox {shared_mailbox.user_principal_name} has sign-in blocked." + + findings.append(report) + + return findings diff --git a/tests/providers/m365/services/exchange/exchange_service_test.py b/tests/providers/m365/services/exchange/exchange_service_test.py index 9a93e616a5..3f0e62cc3b 100644 --- a/tests/providers/m365/services/exchange/exchange_service_test.py +++ b/tests/providers/m365/services/exchange/exchange_service_test.py @@ -9,6 +9,7 @@ from prowler.providers.m365.services.exchange.exchange_service import ( MailboxAuditProperties, Organization, RoleAssignmentPolicy, + SharedMailbox, TransportConfig, TransportRule, ) @@ -143,6 +144,23 @@ def mock_exchange_get_mailbox_audit_properties(_): ] +def mock_exchange_get_shared_mailboxes(_): + return [ + SharedMailbox( + name="Support Mailbox", + user_principal_name="support@contoso.com", + external_directory_object_id="12345678-1234-1234-1234-123456789012", + identity="support@contoso.com", + ), + SharedMailbox( + name="Info Mailbox", + user_principal_name="info@contoso.com", + external_directory_object_id="87654321-4321-4321-4321-210987654321", + identity="info@contoso.com", + ), + ] + + class Test_Exchange_Service: def test_get_client(self): with ( @@ -429,3 +447,37 @@ class Test_Exchange_Service: assert role_assignment_policies[1].assigned_roles == [] exchange_client.powershell.close() + + @patch( + "prowler.providers.m365.services.exchange.exchange_service.Exchange._get_shared_mailboxes", + new=mock_exchange_get_shared_mailboxes, + ) + def test_get_shared_mailboxes(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) + ) + ) + shared_mailboxes = exchange_client.shared_mailboxes + assert len(shared_mailboxes) == 2 + assert shared_mailboxes[0].name == "Support Mailbox" + assert shared_mailboxes[0].user_principal_name == "support@contoso.com" + assert ( + shared_mailboxes[0].external_directory_object_id + == "12345678-1234-1234-1234-123456789012" + ) + assert shared_mailboxes[0].identity == "support@contoso.com" + assert shared_mailboxes[1].name == "Info Mailbox" + assert shared_mailboxes[1].user_principal_name == "info@contoso.com" + assert ( + shared_mailboxes[1].external_directory_object_id + == "87654321-4321-4321-4321-210987654321" + ) + assert shared_mailboxes[1].identity == "info@contoso.com" + + exchange_client.powershell.close() diff --git a/tests/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled_test.py b/tests/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled_test.py new file mode 100644 index 0000000000..7cd9191049 --- /dev/null +++ b/tests/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/exchange_shared_mailbox_sign_in_disabled_test.py @@ -0,0 +1,317 @@ +from unittest import mock + +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_exchange_shared_mailbox_sign_in_disabled: + def test_no_shared_mailboxes(self): + exchange_client = mock.MagicMock() + exchange_client.audited_tenant = "audited_tenant" + exchange_client.audited_domain = DOMAIN + exchange_client.shared_mailboxes = [] + + entra_client = mock.MagicMock() + entra_client.users = {} + + 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_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.exchange_client", + new=exchange_client, + ), + mock.patch( + "prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled import ( + exchange_shared_mailbox_sign_in_disabled, + ) + + check = exchange_shared_mailbox_sign_in_disabled() + result = check.execute() + assert len(result) == 0 + + def test_sign_in_disabled(self): + """PASS: Shared mailbox has sign-in blocked (AccountEnabled = False in Entra ID).""" + 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_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service import User + from prowler.providers.m365.services.exchange.exchange_service import ( + SharedMailbox, + ) + from prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled import ( + exchange_shared_mailbox_sign_in_disabled, + ) + + shared_mailbox = SharedMailbox( + name="Support Mailbox", + user_principal_name="support@contoso.com", + external_directory_object_id="12345678-1234-1234-1234-123456789012", + identity="support@contoso.com", + ) + exchange_client.shared_mailboxes = [shared_mailbox] + + entra_user = User( + id="12345678-1234-1234-1234-123456789012", + name="Support Mailbox", + on_premises_sync_enabled=False, + account_enabled=False, + ) + entra_client = mock.MagicMock() + entra_client.users = { + "12345678-1234-1234-1234-123456789012": entra_user, + } + + with mock.patch( + "prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.entra_client", + new=entra_client, + ): + check = exchange_shared_mailbox_sign_in_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Shared mailbox support@contoso.com has sign-in blocked." + ) + assert result[0].resource_name == "Support Mailbox" + assert result[0].resource_id == "12345678-1234-1234-1234-123456789012" + assert result[0].location == "global" + + def test_sign_in_enabled(self): + """FAIL: Shared mailbox has sign-in enabled (AccountEnabled = True in Entra ID).""" + 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_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service import User + from prowler.providers.m365.services.exchange.exchange_service import ( + SharedMailbox, + ) + from prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled import ( + exchange_shared_mailbox_sign_in_disabled, + ) + + shared_mailbox = SharedMailbox( + name="Info Mailbox", + user_principal_name="info@contoso.com", + external_directory_object_id="87654321-4321-4321-4321-210987654321", + identity="info@contoso.com", + ) + exchange_client.shared_mailboxes = [shared_mailbox] + + entra_user = User( + id="87654321-4321-4321-4321-210987654321", + name="Info Mailbox", + on_premises_sync_enabled=False, + account_enabled=True, + ) + entra_client = mock.MagicMock() + entra_client.users = { + "87654321-4321-4321-4321-210987654321": entra_user, + } + + with mock.patch( + "prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.entra_client", + new=entra_client, + ): + check = exchange_shared_mailbox_sign_in_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Shared mailbox info@contoso.com has sign-in enabled." + ) + assert result[0].resource_name == "Info Mailbox" + assert result[0].resource_id == "87654321-4321-4321-4321-210987654321" + assert result[0].location == "global" + + def test_user_not_found_in_entra(self): + """FAIL: Shared mailbox not found in Entra ID for verification.""" + 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_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.exchange.exchange_service import ( + SharedMailbox, + ) + from prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled import ( + exchange_shared_mailbox_sign_in_disabled, + ) + + shared_mailbox = SharedMailbox( + name="Orphan Mailbox", + user_principal_name="orphan@contoso.com", + external_directory_object_id="00000000-0000-0000-0000-000000000000", + identity="orphan@contoso.com", + ) + exchange_client.shared_mailboxes = [shared_mailbox] + + entra_client = mock.MagicMock() + entra_client.users = {} + + with mock.patch( + "prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.entra_client", + new=entra_client, + ): + check = exchange_shared_mailbox_sign_in_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Shared mailbox orphan@contoso.com could not be found in Entra ID for verification." + ) + assert result[0].resource_name == "Orphan Mailbox" + assert result[0].resource_id == "00000000-0000-0000-0000-000000000000" + assert result[0].location == "global" + + def test_multiple_shared_mailboxes_mixed_status(self): + """Test multiple shared mailboxes with different sign-in statuses.""" + 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_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.exchange_client", + new=exchange_client, + ), + ): + from prowler.providers.m365.services.entra.entra_service import User + from prowler.providers.m365.services.exchange.exchange_service import ( + SharedMailbox, + ) + from prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled import ( + exchange_shared_mailbox_sign_in_disabled, + ) + + mailbox_disabled = SharedMailbox( + name="Secure Mailbox", + user_principal_name="secure@contoso.com", + external_directory_object_id="11111111-1111-1111-1111-111111111111", + identity="secure@contoso.com", + ) + mailbox_enabled = SharedMailbox( + name="Insecure Mailbox", + user_principal_name="insecure@contoso.com", + external_directory_object_id="22222222-2222-2222-2222-222222222222", + identity="insecure@contoso.com", + ) + mailbox_orphan = SharedMailbox( + name="Unknown Mailbox", + user_principal_name="unknown@contoso.com", + external_directory_object_id="33333333-3333-3333-3333-333333333333", + identity="unknown@contoso.com", + ) + + exchange_client.shared_mailboxes = [ + mailbox_disabled, + mailbox_enabled, + mailbox_orphan, + ] + + user_disabled = User( + id="11111111-1111-1111-1111-111111111111", + name="Secure Mailbox", + on_premises_sync_enabled=False, + account_enabled=False, + ) + user_enabled = User( + id="22222222-2222-2222-2222-222222222222", + name="Insecure Mailbox", + on_premises_sync_enabled=False, + account_enabled=True, + ) + + entra_client = mock.MagicMock() + entra_client.users = { + "11111111-1111-1111-1111-111111111111": user_disabled, + "22222222-2222-2222-2222-222222222222": user_enabled, + } + + with mock.patch( + "prowler.providers.m365.services.exchange.exchange_shared_mailbox_sign_in_disabled.exchange_shared_mailbox_sign_in_disabled.entra_client", + new=entra_client, + ): + check = exchange_shared_mailbox_sign_in_disabled() + result = check.execute() + + assert len(result) == 3 + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Shared mailbox secure@contoso.com has sign-in blocked." + ) + + assert result[1].status == "FAIL" + assert ( + result[1].status_extended + == "Shared mailbox insecure@contoso.com has sign-in enabled." + ) + + assert result[2].status == "FAIL" + assert ( + result[2].status_extended + == "Shared mailbox unknown@contoso.com could not be found in Entra ID for verification." + )