Skip to content

Commit c29acba

Browse files
feat(exchange): add new check exchange_roles_assignment_policy_addins_disabled (#7644)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
1 parent 3cb8d2a commit c29acba

File tree

8 files changed

+323
-0
lines changed

8 files changed

+323
-0
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
3838
- Add Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
3939
- Add new check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
4040
- Add new check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
41+
- Add new check for Roles Assignment Policy with no AddIns for Exchange in M365 [(#7644)](https://github.com/prowler-cloud/prowler/pull/7644)
4142
- Add new check for Auditing Mailbox on E3 users is enabled for Exchange in M365 [(#7642)](https://github.com/prowler-cloud/prowler/pull/7642)
4243
- Add new check for SMTP Auth disabled for Exchange in M365 [(#7640)](https://github.com/prowler-cloud/prowler/pull/7640)
4344
- Add new check for MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637)

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,27 @@ def get_inbound_spam_filter_policy(self) -> dict:
487487
"Get-HostedContentFilterPolicy | ConvertTo-Json", json_parse=True
488488
)
489489

490+
def get_role_assignment_policies(self) -> dict:
491+
"""
492+
Get Role Assignment Policies.
493+
494+
Retrieves the current role assignment policies for Exchange Online.
495+
496+
Returns:
497+
dict: Role assignment policies in JSON format.
498+
499+
Example:
500+
>>> get_role_assignment_policies()
501+
{
502+
"Name": "Default Role Assignment Policy",
503+
"Guid": "12345678-1234-1234-1234-123456789012",
504+
"AssignedRoles": ["MyRole"]
505+
}
506+
"""
507+
return self.execute(
508+
"Get-RoleAssignmentPolicy | ConvertTo-Json", json_parse=True
509+
)
510+
490511
def get_mailbox_audit_properties(self) -> dict:
491512
"""
492513
Get Mailbox Properties.

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "exchange_roles_assignment_policy_addins_disabled",
4+
"CheckTitle": "Ensure there is no policy with Outlook add-ins allowed.",
5+
"CheckType": [],
6+
"ServiceName": "exchange",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "high",
10+
"ResourceType": "Exchange Role Assignment Policy",
11+
"Description": "Restricting users from installing Outlook add-ins reduces the risk of data exposure or exploitation through unapproved or vulnerable add-ins.",
12+
"Risk": "Allowing users to install add-ins may expose sensitive information or introduce malicious behavior through third-party integrations. Disabling this capability mitigates the risk of unauthorized data access.",
13+
"RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/add-ins-for-outlook/specify-who-can-install-and-manage-add-ins",
14+
"Remediation": {
15+
"Code": {
16+
"CLI": "$policy = \"Role Assignment Policy - Prevent Add-ins\"; $roles = \"MyTextMessaging\", \"MyDistributionGroups\", \"MyMailSubscriptions\", \"MyBaseOptions\", \"MyVoiceMail\", \"MyProfileInformation\", \"MyContactInformation\", \"MyRetentionPolicies\", \"MyDistributionGroupMembership\"; New-RoleAssignmentPolicy -Name $policy -Roles $roles; Set-RoleAssignmentPolicy -id $policy -IsDefault; Get-EXOMailbox -ResultSize Unlimited | Set-Mailbox -RoleAssignmentPolicy $policy",
17+
"NativeIaC": "",
18+
"Other": "1. Navigate to Exchange admin center https://admin.exchange.microsoft.com. 2. Click to expand Roles > User roles. 3. Select Default Role Assignment Policy. 4. In the right pane, click Manage permissions. 5. Uncheck My Custom Apps, My Marketplace Apps and My ReadWriteMailboxApps under Other roles. 6. Save changes.",
19+
"Terraform": ""
20+
},
21+
"Recommendation": {
22+
"Text": "Restrict Outlook add-in installation by updating the Role Assignment Policy to exclude roles that allow app installation.",
23+
"Url": "https://learn.microsoft.com/en-us/exchange/permissions-exo/role-assignment-policies"
24+
}
25+
},
26+
"Categories": [],
27+
"DependsOn": [],
28+
"RelatedTo": [],
29+
"Notes": ""
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import List
2+
3+
from prowler.lib.check.models import Check, CheckReportM365
4+
from prowler.providers.m365.services.exchange.exchange_client import exchange_client
5+
from prowler.providers.m365.services.exchange.exchange_service import AddinRoles
6+
7+
8+
class exchange_roles_assignment_policy_addins_disabled(Check):
9+
"""Check if any Exchange role assignment policy allows Outlook add-ins.
10+
11+
Attributes:
12+
metadata: Metadata associated with the check (inherited from Check).
13+
"""
14+
15+
def execute(self) -> List[CheckReportM365]:
16+
"""Execute the check for role assignment policies that allow Outlook add-ins.
17+
18+
This method checks all Exchange Online Role Assignment Policies to verify
19+
whether any of them allow the installation of add-ins by including risky roles.
20+
21+
Returns:
22+
List[CheckReportM365]: A list of reports containing the result of the check.
23+
"""
24+
findings = []
25+
26+
addin_roles = [e.value for e in AddinRoles]
27+
28+
for policy in exchange_client.role_assignment_policies:
29+
report = CheckReportM365(
30+
metadata=self.metadata(),
31+
resource=policy,
32+
resource_name=policy.name,
33+
resource_id=policy.id,
34+
)
35+
36+
report.status = "PASS"
37+
report.status_extended = f"Role assignment policy '{policy.name}' does not allow Outlook add-ins."
38+
39+
risky_roles_found = []
40+
for role in policy.assigned_roles:
41+
if role in addin_roles:
42+
risky_roles_found.append(role)
43+
44+
if risky_roles_found:
45+
report.status = "FAIL"
46+
report.status_extended = f"Role assignment policy '{policy.name}' allows Outlook add-ins via roles: {', '.join(risky_roles_found)}."
47+
48+
findings.append(report)
49+
50+
return findings

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(self, provider: M365Provider):
1717
self.transport_rules = []
1818
self.transport_config = None
1919
self.mailbox_policy = None
20+
self.role_assignment_policies = []
2021
self.mailbox_audit_properties = []
2122

2223
if self.powershell:
@@ -27,6 +28,7 @@ def __init__(self, provider: M365Provider):
2728
self.transport_rules = self._get_transport_rules()
2829
self.transport_config = self._get_transport_config()
2930
self.mailbox_policy = self._get_mailbox_policy()
31+
self.role_assignment_policies = self._get_role_assignment_policies()
3032
self.mailbox_audit_properties = self._get_mailbox_audit_properties()
3133
self.powershell.close()
3234

@@ -166,6 +168,30 @@ def _get_mailbox_policy(self):
166168
)
167169
return mailboxes_policy
168170

171+
def _get_role_assignment_policies(self):
172+
logger.info("Microsoft365 - Getting role assignment policies...")
173+
role_assignment_policies = []
174+
try:
175+
policies_data = self.powershell.get_role_assignment_policies()
176+
if not policies_data:
177+
return role_assignment_policies
178+
if isinstance(policies_data, dict):
179+
policies_data = [policies_data]
180+
for policy in policies_data:
181+
if policy:
182+
role_assignment_policies.append(
183+
RoleAssignmentPolicy(
184+
name=policy.get("Name", ""),
185+
id=policy.get("Guid", ""),
186+
assigned_roles=policy.get("AssignedRoles", []),
187+
)
188+
)
189+
except Exception as error:
190+
logger.error(
191+
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
192+
)
193+
return role_assignment_policies
194+
169195
def _get_mailbox_audit_properties(self):
170196
logger.info("Microsoft365 - Getting mailbox audit properties...")
171197
mailbox_audit_properties = []
@@ -241,6 +267,18 @@ class MailboxPolicy(BaseModel):
241267
additional_storage_enabled: bool
242268

243269

270+
class RoleAssignmentPolicy(BaseModel):
271+
name: str
272+
id: str
273+
assigned_roles: list[str]
274+
275+
276+
class AddinRoles(Enum):
277+
MY_CUSTOM_APPS = "My Custom Apps"
278+
MY_MARKETPLACE_APPS = "My Marketplace Apps"
279+
MY_READWRITE_MAILBOX_APPS = "My ReadWriteMailbox Apps"
280+
281+
244282
class MailboxAuditProperties(BaseModel):
245283
name: str
246284
audit_enabled: bool
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from unittest import mock
2+
3+
from prowler.providers.m365.services.exchange.exchange_service import (
4+
RoleAssignmentPolicy,
5+
)
6+
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
7+
8+
9+
class Test_exchange_roles_assignment_policy_addins_disabled:
10+
def test_no_policies(self):
11+
exchange_client = mock.MagicMock()
12+
exchange_client.audited_tenant = "audited_tenant"
13+
exchange_client.audited_domain = DOMAIN
14+
15+
with (
16+
mock.patch(
17+
"prowler.providers.common.provider.Provider.get_global_provider",
18+
return_value=set_mocked_m365_provider(),
19+
),
20+
mock.patch(
21+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
22+
),
23+
mock.patch(
24+
"prowler.providers.m365.services.exchange.exchange_roles_assignment_policy_addins_disabled.exchange_roles_assignment_policy_addins_disabled.exchange_client",
25+
new=exchange_client,
26+
),
27+
):
28+
from prowler.providers.m365.services.exchange.exchange_roles_assignment_policy_addins_disabled.exchange_roles_assignment_policy_addins_disabled import (
29+
exchange_roles_assignment_policy_addins_disabled,
30+
)
31+
32+
exchange_client.role_assignment_policies = []
33+
34+
check = exchange_roles_assignment_policy_addins_disabled()
35+
result = check.execute()
36+
37+
assert len(result) == 0
38+
39+
def test_policy_with_no_addin_roles(self):
40+
exchange_client = mock.MagicMock()
41+
exchange_client.audited_tenant = "audited_tenant"
42+
exchange_client.audited_domain = DOMAIN
43+
44+
with (
45+
mock.patch(
46+
"prowler.providers.common.provider.Provider.get_global_provider",
47+
return_value=set_mocked_m365_provider(),
48+
),
49+
mock.patch(
50+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
51+
),
52+
mock.patch(
53+
"prowler.providers.m365.services.exchange.exchange_roles_assignment_policy_addins_disabled.exchange_roles_assignment_policy_addins_disabled.exchange_client",
54+
new=exchange_client,
55+
),
56+
):
57+
from prowler.providers.m365.services.exchange.exchange_roles_assignment_policy_addins_disabled.exchange_roles_assignment_policy_addins_disabled import (
58+
exchange_roles_assignment_policy_addins_disabled,
59+
)
60+
61+
exchange_client.role_assignment_policies = [
62+
RoleAssignmentPolicy(
63+
name="Policy1",
64+
id="id-policy1",
65+
assigned_roles=["My Base Options", "My Voice Mail"],
66+
)
67+
]
68+
69+
check = exchange_roles_assignment_policy_addins_disabled()
70+
result = check.execute()
71+
72+
assert len(result) == 1
73+
assert result[0].status == "PASS"
74+
assert (
75+
result[0].status_extended
76+
== "Role assignment policy 'Policy1' does not allow Outlook add-ins."
77+
)
78+
assert result[0].resource_name == "Policy1"
79+
assert result[0].resource_id == "id-policy1"
80+
assert result[0].location == "global"
81+
assert (
82+
result[0].resource == exchange_client.role_assignment_policies[0].dict()
83+
)
84+
85+
def test_policy_with_addin_roles(self):
86+
exchange_client = mock.MagicMock()
87+
exchange_client.audited_tenant = "audited_tenant"
88+
exchange_client.audited_domain = DOMAIN
89+
90+
with (
91+
mock.patch(
92+
"prowler.providers.common.provider.Provider.get_global_provider",
93+
return_value=set_mocked_m365_provider(),
94+
),
95+
mock.patch(
96+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online",
97+
),
98+
mock.patch(
99+
"prowler.providers.m365.services.exchange.exchange_roles_assignment_policy_addins_disabled.exchange_roles_assignment_policy_addins_disabled.exchange_client",
100+
new=exchange_client,
101+
),
102+
):
103+
from prowler.providers.m365.services.exchange.exchange_roles_assignment_policy_addins_disabled.exchange_roles_assignment_policy_addins_disabled import (
104+
exchange_roles_assignment_policy_addins_disabled,
105+
)
106+
107+
exchange_client.role_assignment_policies = [
108+
RoleAssignmentPolicy(
109+
name="Policy2",
110+
id="id-policy2",
111+
assigned_roles=["My Custom Apps", "My Voice Mail"],
112+
)
113+
]
114+
115+
check = exchange_roles_assignment_policy_addins_disabled()
116+
result = check.execute()
117+
118+
assert len(result) == 1
119+
assert result[0].status == "FAIL"
120+
assert (
121+
result[0].status_extended
122+
== "Role assignment policy 'Policy2' allows Outlook add-ins via roles: My Custom Apps."
123+
)
124+
assert result[0].resource_name == "Policy2"
125+
assert result[0].resource_id == "id-policy2"
126+
assert result[0].location == "global"
127+
assert (
128+
result[0].resource == exchange_client.role_assignment_policies[0].dict()
129+
)

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
MailboxAuditProperties,
1010
MailboxPolicy,
1111
Organization,
12+
RoleAssignmentPolicy,
1213
TransportConfig,
1314
TransportRule,
1415
)
@@ -75,6 +76,27 @@ def mock_exchange_get_mailbox_policy(_):
7576
)
7677

7778

79+
def mock_exchange_get_role_assignment_policies(_):
80+
return [
81+
RoleAssignmentPolicy(
82+
name="Default Role Assignment Policy",
83+
id="12345678-1234-1234-1234",
84+
assigned_roles=[
85+
"MyProfileInformation",
86+
"MyDistributionGroupMembership",
87+
"MyRetentionPolicies",
88+
"MyDistributionGroups",
89+
"MyVoiceMail",
90+
],
91+
),
92+
RoleAssignmentPolicy(
93+
name="Test Policy",
94+
id="12345678-1234-1234",
95+
assigned_roles=[],
96+
),
97+
]
98+
99+
78100
def mock_exchange_get_mailbox_audit_properties(_):
79101
return [
80102
MailboxAuditProperties(
@@ -345,3 +367,35 @@ def test_get_mailbox_audit_properties(self):
345367
assert mailbox_audit_properties[0].audit_log_age == 90
346368
assert mailbox_audit_properties[0].identity == "test"
347369
exchange_client.powershell.close()
370+
371+
@patch(
372+
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_role_assignment_policies",
373+
new=mock_exchange_get_role_assignment_policies,
374+
)
375+
def test_get_role_assignment_policies(self):
376+
with (
377+
mock.patch(
378+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
379+
),
380+
):
381+
exchange_client = Exchange(
382+
set_mocked_m365_provider(
383+
identity=M365IdentityInfo(tenant_domain=DOMAIN)
384+
)
385+
)
386+
role_assignment_policies = exchange_client.role_assignment_policies
387+
assert len(role_assignment_policies) == 2
388+
assert role_assignment_policies[0].name == "Default Role Assignment Policy"
389+
assert role_assignment_policies[0].id == "12345678-1234-1234-1234"
390+
assert role_assignment_policies[0].assigned_roles == [
391+
"MyProfileInformation",
392+
"MyDistributionGroupMembership",
393+
"MyRetentionPolicies",
394+
"MyDistributionGroups",
395+
"MyVoiceMail",
396+
]
397+
assert role_assignment_policies[1].name == "Test Policy"
398+
assert role_assignment_policies[1].id == "12345678-1234-1234"
399+
assert role_assignment_policies[1].assigned_roles == []
400+
401+
exchange_client.powershell.close()

0 commit comments

Comments
 (0)