Skip to content

Commit 7f4c02c

Browse files
authored
feat(m365): add exchange_shared_mailbox_sign_in_disabled check (#9828)
1 parent d386730 commit 7f4c02c

10 files changed

Lines changed: 577 additions & 3 deletions

File tree

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
88

99
- `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747)
1010
- `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840)
11+
- `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828)
1112

1213
### Changed
1314

prowler/compliance/m365/cis_4.0_m365.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@
121121
{
122122
"Id": "1.2.2",
123123
"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`.",
124-
"Checks": [],
124+
"Checks": [
125+
"exchange_shared_mailbox_sign_in_disabled"
126+
],
125127
"Attributes": [
126128
{
127129
"Section": "1 Microsoft 365 admin center",

prowler/compliance/m365/cis_6.0_m365.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@
121121
{
122122
"Id": "1.2.2",
123123
"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.",
124-
"Checks": [],
124+
"Checks": [
125+
"exchange_shared_mailbox_sign_in_disabled"
126+
],
125127
"Attributes": [
126128
{
127129
"Section": "1 Microsoft 365 admin center",

prowler/providers/m365/lib/powershell/m365_powershell.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,32 @@ def get_sharing_policy(self) -> dict:
823823
"Get-SharingPolicy | ConvertTo-Json -Depth 10", json_parse=True
824824
)
825825

826+
def get_shared_mailboxes(self) -> dict:
827+
"""
828+
Get Exchange Online Shared Mailboxes.
829+
830+
Retrieves all shared mailboxes from Exchange Online with their external
831+
directory object IDs for cross-referencing with Entra ID user accounts.
832+
833+
Returns:
834+
dict: Shared mailbox information in JSON format.
835+
836+
Example:
837+
>>> get_shared_mailboxes()
838+
[
839+
{
840+
"DisplayName": "Support Mailbox",
841+
"UserPrincipalName": "[email protected]",
842+
"ExternalDirectoryObjectId": "12345678-1234-1234-1234-123456789012",
843+
"Identity": "[email protected]"
844+
}
845+
]
846+
"""
847+
return self.execute(
848+
"Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited | Select-Object DisplayName, UserPrincipalName, ExternalDirectoryObjectId, Identity | ConvertTo-Json -Depth 10",
849+
json_parse=True,
850+
)
851+
826852
def get_user_account_status(self) -> dict:
827853
"""
828854
Get User Account Status.
@@ -833,7 +859,7 @@ def get_user_account_status(self) -> dict:
833859
dict: User account status settings in JSON format.
834860
"""
835861
return self.execute(
836-
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json -Depth 10",
862+
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.ExternalDirectoryObjectId] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json -Depth 10",
837863
json_parse=True,
838864
)
839865

prowler/providers/m365/services/exchange/exchange_service.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,20 @@
99

1010

1111
class Exchange(M365Service):
12+
"""
13+
Exchange Online service for Microsoft 365.
14+
15+
This service provides access to Exchange Online resources and configurations
16+
including organization settings, mailboxes, transport rules, and policies.
17+
"""
18+
1219
def __init__(self, provider: M365Provider):
20+
"""
21+
Initialize the Exchange service.
22+
23+
Args:
24+
provider: The M365Provider instance for authentication and configuration.
25+
"""
1326
super().__init__(provider)
1427
self.organization_config = None
1528
self.mailboxes_config = []
@@ -19,6 +32,7 @@ def __init__(self, provider: M365Provider):
1932
self.mailbox_policies = []
2033
self.role_assignment_policies = []
2134
self.mailbox_audit_properties = []
35+
self.shared_mailboxes = []
2236

2337
if self.powershell:
2438
if self.powershell.connect_exchange_online():
@@ -30,6 +44,7 @@ def __init__(self, provider: M365Provider):
3044
self.mailbox_policies = self._get_mailbox_policy()
3145
self.role_assignment_policies = self._get_role_assignment_policies()
3246
self.mailbox_audit_properties = self._get_mailbox_audit_properties()
47+
self.shared_mailboxes = self._get_shared_mailboxes()
3348
self.powershell.close()
3449

3550
def _get_organization_config(self):
@@ -211,6 +226,12 @@ def _get_role_assignment_policies(self):
211226
return role_assignment_policies
212227

213228
def _get_mailbox_audit_properties(self):
229+
"""
230+
Get mailbox audit properties for all mailboxes.
231+
232+
Returns:
233+
list[MailboxAuditProperties]: List of mailbox audit property configurations.
234+
"""
214235
logger.info("Microsoft365 - Getting mailbox audit properties...")
215236
mailbox_audit_properties = []
216237
try:
@@ -248,6 +269,44 @@ def _get_mailbox_audit_properties(self):
248269
)
249270
return mailbox_audit_properties
250271

272+
def _get_shared_mailboxes(self):
273+
"""
274+
Get all shared mailboxes from Exchange Online.
275+
276+
Retrieves shared mailboxes with their external directory object IDs
277+
for cross-referencing with Entra ID user accounts.
278+
279+
Returns:
280+
list[SharedMailbox]: List of shared mailbox configurations.
281+
"""
282+
logger.info("Microsoft365 - Getting shared mailboxes...")
283+
shared_mailboxes = []
284+
try:
285+
shared_mailboxes_data = self.powershell.get_shared_mailboxes()
286+
if not shared_mailboxes_data:
287+
return shared_mailboxes
288+
if isinstance(shared_mailboxes_data, dict):
289+
shared_mailboxes_data = [shared_mailboxes_data]
290+
for shared_mailbox in shared_mailboxes_data:
291+
if shared_mailbox:
292+
shared_mailboxes.append(
293+
SharedMailbox(
294+
name=shared_mailbox.get("DisplayName", ""),
295+
user_principal_name=shared_mailbox.get(
296+
"UserPrincipalName", ""
297+
),
298+
external_directory_object_id=shared_mailbox.get(
299+
"ExternalDirectoryObjectId", ""
300+
),
301+
identity=shared_mailbox.get("Identity", ""),
302+
)
303+
)
304+
except Exception as error:
305+
logger.error(
306+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
307+
)
308+
return shared_mailboxes
309+
251310

252311
class Organization(BaseModel):
253312
name: str
@@ -342,6 +401,8 @@ class AuditDelegate(Enum):
342401

343402

344403
class AuditOwner(Enum):
404+
"""Audit actions for mailbox owner operations."""
405+
345406
APPLY_RECORD = "ApplyRecord"
346407
CREATE = "Create"
347408
HARD_DELETE = "HardDelete"
@@ -353,3 +414,20 @@ class AuditOwner(Enum):
353414
UPDATE_CALENDAR_DELEGATION = "UpdateCalendarDelegation"
354415
UPDATE_FOLDER_PERMISSIONS = "UpdateFolderPermissions"
355416
UPDATE_INBOX_RULES = "UpdateInboxRules"
417+
418+
419+
class SharedMailbox(BaseModel):
420+
"""
421+
Model for Exchange Online shared mailbox.
422+
423+
Attributes:
424+
name: Display name of the shared mailbox.
425+
user_principal_name: User principal name (email) of the shared mailbox.
426+
external_directory_object_id: The Entra ID object ID for cross-referencing.
427+
identity: Identity of the shared mailbox in Exchange.
428+
"""
429+
430+
name: str
431+
user_principal_name: str
432+
external_directory_object_id: str
433+
identity: str

prowler/providers/m365/services/exchange/exchange_shared_mailbox_sign_in_disabled/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "exchange_shared_mailbox_sign_in_disabled",
4+
"CheckTitle": "Shared mailbox has sign-in blocked",
5+
"CheckType": [],
6+
"ServiceName": "exchange",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "Shared Mailbox",
11+
"ResourceGroup": "IAM",
12+
"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.",
13+
"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.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://learn.microsoft.com/en-us/microsoft-365/admin/email/about-shared-mailboxes",
17+
"https://learn.microsoft.com/en-us/microsoft-365/admin/email/create-a-shared-mailbox#block-sign-in-for-the-shared-mailbox-account"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "Get-EXOMailbox -RecipientTypeDetails SharedMailbox | ForEach-Object { Update-MgUser -UserId $_.ExternalDirectoryObjectId -AccountEnabled:$false }",
22+
"NativeIaC": "",
23+
"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",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"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.",
28+
"Url": "https://hub.prowler.com/check/exchange_shared_mailbox_sign_in_disabled"
29+
}
30+
},
31+
"Categories": [
32+
"identity-access"
33+
],
34+
"DependsOn": [],
35+
"RelatedTo": [],
36+
"Notes": ""
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from typing import List
2+
3+
from prowler.lib.check.models import Check, CheckReportM365
4+
from prowler.providers.m365.services.entra.entra_client import entra_client
5+
from prowler.providers.m365.services.exchange.exchange_client import exchange_client
6+
7+
8+
class exchange_shared_mailbox_sign_in_disabled(Check):
9+
"""
10+
Verify that sign-in is blocked for all shared mailboxes.
11+
12+
Shared mailboxes are designed for collaboration and should not permit direct
13+
sign-in. Users should access shared mailboxes through delegation only, which
14+
ensures accountability and proper access controls.
15+
16+
- PASS: Shared mailbox has sign-in blocked (AccountEnabled = False in Entra ID).
17+
- FAIL: Shared mailbox has sign-in enabled (AccountEnabled = True in Entra ID).
18+
"""
19+
20+
def execute(self) -> List[CheckReportM365]:
21+
"""
22+
Execute the check to verify shared mailbox sign-in status.
23+
24+
Cross-references shared mailboxes from Exchange Online with user accounts
25+
in Entra ID to determine if sign-in is blocked.
26+
27+
Returns:
28+
List[CheckReportM365]: A list of reports with the sign-in status for
29+
each shared mailbox.
30+
"""
31+
findings = []
32+
33+
for shared_mailbox in exchange_client.shared_mailboxes:
34+
report = CheckReportM365(
35+
metadata=self.metadata(),
36+
resource=shared_mailbox,
37+
resource_name=shared_mailbox.name or shared_mailbox.user_principal_name,
38+
resource_id=shared_mailbox.external_directory_object_id
39+
or shared_mailbox.identity,
40+
)
41+
42+
# Look up the user in Entra ID by their external directory object ID
43+
entra_user = entra_client.users.get(
44+
shared_mailbox.external_directory_object_id
45+
)
46+
47+
if not entra_user:
48+
report.status = "FAIL"
49+
report.status_extended = f"Shared mailbox {shared_mailbox.user_principal_name} could not be found in Entra ID for verification."
50+
elif entra_user.account_enabled:
51+
report.status = "FAIL"
52+
report.status_extended = f"Shared mailbox {shared_mailbox.user_principal_name} has sign-in enabled."
53+
else:
54+
report.status = "PASS"
55+
report.status_extended = f"Shared mailbox {shared_mailbox.user_principal_name} has sign-in blocked."
56+
57+
findings.append(report)
58+
59+
return findings

