refactor(m365): normalize platforms at PlatformConditions model level

- Add pydantic validator to PlatformConditions that lowercases and
  extracts .value from enum objects on construction
- Remove duplicated _normalize_platform() from both CA checks
- Export MOBILE_PLATFORMS from entra_service for the mobile check
This commit is contained in:
Hugo P.Brito committed 2026-04-08 14:42:28 +01:00
1 parent b2f34d2df0
commit dfce18cc04
3 files changed
+29 -70

No files matched your search

@@ -1,11 +1,17 @@
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
from prowler.providers.m365.services.entra.entra_service import (
MOBILE_PLATFORMS,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
GrantControlOperator,
)
MOBILE_APP_GRANT_CONTROLS = {
ConditionalAccessGrantControl.APPROVED_APPLICATION,
ConditionalAccessGrantControl.COMPLIANT_APPLICATION,
}
class entra_conditional_access_policy_approved_client_app_required_for_mobile(Check):
"""Check if a Conditional Access policy requires approved client apps or app protection for mobile devices.
@@ -17,19 +23,6 @@ class entra_conditional_access_policy_approved_client_app_required_for_mobile(Ch
- FAIL: No policy restricts mobile app access to approved or protected apps.
"""
REQUIRED_MOBILE_PLATFORMS = {"android", "ios"}
MOBILE_APP_GRANT_CONTROLS = {
ConditionalAccessGrantControl.APPROVED_APPLICATION,
ConditionalAccessGrantControl.COMPLIANT_APPLICATION,
}
@staticmethod
def _normalize_platform(platform: object) -> str:
normalized_platform = getattr(platform, "value", platform)
return (
normalized_platform.lower() if isinstance(normalized_platform, str) else ""
)
def execute(self) -> list[CheckReportM365]:
"""Execute the check logic.
@@ -54,43 +47,22 @@ class entra_conditional_access_policy_approved_client_app_required_for_mobile(Ch
if not policy.conditions.platform_conditions:
continue
included_platforms = {
normalized_platform
for normalized_platform in map(
self._normalize_platform,
policy.conditions.platform_conditions.include_platforms,
)
if normalized_platform
}
excluded_platforms = {
normalized_platform
for normalized_platform in map(
self._normalize_platform,
policy.conditions.platform_conditions.exclude_platforms,
)
if normalized_platform
}
included = set(policy.conditions.platform_conditions.include_platforms)
excluded = set(policy.conditions.platform_conditions.exclude_platforms)
targets_mobile_platforms = (
"all" in included_platforms
or self.REQUIRED_MOBILE_PLATFORMS.issubset(included_platforms)
) and not (
"all" in excluded_platforms
or self.REQUIRED_MOBILE_PLATFORMS.intersection(excluded_platforms)
)
if not targets_mobile_platforms:
targets_mobile = (
"all" in included or MOBILE_PLATFORMS.issubset(included)
) and not ("all" in excluded or MOBILE_PLATFORMS.intersection(excluded))
if not targets_mobile:
continue
built_in_controls = set(policy.grant_controls.built_in_controls)
has_mobile_app_control = bool(
self.MOBILE_APP_GRANT_CONTROLS.intersection(built_in_controls)
)
if not has_mobile_app_control:
if not MOBILE_APP_GRANT_CONTROLS.intersection(built_in_controls):
continue
if (
policy.grant_controls.operator == GrantControlOperator.OR
and not built_in_controls.issubset(self.MOBILE_APP_GRANT_CONTROLS)
and not built_in_controls.issubset(MOBILE_APP_GRANT_CONTROLS)
):
continue
@@ -19,13 +19,6 @@ class entra_conditional_access_policy_unknown_device_blocked(Check):
- FAIL: No CA policy restricts access from unrecognized device platforms.
"""
@staticmethod
def _normalize_platform(platform: object) -> str:
normalized_platform = getattr(platform, "value", platform)
return (
normalized_platform.lower() if isinstance(normalized_platform, str) else ""
)
def execute(self) -> list[CheckReportM365]:
"""Execute the check logic.
@@ -49,29 +42,13 @@ class entra_conditional_access_policy_unknown_device_blocked(Check):
if not policy.conditions.platform_conditions:
continue
included_platforms = {
normalized_platform
for normalized_platform in map(
self._normalize_platform,
policy.conditions.platform_conditions.include_platforms,
)
if normalized_platform
}
excluded_platforms = {
normalized_platform
for normalized_platform in map(
self._normalize_platform,
policy.conditions.platform_conditions.exclude_platforms,
)
if normalized_platform
}
included = set(policy.conditions.platform_conditions.include_platforms)
excluded = set(policy.conditions.platform_conditions.exclude_platforms)
if "all" not in included_platforms:
if "all" not in included:
continue
if not {
self._normalize_platform(platform) for platform in KNOWN_PLATFORMS
}.issubset(excluded_platforms):
if not KNOWN_PLATFORMS.issubset(excluded):
continue
if (
@@ -9,7 +9,7 @@ from msgraph.generated.models.o_data_errors.o_data_error import ODataError
from msgraph.generated.security.microsoft_graph_security_run_hunting_query.run_hunting_query_post_request_body import (
RunHuntingQueryPostRequestBody,
)
from pydantic.v1 import BaseModel, Field
from pydantic.v1 import BaseModel, Field, validator
from prowler.lib.logger import logger
from prowler.providers.m365.lib.service.service import M365Service
@@ -1003,8 +1003,18 @@ class PlatformConditions(BaseModel):
class Config:
allow_population_by_field_name = True
@validator("include_platforms", "exclude_platforms", pre=True)
def normalize_platforms(cls, v):
normalized = []
for platform in v:
value = getattr(platform, "value", platform)
if isinstance(value, str) and value:
normalized.append(value.lower())
return normalized
KNOWN_PLATFORMS = frozenset({"android", "ios", "windows", "macos", "linux"})
MOBILE_PLATFORMS = frozenset({"android", "ios"})
class TransferMethod(Enum):