Skip to content

Commit 726b566

Browse files
feat(m365): add entra_conditional_access_policy_approved_client_app_required_for_mobile security check (#10216)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
1 parent 5968441 commit 726b566

File tree

7 files changed

+1423
-0
lines changed

7 files changed

+1423
-0
lines changed

prowler/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
44

55
## [5.20.0] (Prowler UNRELEASED)
66

7+
### 🚀 Added
8+
9+
- `entra_conditional_access_policy_approved_client_app_required_for_mobile` check for m365 provider [(#10216)](https://github.com/prowler-cloud/prowler/pull/10216)
10+
711
### 🔄 Changed
812

913
- Update Kubernetes API server checks metadata to new format [(#9674)](https://github.com/prowler-cloud/prowler/pull/9674)

prowler/compliance/m365/iso27001_2022_m365.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@
616616
"Checks": [
617617
"defenderxdr_endpoint_privileged_user_exposed_credentials",
618618
"entra_admin_users_phishing_resistant_mfa_enabled",
619+
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
619620
"entra_conditional_access_policy_app_enforced_restrictions",
620621
"entra_managed_device_required_for_authentication",
621622
"entra_managed_device_required_for_mfa_registration",
@@ -671,6 +672,7 @@
671672
],
672673
"Checks": [
673674
"entra_admin_portals_access_restriction",
675+
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
674676
"entra_conditional_access_policy_app_enforced_restrictions",
675677
"entra_policy_guest_users_access_restrictions",
676678
"sharepoint_external_sharing_restricted"
@@ -692,6 +694,7 @@
692694
"entra_admin_users_mfa_enabled",
693695
"entra_admin_users_sign_in_frequency_enabled",
694696
"entra_all_apps_conditional_access_coverage",
697+
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
695698
"entra_break_glass_account_fido2_security_key_registered",
696699
"entra_identity_protection_sign_in_risk_enabled",
697700
"entra_managed_device_required_for_authentication",

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "entra_conditional_access_policy_approved_client_app_required_for_mobile",
4+
"CheckTitle": "Conditional Access policy enforces approved client apps or app protection for mobile devices",
5+
"CheckType": [],
6+
"ServiceName": "entra",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "medium",
10+
"ResourceType": "NotDefined",
11+
"ResourceGroup": "IAM",
12+
"Description": "Conditional Access policies can require that only **approved client apps** or apps with **app protection policies** are used on iOS and Android devices. This ensures corporate data on mobile platforms is accessed only through managed or protected applications.",
13+
"Risk": "Without requiring approved or protected client apps on mobile platforms, users can access corporate data through **unmanaged applications** that lack security controls. This increases the risk of **data leakage**, unauthorized data sharing, and exposure of sensitive information on personal or compromised mobile devices.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-approved-app-or-app-protection",
17+
"https://learn.microsoft.com/en-us/mem/intune/apps/app-protection-policy"
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. Select **New policy**.\n4. Under **Users**, include **All users**.\n5. Under **Target resources**, include **All cloud apps**.\n6. Under **Conditions** > **Device platforms**, select **Android** and **iOS**.\n7. Under **Grant**, select **Require app protection policy** (preferred) or **Require approved client app**.\n8. Set the operator to **OR**.\n9. Enable the policy and click **Create**.",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"Text": "Enforce Conditional Access policies requiring app protection policies or approved client apps on iOS and Android devices. Prefer **app protection policies** over approved client apps, as the approved client app grant control is retiring June 30, 2026. Regularly review policies to ensure mobile access is restricted to managed applications.",
28+
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_approved_client_app_required_for_mobile"
29+
}
30+
},
31+
"Categories": [
32+
"e3"
33+
],
34+
"DependsOn": [],
35+
"RelatedTo": [],
36+
"Notes": "The 'Require approved client app' grant control is retiring June 30, 2026. Organizations should migrate to 'Require app protection policy' (compliantApplication)."
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
ConditionalAccessGrantControl,
5+
ConditionalAccessPolicyState,
6+
GrantControlOperator,
7+
)
8+
9+
10+
class entra_conditional_access_policy_approved_client_app_required_for_mobile(Check):
11+
"""Check if a Conditional Access policy requires approved client apps or app protection for mobile devices.
12+
13+
This check ensures that at least one enabled Conditional Access policy
14+
targets iOS and Android platforms and requires approved client apps or
15+
app protection policies.
16+
- PASS: An enabled policy requires approved client apps or app protection for iOS/Android.
17+
- FAIL: No policy restricts mobile app access to approved or protected apps.
18+
"""
19+
20+
REQUIRED_MOBILE_PLATFORMS = {"android", "ios"}
21+
MOBILE_APP_GRANT_CONTROLS = {
22+
ConditionalAccessGrantControl.APPROVED_APPLICATION,
23+
ConditionalAccessGrantControl.COMPLIANT_APPLICATION,
24+
}
25+
26+
@staticmethod
27+
def _normalize_platform(platform: object) -> str:
28+
normalized_platform = getattr(platform, "value", platform)
29+
return (
30+
normalized_platform.lower() if isinstance(normalized_platform, str) else ""
31+
)
32+
33+
def execute(self) -> list[CheckReportM365]:
34+
"""Execute the check logic.
35+
36+
Returns:
37+
A list of reports containing the result of the check.
38+
"""
39+
findings = []
40+
41+
report = CheckReportM365(
42+
metadata=self.metadata(),
43+
resource={},
44+
resource_name="Conditional Access Policies",
45+
resource_id="conditionalAccessPolicies",
46+
)
47+
report.status = "FAIL"
48+
report.status_extended = "No Conditional Access Policy requires approved client apps or app protection for mobile devices."
49+
50+
for policy in entra_client.conditional_access_policies.values():
51+
if policy.state == ConditionalAccessPolicyState.DISABLED:
52+
continue
53+
54+
if not policy.conditions.platform_conditions:
55+
continue
56+
57+
included_platforms = {
58+
normalized_platform
59+
for normalized_platform in map(
60+
self._normalize_platform,
61+
policy.conditions.platform_conditions.include_platforms,
62+
)
63+
if normalized_platform
64+
}
65+
excluded_platforms = {
66+
normalized_platform
67+
for normalized_platform in map(
68+
self._normalize_platform,
69+
policy.conditions.platform_conditions.exclude_platforms,
70+
)
71+
if normalized_platform
72+
}
73+
74+
targets_mobile_platforms = (
75+
"all" in included_platforms
76+
or self.REQUIRED_MOBILE_PLATFORMS.issubset(included_platforms)
77+
) and not (
78+
"all" in excluded_platforms
79+
or self.REQUIRED_MOBILE_PLATFORMS.intersection(excluded_platforms)
80+
)
81+
if not targets_mobile_platforms:
82+
continue
83+
84+
built_in_controls = set(policy.grant_controls.built_in_controls)
85+
has_mobile_app_control = bool(
86+
self.MOBILE_APP_GRANT_CONTROLS.intersection(built_in_controls)
87+
)
88+
if not has_mobile_app_control:
89+
continue
90+
91+
if (
92+
policy.grant_controls.operator == GrantControlOperator.OR
93+
and not built_in_controls.issubset(self.MOBILE_APP_GRANT_CONTROLS)
94+
):
95+
continue
96+
97+
report = CheckReportM365(
98+
metadata=self.metadata(),
99+
resource=policy,
100+
resource_name=policy.display_name,
101+
resource_id=policy.id,
102+
)
103+
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
104+
report.status = "FAIL"
105+
report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports the requirement of approved client apps or app protection for mobile devices but does not enforce it."
106+
else:
107+
report.status = "PASS"
108+
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires approved client apps or app protection for mobile devices."
109+
break
110+
111+
findings.append(report)
112+
113+
return findings

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,30 @@ async def _get_conditional_access_policies(self):
277277
[],
278278
)
279279
],
280+
platform_conditions=PlatformConditions(
281+
include_platforms=[
282+
platform
283+
for platform in (
284+
getattr(
285+
getattr(policy.conditions, "platforms", None),
286+
"include_platforms",
287+
[],
288+
)
289+
or []
290+
)
291+
],
292+
exclude_platforms=[
293+
platform
294+
for platform in (
295+
getattr(
296+
getattr(policy.conditions, "platforms", None),
297+
"exclude_platforms",
298+
[],
299+
)
300+
or []
301+
)
302+
],
303+
),
280304
),
281305
grant_controls=GrantControls(
282306
built_in_controls=(
@@ -857,12 +881,20 @@ class ClientAppType(Enum):
857881
OTHER_CLIENTS = "other"
858882

859883

884+
class PlatformConditions(BaseModel):
885+
"""Model representing platform conditions for Conditional Access policies."""
886+
887+
include_platforms: List[str] = []
888+
exclude_platforms: List[str] = []
889+
890+
860891
class Conditions(BaseModel):
861892
application_conditions: Optional[ApplicationsConditions]
862893
user_conditions: Optional[UsersConditions]
863894
client_app_types: Optional[List[ClientAppType]]
864895
user_risk_levels: List[RiskLevel] = []
865896
sign_in_risk_levels: List[RiskLevel] = []
897+
platform_conditions: Optional[PlatformConditions] = None
866898

867899

868900
class PersistentBrowser(BaseModel):

0 commit comments

Comments
 (0)