tests/providers/m365/services/exchange/exchange_service_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
MailboxAuditProperties,
1010
Organization,
1111
RoleAssignmentPolicy,
12+
SharedMailbox,
1213
TransportConfig,
1314
TransportRule,
1415
)
@@ -143,6 +144,23 @@ def mock_exchange_get_mailbox_audit_properties(_):
143144
]
144145

145146

147+
def mock_exchange_get_shared_mailboxes(_):
148+
return [
149+
SharedMailbox(
150+
name="Support Mailbox",
151+
user_principal_name="[email protected]",
152+
external_directory_object_id="12345678-1234-1234-1234-123456789012",
153+
identity="[email protected]",
154+
),
155+
SharedMailbox(
156+
name="Info Mailbox",
157+
user_principal_name="[email protected]",
158+
external_directory_object_id="87654321-4321-4321-4321-210987654321",
159+
identity="[email protected]",
160+
),
161+
]
162+
163+
146164
class Test_Exchange_Service:
147165
def test_get_client(self):
148166
with (
@@ -429,3 +447,37 @@ def test_get_role_assignment_policies(self):
429447
assert role_assignment_policies[1].assigned_roles == []
430448

431449
exchange_client.powershell.close()
450+
451+
@patch(
452+
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_shared_mailboxes",
453+
new=mock_exchange_get_shared_mailboxes,
454+
)
455+
def test_get_shared_mailboxes(self):
456+
with (
457+
mock.patch(
458+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
459+
),
460+
):
461+
exchange_client = Exchange(
462+
set_mocked_m365_provider(
463+
identity=M365IdentityInfo(tenant_domain=DOMAIN)
464+
)
465+
)
466+
shared_mailboxes = exchange_client.shared_mailboxes
467+
assert len(shared_mailboxes) == 2
468+
assert shared_mailboxes[0].name == "Support Mailbox"
469+
assert shared_mailboxes[0].user_principal_name == "[email protected]"
470+
assert (
471+
shared_mailboxes[0].external_directory_object_id
472+
== "12345678-1234-1234-1234-123456789012"
473+
)
474+
assert shared_mailboxes[0].identity == "[email protected]"
475+
assert shared_mailboxes[1].name == "Info Mailbox"
476+
assert shared_mailboxes[1].user_principal_name == "[email protected]"
477+
assert (
478+
shared_mailboxes[1].external_directory_object_id
479+
== "87654321-4321-4321-4321-210987654321"
480+
)
481+
assert shared_mailboxes[1].identity == "[email protected]"
482+
483+
exchange_client.powershell.close()

0 commit comments

Comments
 (0)