Skip to content

Commit 6935c4e

Browse files
authored
feat(m365): add entra_app_enforced_restrictions security check (#10058)
1 parent e47f2b4 commit 6935c4e

File tree

22 files changed

+1418
-79
lines changed

22 files changed

+1418
-79
lines changed

prowler/CHANGELOG.md

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

77
### 🚀 Added
88

9+
- `entra_app_enforced_restrictions` check for M365 provider [(#10058)](https://github.com/prowler-cloud/prowler/pull/10058)
910
- `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
1011
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
1112
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)

prowler/compliance/m365/iso27001_2022_m365.json

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,17 @@
174174
}
175175
],
176176
"Checks": [
177-
"teams_external_file_sharing_restricted",
177+
"entra_app_enforced_restrictions",
178+
"exchange_transport_config_smtp_auth_disabled",
179+
"exchange_transport_rules_mail_forwarding_disabled",
180+
"exchange_transport_rules_whitelist_disabled",
178181
"sharepoint_external_sharing_managed",
179182
"sharepoint_external_sharing_restricted",
180183
"sharepoint_guest_sharing_restricted",
181184
"sharepoint_modern_authentication_required",
182185
"sharepoint_onedrive_sync_restricted_unmanaged_devices",
183186
"teams_external_file_sharing_restricted",
184-
"exchange_transport_config_smtp_auth_disabled",
185-
"exchange_transport_rules_mail_forwarding_disabled",
186-
"exchange_transport_rules_whitelist_disabled"
187+
"teams_external_file_sharing_restricted"
187188
]
188189
},
189190
{
@@ -612,11 +613,12 @@
612613
],
613614
"Checks": [
614615
"defenderxdr_endpoint_privileged_user_exposed_credentials",
616+
"entra_admin_users_phishing_resistant_mfa_enabled",
617+
"entra_app_enforced_restrictions",
615618
"entra_managed_device_required_for_authentication",
616-
"entra_users_mfa_enabled",
617619
"entra_managed_device_required_for_mfa_registration",
618-
"entra_admin_users_phishing_resistant_mfa_enabled",
619-
"entra_users_mfa_capable"
620+
"entra_users_mfa_capable",
621+
"entra_users_mfa_enabled"
620622
]
621623
},
622624
{
@@ -665,9 +667,10 @@
665667
}
666668
],
667669
"Checks": [
668-
"sharepoint_external_sharing_restricted",
669670
"entra_admin_portals_access_restriction",
670-
"entra_policy_guest_users_access_restrictions"
671+
"entra_app_enforced_restrictions",
672+
"entra_policy_guest_users_access_restrictions",
673+
"sharepoint_external_sharing_restricted"
671674
]
672675
},
673676
{
@@ -749,7 +752,8 @@
749752
"Checks": [
750753
"defender_antiphishing_policy_configured",
751754
"defender_safelinks_policy_enabled",
752-
"entra_admin_users_phishing_resistant_mfa_enabled"
755+
"entra_admin_users_phishing_resistant_mfa_enabled",
756+
"entra_app_enforced_restrictions"
753757
]
754758
},
755759
{

prowler/compliance/m365/prowler_threatscore_m365.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,7 @@
823823
"Id": "1.3.9",
824824
"Description": "Ensure OneDrive sync is restricted for unmanaged devices",
825825
"Checks": [
826+
"entra_app_enforced_restrictions",
826827
"sharepoint_onedrive_sync_restricted_unmanaged_devices"
827828
],
828829
"Attributes": [

prowler/providers/m365/services/entra/entra_app_enforced_restrictions/__init__.py

Whitespace-only changes.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "entra_app_enforced_restrictions",
4+
"CheckTitle": "Conditional Access policy enforces application restrictions for unmanaged devices",
5+
"CheckType": [],
6+
"ServiceName": "entra",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "Conditional Access Policy",
11+
"ResourceGroup": "IAM",
12+
"Description": "Conditional Access policy with **application enforced restrictions** limits access to SharePoint, OneDrive, and Exchange content from unmanaged devices.\n\nThis control helps prevent data exfiltration by restricting download, print, and sync capabilities on devices that are not managed by the organization.",
13+
"Risk": "Without application enforced restrictions, users accessing SharePoint, OneDrive, and Exchange from unmanaged devices can:\n\n- **Download** sensitive files to personal devices\n- **Print** confidential documents\n- **Sync** corporate data to uncontrolled locations\n\nThis increases the risk of data leakage and unauthorized access to sensitive information.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-app-enforced-restriction",
17+
"https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "",
22+
"NativeIaC": "",
23+
"Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **All users**.\n5. Under **Target resources**, select **Office 365** from the cloud apps.\n6. Under **Conditions** > **Client apps**, select **All client apps**.\n7. Under **Session**, check **Use app enforced restrictions**.\n8. Set the policy to **On** and click **Create**.",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"Text": "Configure Conditional Access policies with **application enforced restrictions** to control access from unmanaged devices. Apply this to Office 365 applications (SharePoint, OneDrive, Exchange) to limit download, print, and sync operations.\n\nCombine with SharePoint access control settings for comprehensive protection.",
28+
"Url": "https://hub.prowler.com/check/entra_app_enforced_restrictions"
29+
}
30+
},
31+
"Categories": [
32+
"e3"
33+
],
34+
"DependsOn": [],
35+
"RelatedTo": [
36+
"entra_managed_device_required_for_authentication"
37+
],
38+
"Notes": "Application enforced restrictions only work with Exchange Online and SharePoint Online (including OneDrive)."
39+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from prowler.lib.check.models import Check, CheckReportM365
2+
from prowler.providers.m365.services.entra.entra_client import entra_client
3+
from prowler.providers.m365.services.entra.entra_service import (
4+
ClientAppType,
5+
ConditionalAccessPolicyState,
6+
)
7+
8+
9+
class entra_app_enforced_restrictions(Check):
10+
"""Check if at least one Conditional Access policy enforces application restrictions.
11+
12+
This check verifies that the tenant has at least one enabled Conditional Access policy
13+
with application enforced restrictions to protect SharePoint, OneDrive, and Exchange
14+
from unmanaged devices.
15+
16+
- PASS: At least one policy is enabled with application enforced restrictions targeting
17+
all users, all client app types, and either the Office365 suite or
18+
SharePoint Online and Exchange Online individually.
19+
- FAIL: No policy meets the criteria for application enforced restrictions.
20+
"""
21+
22+
# SharePoint Online / OneDrive for Business
23+
SHAREPOINT_APP_ID = "00000003-0000-0ff1-ce00-000000000000"
24+
# Exchange Online
25+
EXCHANGE_APP_ID = "00000002-0000-0ff1-ce00-000000000000"
26+
# Office 365 suite (includes SharePoint, OneDrive, and Exchange)
27+
OFFICE365_APP_ID = "Office365"
28+
29+
REQUIRED_APPS = {SHAREPOINT_APP_ID, EXCHANGE_APP_ID}
30+
MODERN_CLIENT_APP_TYPES = {
31+
ClientAppType.BROWSER,
32+
ClientAppType.MOBILE_APPS_AND_DESKTOP_CLIENTS,
33+
}
34+
35+
def _targets_all_client_apps(self, client_app_types: list[ClientAppType]) -> bool:
36+
"""Check if the policy targets all modern client app types.
37+
38+
Returns True if the policy includes ALL explicitly or both
39+
Browser and Mobile apps and desktop clients.
40+
"""
41+
client_app_set = set(client_app_types)
42+
if ClientAppType.ALL in client_app_set:
43+
return True
44+
return self.MODERN_CLIENT_APP_TYPES.issubset(client_app_set)
45+
46+
def _targets_required_apps(self, included_applications: list[str]) -> bool:
47+
"""Check if the policy targets the required applications.
48+
49+
Returns True if the policy includes Office365 (the suite) or both
50+
SharePoint Online and Exchange Online individually.
51+
"""
52+
if self.OFFICE365_APP_ID in included_applications:
53+
return True
54+
return self.REQUIRED_APPS.issubset(set(included_applications))
55+
56+
def execute(self) -> list[CheckReportM365]:
57+
"""Execute the check for application enforced restrictions in Conditional Access policies.
58+
59+
Returns:
60+
list[CheckReportM365]: A list containing the result of the check.
61+
"""
62+
findings = []
63+
report = CheckReportM365(
64+
metadata=self.metadata(),
65+
resource={},
66+
resource_name="Conditional Access Policies",
67+
resource_id="conditionalAccessPolicies",
68+
)
69+
report.status = "FAIL"
70+
report.status_extended = "No Conditional Access Policy enforces application restrictions for unmanaged devices."
71+
72+
for policy in entra_client.conditional_access_policies.values():
73+
if policy.state == ConditionalAccessPolicyState.DISABLED:
74+
continue
75+
76+
if "All" not in policy.conditions.user_conditions.included_users:
77+
continue
78+
79+
if not self._targets_all_client_apps(policy.conditions.client_app_types):
80+
continue
81+
82+
if not self._targets_required_apps(
83+
policy.conditions.application_conditions.included_applications
84+
):
85+
continue
86+
87+
if (
88+
not policy.session_controls.application_enforced_restrictions
89+
or not policy.session_controls.application_enforced_restrictions.is_enabled
90+
):
91+
continue
92+
93+
report = CheckReportM365(
94+
metadata=self.metadata(),
95+
resource=policy,
96+
resource_name=policy.display_name,
97+
resource_id=policy.id,
98+
)
99+
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
100+
report.status = "FAIL"
101+
report.status_extended = f"Conditional Access Policy {policy.display_name} reports application enforced restrictions but does not enforce them."
102+
else:
103+
report.status = "PASS"
104+
report.status_extended = f"Conditional Access Policy {policy.display_name} enforces application restrictions for unmanaged devices."
105+
break
106+
107+
findings.append(report)
108+
return findings

prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -84,28 +84,25 @@ def execute(self) -> list[CheckReportM365]:
8484
users_excluded_from_all or groups_excluded_from_all
8585
)
8686

87-
for policy in enabled_policies:
88-
report = CheckReportM365(
89-
metadata=self.metadata(),
90-
resource=policy,
91-
resource_name=policy.display_name,
92-
resource_id=policy.id,
93-
)
94-
95-
if has_emergency_exclusion:
96-
report.status = "PASS"
97-
exclusion_details = []
98-
if users_excluded_from_all:
99-
exclusion_details.append(f"{len(users_excluded_from_all)} user(s)")
100-
if groups_excluded_from_all:
101-
exclusion_details.append(
102-
f"{len(groups_excluded_from_all)} group(s)"
103-
)
104-
report.status_extended = f"Conditional Access Policy '{policy.display_name}' has {' and '.join(exclusion_details)} excluded as emergency access across all {total_policy_count} enabled policies."
105-
else:
106-
report.status = "FAIL"
107-
report.status_extended = f"Conditional Access Policy '{policy.display_name}' does not have any user or group excluded as emergency access from all enabled Conditional Access policies."
87+
report = CheckReportM365(
88+
metadata=self.metadata(),
89+
resource={},
90+
resource_name="Conditional Access Policies",
91+
resource_id="conditionalAccessPolicies",
92+
)
10893

109-
findings.append(report)
94+
if has_emergency_exclusion:
95+
report.status = "PASS"
96+
exclusion_details = []
97+
if users_excluded_from_all:
98+
exclusion_details.append(f"{len(users_excluded_from_all)} user(s)")
99+
if groups_excluded_from_all:
100+
exclusion_details.append(f"{len(groups_excluded_from_all)} group(s)")
101+
report.status_extended = f"{' and '.join(exclusion_details)} excluded as emergency access across all {total_policy_count} enabled Conditional Access policies."
102+
else:
103+
report.status = "FAIL"
104+
report.status_extended = f"No user or group is excluded as emergency access from all {total_policy_count} enabled Conditional Access policies."
105+
106+
findings.append(report)
110107

111108
return findings

prowler/providers/m365/services/entra/entra_service.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ async def _get_conditional_access_policies(self):
341341
else None
342342
),
343343
),
344+
application_enforced_restrictions=ApplicationEnforcedRestrictions(
345+
is_enabled=(
346+
policy.session_controls.application_enforced_restrictions.is_enabled
347+
if policy.session_controls
348+
and policy.session_controls.application_enforced_restrictions
349+
else False
350+
),
351+
),
344352
),
345353
state=ConditionalAccessPolicyState(
346354
getattr(policy, "state", "disabled")
@@ -735,9 +743,18 @@ class SignInFrequency(BaseModel):
735743
interval: Optional[SignInFrequencyInterval]
736744

737745

746+
class ApplicationEnforcedRestrictions(BaseModel):
747+
"""Model representing application enforced restrictions session control."""
748+
749+
is_enabled: bool = False
750+
751+
738752
class SessionControls(BaseModel):
753+
"""Model representing session controls for Conditional Access policies."""
754+
739755
persistent_browser: PersistentBrowser
740756
sign_in_frequency: SignInFrequency
757+
application_enforced_restrictions: Optional[ApplicationEnforcedRestrictions] = None
741758

742759

743760
class ConditionalAccessGrantControl(Enum):

tests/providers/m365/services/entra/entra_admin_portals_access_restriction/entra_admin_portals_access_restriction_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from uuid import uuid4
33

44
from prowler.providers.m365.services.entra.entra_service import (
5+
ApplicationEnforcedRestrictions,
56
ApplicationsConditions,
67
ConditionalAccessGrantControl,
78
ConditionalAccessPolicyState,
@@ -106,6 +107,9 @@ def test_entra_admin_center_limited_access_disabled(self):
106107
type=None,
107108
interval=SignInFrequencyInterval.EVERY_TIME,
108109
),
110+
application_enforced_restrictions=ApplicationEnforcedRestrictions(
111+
is_enabled=False
112+
),
109113
),
110114
state=ConditionalAccessPolicyState.DISABLED,
111115
)
@@ -181,6 +185,9 @@ def test_entra_admin_center_limited_access_enabled_for_reporting(self):
181185
type=None,
182186
interval=SignInFrequencyInterval.EVERY_TIME,
183187
),
188+
application_enforced_restrictions=ApplicationEnforcedRestrictions(
189+
is_enabled=False
190+
),
184191
),
185192
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
186193
)
@@ -259,6 +266,9 @@ def test_entra_admin_center_limited_access_enabled(self):
259266
type=None,
260267
interval=SignInFrequencyInterval.EVERY_TIME,
261268
),
269+
application_enforced_restrictions=ApplicationEnforcedRestrictions(
270+
is_enabled=False
271+
),
262272
),
263273
state=ConditionalAccessPolicyState.ENABLED,
264274
)

0 commit comments

Comments
 (0)