Skip to content

Commit fd9808f

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

File tree

9 files changed

+217
-0
lines changed

9 files changed

+217
-0
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
3636
- Add new check `teams_meeting_presenters_restricted` [(#7613)](https://github.com/prowler-cloud/prowler/pull/7613)
3737
- Add new check `teams_meeting_chat_anonymous_users_disabled` [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579)
3838
- Add Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
39+
- Add new check for Modern Authentication enabled for Exchange Online in M365 [(#7636)](https://github.com/prowler-cloud/prowler/pull/7636)
3940
- Add new check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
4041
- Add new check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
4142
- Add new check for Roles Assignment Policy with no AddIns for Exchange in M365 [(#7644)](https://github.com/prowler-cloud/prowler/pull/7644)

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "exchange_organization_modern_authentication_enabled",
4+
"CheckTitle": "Ensure Modern Authentication for Exchange Online is enabled.",
5+
"CheckType": [],
6+
"ServiceName": "exchange",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "critical",
10+
"ResourceType": "Exchange Organization Configuration",
11+
"Description": "Ensure that modern authentication is enabled for Exchange Online, requiring exchange and mailboxes clients to use strong authentication mechanisms instead of basic authentication.",
12+
"Risk": "If modern authentication is not enabled, Exchange Online email clients may fall back to basic authentication, making it easier for attackers to bypass multifactor authentication and compromise user credentials.",
13+
"RelatedUrl": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online",
14+
"Remediation": {
15+
"Code": {
16+
"CLI": "Set-OrganizationConfig -OAuth2ClientProfileEnabled $True",
17+
"NativeIaC": "",
18+
"Other": "",
19+
"Terraform": ""
20+
},
21+
"Recommendation": {
22+
"Text": "Enable modern authentication in Exchange Online to enforce secure authentication methods for email clients.",
23+
"Url": "https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/enable-or-disable-modern-authentication-in-exchange-online"
24+
}
25+
},
26+
"Categories": [],
27+
"DependsOn": [],
28+
"RelatedTo": [],
29+
"Notes": ""
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
6+
7+
class exchange_organization_modern_authentication_enabled(Check):
8+
"""
9+
Check if Modern Authentication is enabled for Exchange Online.
10+
11+
Attributes:
12+
metadata: Metadata associated with the check (inherited from Check).
13+
"""
14+
15+
def execute(self) -> List[CheckReportM365]:
16+
"""
17+
Execute the check for Modern Authentication in Exchange Online.
18+
19+
This method checks if Modern Authentication is enabled in the Exchange organization configuration.
20+
21+
Returns:
22+
List[CheckReportM365]: A list of reports containing the result of the check.
23+
"""
24+
findings = []
25+
organization_config = exchange_client.organization_config
26+
if organization_config:
27+
report = CheckReportM365(
28+
metadata=self.metadata(),
29+
resource=organization_config,
30+
resource_name=organization_config.name,
31+
resource_id=organization_config.guid,
32+
)
33+
report.status = "FAIL"
34+
report.status_extended = (
35+
"Modern Authentication is not enabled for Exchange Online."
36+
)
37+
38+
if organization_config.oauth_enabled:
39+
report.status = "PASS"
40+
report.status_extended = (
41+
"Modern Authentication is enabled for Exchange Online."
42+
)
43+
44+
findings.append(report)
45+
46+
return findings

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def _get_organization_config(self):
4444
audit_disabled=organization_configuration.get(
4545
"AuditDisabled", False
4646
),
47+
oauth_enabled=organization_configuration.get(
48+
"OAuth2ClientProfileEnabled", True
49+
),
4750
mailtips_enabled=organization_configuration.get(
4851
"MailTipsAllTipsEnabled", True
4952
),
@@ -235,6 +238,7 @@ class Organization(BaseModel):
235238
name: str
236239
guid: str
237240
audit_disabled: bool
241+
oauth_enabled: bool
238242
mailtips_enabled: bool
239243
mailtips_external_recipient_enabled: bool
240244
mailtips_group_metrics_enabled: bool

tests/providers/m365/services/exchange/exchange_organization_mailbox_auditing_enabled/exchange_organization_mailbox_auditing_enabled_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def test_audit_log_search_disabled(self):
6060
audit_disabled=True,
6161
name="test",
6262
guid="test",
63+
oauth_enabled=True,
6364
mailtips_enabled=True,
6465
mailtips_external_recipient_enabled=True,
6566
mailtips_group_metrics_enabled=True,
@@ -108,6 +109,7 @@ def test_audit_log_search_enabled(self):
108109
audit_disabled=False,
109110
name="test",
110111
guid="test",
112+
oauth_enabled=True,
111113
mailtips_enabled=True,
112114
mailtips_external_recipient_enabled=True,
113115
mailtips_group_metrics_enabled=True,

tests/providers/m365/services/exchange/exchange_organization_mailtips_enabled/exchange_organization_mailtips_enabled_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def test_mailtips_not_fully_enabled(self):
6464
name="test-org",
6565
guid="org-guid",
6666
audit_disabled=False,
67+
oauth_enabled=True,
6768
mailtips_enabled=False,
6869
mailtips_external_recipient_enabled=False,
6970
mailtips_group_metrics_enabled=True,
@@ -116,6 +117,7 @@ def test_mailtips_fully_enabled(self):
116117
name="test-org",
117118
guid="org-guid",
118119
audit_disabled=False,
120+
oauth_enabled=True,
119121
mailtips_enabled=True,
120122
mailtips_external_recipient_enabled=True,
121123
mailtips_group_metrics_enabled=True,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from unittest import mock
2+
3+
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
4+
5+
6+
class Test_exchange_organization_modern_authentication_enabled:
7+
def test_no_organization(self):
8+
exchange_client = mock.MagicMock()
9+
exchange_client.audited_tenant = "audited_tenant"
10+
exchange_client.audited_domain = DOMAIN
11+
exchange_client.organization_config = None
12+
13+
with (
14+
mock.patch(
15+
"prowler.providers.common.provider.Provider.get_global_provider",
16+
return_value=set_mocked_m365_provider(),
17+
),
18+
mock.patch(
19+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
20+
),
21+
mock.patch(
22+
"prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled.exchange_client",
23+
new=exchange_client,
24+
),
25+
):
26+
from prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled import (
27+
exchange_organization_modern_authentication_enabled,
28+
)
29+
30+
check = exchange_organization_modern_authentication_enabled()
31+
result = check.execute()
32+
assert len(result) == 0
33+
34+
def test_modern_authentication_disabled(self):
35+
exchange_client = mock.MagicMock()
36+
exchange_client.audited_tenant = "audited_tenant"
37+
exchange_client.audited_domain = DOMAIN
38+
39+
with (
40+
mock.patch(
41+
"prowler.providers.common.provider.Provider.get_global_provider",
42+
return_value=set_mocked_m365_provider(),
43+
),
44+
mock.patch(
45+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
46+
),
47+
mock.patch(
48+
"prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled.exchange_client",
49+
new=exchange_client,
50+
),
51+
):
52+
from prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled import (
53+
exchange_organization_modern_authentication_enabled,
54+
)
55+
from prowler.providers.m365.services.exchange.exchange_service import (
56+
Organization,
57+
)
58+
59+
exchange_client.organization_config = Organization(
60+
oauth_enabled=False,
61+
name="test",
62+
guid="test",
63+
audit_disabled=False,
64+
mailtips_enabled=False,
65+
mailtips_external_recipient_enabled=False,
66+
mailtips_group_metrics_enabled=True,
67+
mailtips_large_audience_threshold=25,
68+
)
69+
70+
check = exchange_organization_modern_authentication_enabled()
71+
result = check.execute()
72+
assert len(result) == 1
73+
assert result[0].status == "FAIL"
74+
assert (
75+
result[0].status_extended
76+
== "Modern Authentication is not enabled for Exchange Online."
77+
)
78+
assert result[0].resource == exchange_client.organization_config.dict()
79+
assert result[0].resource_name == "test"
80+
assert result[0].resource_id == "test"
81+
assert result[0].location == "global"
82+
83+
def test_modern_authentication_enabled(self):
84+
exchange_client = mock.MagicMock()
85+
exchange_client.audited_tenant = "audited_tenant"
86+
exchange_client.audited_domain = DOMAIN
87+
88+
with (
89+
mock.patch(
90+
"prowler.providers.common.provider.Provider.get_global_provider",
91+
return_value=set_mocked_m365_provider(),
92+
),
93+
mock.patch(
94+
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
95+
),
96+
mock.patch(
97+
"prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled.exchange_client",
98+
new=exchange_client,
99+
),
100+
):
101+
from prowler.providers.m365.services.exchange.exchange_organization_modern_authentication_enabled.exchange_organization_modern_authentication_enabled import (
102+
exchange_organization_modern_authentication_enabled,
103+
)
104+
from prowler.providers.m365.services.exchange.exchange_service import (
105+
Organization,
106+
)
107+
108+
exchange_client.organization_config = Organization(
109+
oauth_enabled=True,
110+
name="test",
111+
guid="test",
112+
audit_disabled=False,
113+
mailtips_enabled=False,
114+
mailtips_external_recipient_enabled=False,
115+
mailtips_group_metrics_enabled=True,
116+
mailtips_large_audience_threshold=25,
117+
)
118+
119+
check = exchange_organization_modern_authentication_enabled()
120+
result = check.execute()
121+
assert len(result) == 1
122+
assert result[0].status == "PASS"
123+
assert (
124+
result[0].status_extended
125+
== "Modern Authentication is enabled for Exchange Online."
126+
)
127+
assert result[0].resource == exchange_client.organization_config.dict()
128+
assert result[0].resource_name == "test"
129+
assert result[0].resource_id == "test"
130+
assert result[0].location == "global"

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def mock_exchange_get_organization_config(_):
2121
audit_disabled=True,
2222
name="test",
2323
guid="test",
24+
oauth_enabled=True,
2425
mailtips_enabled=True,
2526
mailtips_external_recipient_enabled=False,
2627
mailtips_group_metrics_enabled=True,
@@ -183,6 +184,7 @@ def test_get_organization_config(self):
183184
assert organization_config.name == "test"
184185
assert organization_config.guid == "test"
185186
assert organization_config.audit_disabled is True
187+
assert organization_config.oauth_enabled is True
186188
assert organization_config.mailtips_enabled is True
187189
assert organization_config.mailtips_external_recipient_enabled is False
188190
assert organization_config.mailtips_group_metrics_enabled is True

0 commit comments

Comments
 (0)