|
| 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 |
0 commit comments