Compare commits

...

4 Commits

Author SHA1 Message Date
Hugo P.Brito
0414dde6ff refactor(m365): normalize CA platforms in models
- Normalize Conditional Access platform values in PlatformConditions
- Remove duplicated platform normalization from Entra checks
- Simplify platform-based comparisons in device platform checks
2026-04-09 14:46:14 +01:00
Hugo P.Brito
a0b6c5e3f8 fix(sdk): align unknown platform check with Maester
- Remove the global tenant scope requirement from the check
- Document Microsoft broad-scope guidance in metadata
- Keep report-only policies non-compliant with Maester semantics
2026-04-09 14:45:50 +01:00
Hugo P.Brito
f7903cf1cf fix(sdk): tighten unknown platform policy scope
- Require all users and all cloud apps for compliance
- Ignore policies scoped to user actions
- Add regression coverage for scoped policies
2026-04-09 12:50:08 +01:00
Hugo P.Brito
e8ae0902a5 feat(m365): add entra_conditional_access_policy_block_unknown_device_platforms security check
Add new security check entra_conditional_access_policy_block_unknown_device_platforms for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-04-08 15:53:37 +01:00
9 changed files with 882 additions and 24 deletions

View File

@@ -20,6 +20,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `calendar_external_sharing_primary_calendar`, `calendar_external_sharing_secondary_calendar`, and `calendar_external_invitations_warning` checks for Google Workspace provider using the Cloud Identity Policy API [(#10597)](https://github.com/prowler-cloud/prowler/pull/10597)
- `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222)
- `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234)
- `entra_conditional_access_policy_block_unknown_device_platforms` check for m365 provider [(#10615)](https://github.com/prowler-cloud/prowler/pull/10615)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)
### 🔄 Changed
@@ -27,6 +28,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Added `internet-exposed` category to 13 AWS checks (CloudFront, CodeArtifact, EC2, EFS, RDS, SageMaker, Shield, VPC) [(#10502)](https://github.com/prowler-cloud/prowler/pull/10502)
- Minimum Python version from 3.9 to 3.10 and updated classifiers to reflect supported versions (3.10, 3.11, 3.12) [(#10464)](https://github.com/prowler-cloud/prowler/pull/10464)
- Sensitive CLI flags now warn when values are passed directly, recommending environment variables instead [(#10532)](https://github.com/prowler-cloud/prowler/pull/10532)
- Normalize Conditional Access platform values in Entra models and simplify platform-based checks
### 🐞 Fixed

View File

@@ -207,6 +207,7 @@
"entra_admin_portals_access_restriction",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_conditional_access_policy_block_unknown_device_platforms",
"entra_policy_guest_users_access_restrictions",
"entra_seamless_sso_disabled"
]
@@ -249,6 +250,7 @@
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_conditional_access_policy_block_unknown_device_platforms",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
@@ -626,6 +628,7 @@
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_block_unknown_device_platforms",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_managed_device_required_for_authentication",
@@ -686,6 +689,7 @@
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_block_elevated_insider_risk",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_conditional_access_policy_block_unknown_device_platforms",
"entra_policy_guest_users_access_restrictions",
"sharepoint_external_sharing_restricted"
]
@@ -710,6 +714,7 @@
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_break_glass_account_fido2_security_key_registered",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_block_unknown_device_platforms",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_identity_protection_sign_in_risk_enabled",
"entra_managed_device_required_for_authentication",

View File

@@ -23,13 +23,6 @@ class entra_conditional_access_policy_approved_client_app_required_for_mobile(Ch
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,22 +47,12 @@ 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_platforms = set(
policy.conditions.platform_conditions.include_platforms
)
excluded_platforms = set(
policy.conditions.platform_conditions.exclude_platforms
)
targets_mobile_platforms = (
"all" in included_platforms

View File

@@ -0,0 +1,39 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_block_unknown_device_platforms",
"CheckTitle": "Conditional Access policy blocks access from unknown or unsupported device platforms",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra **Conditional Access** can block sign-ins from device platforms that the organization does not recognize or support. A policy that includes **all** platforms and excludes the known ones (Android, iOS, Windows, macOS, Linux) effectively blocks any **unknown or unsupported** platform, reducing the attack surface from unmanaged or unexpected devices.",
"Risk": "Without blocking unknown device platforms, attackers may authenticate from **spoofed or uncommon operating systems** that bypass platform-specific security controls. This increases the risk of **unauthorized access** and makes it harder to enforce device compliance, potentially leading to **credential theft** or **data exfiltration** from unmanaged environments.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-unknown-unsupported-device",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions#device-platforms",
"https://maester.dev/docs/tests/MT.1015"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"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 the desired scope. Microsoft recommends **All users** with appropriate exclusions.\n5. Under **Target resources**, include the applications or resources you want to protect. Microsoft recommends **All resources / All cloud apps**.\n6. Under **Conditions** > **Device platforms**, set **Configure** to **Yes**.\n7. Under **Include**, select **Any device**.\n8. Under **Exclude**, select **Android**, **iOS**, **Windows**, **macOS**, and **Linux**.\n9. Under **Grant**, select **Block access**.\n10. Set **Enable policy** to **On** and click **Create**.",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that includes **all device platforms** and excludes the known ones (Android, iOS, Windows, macOS, Linux), then set the grant control to **Block access**. This ensures that only recognized platforms can authenticate, following a **zero-trust** approach to device management.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_block_unknown_device_platforms"
}
},
"Categories": [
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check corresponds to Maester test **MT.1015** (`Test-MtCaBlockUnknownOrUnsupportedDevicePlatform`). The policy must include all platforms and exclude the five known platforms (Android, iOS, Windows, macOS, Linux) so that only unknown or unsupported platforms are blocked. The check requires the policy to be fully enabled; report-only policies are treated as non-compliant. Microsoft recommends applying this policy broadly to **All users** and **All resources / All cloud apps**, but that broader scope is guidance rather than a requirement enforced by this check."
}

View File

@@ -0,0 +1,114 @@
"""Check for Conditional Access policy blocking unknown or unsupported device platforms."""
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 (
ConditionalAccessGrantControl,
ConditionalAccessPolicy,
ConditionalAccessPolicyState,
)
class entra_conditional_access_policy_block_unknown_device_platforms(Check):
"""Ensure a Conditional Access policy blocks access from unknown or unsupported device platforms.
This check verifies that at least one enabled Conditional Access policy
blocks access when the device platform is unknown or unsupported. The
recommended configuration includes all device platforms and excludes the
known platforms (Android, iOS, Windows, macOS, Linux), so only
unrecognised platforms are blocked.
- PASS: An enabled policy blocks access from unknown or unsupported device platforms.
- FAIL: No policy blocks access from unknown or unsupported device platforms.
"""
KNOWN_PLATFORMS = {"android", "ios", "windows", "macos", "linux"}
def _is_candidate_policy(self, policy: ConditionalAccessPolicy) -> bool:
"""Determine whether a policy is a candidate for blocking unknown device platforms.
A candidate policy must:
- Not be disabled.
- Have platform conditions configured.
- Include all platforms.
- Exclude all known platforms so only unknown ones are affected.
- Use the block grant control.
Args:
policy: The Conditional Access policy to evaluate.
Returns:
True if the policy is a candidate, False otherwise.
"""
if policy.state == ConditionalAccessPolicyState.DISABLED:
return False
if not policy.conditions.platform_conditions:
return False
included_platforms = set(
policy.conditions.platform_conditions.include_platforms
)
if "all" not in included_platforms:
return False
excluded_platforms = set(
policy.conditions.platform_conditions.exclude_platforms
)
if not self.KNOWN_PLATFORMS.issubset(excluded_platforms):
return False
if (
ConditionalAccessGrantControl.BLOCK
not in policy.grant_controls.built_in_controls
):
return False
return True
def execute(self) -> list[CheckReportM365]:
"""Execute the check logic.
Returns:
A list of reports containing the result of the check.
"""
findings = []
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = "No Conditional Access Policy blocks access from unknown or unsupported device platforms."
for policy in entra_client.conditional_access_policies.values():
if not self._is_candidate_policy(policy):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name,
resource_id=policy.id,
)
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
report.status = "FAIL"
report.status_extended = (
f"Conditional Access Policy '{policy.display_name}' reports "
"blocking unknown or unsupported device platforms but does not enforce it."
)
else:
report.status = "PASS"
report.status_extended = (
f"Conditional Access Policy '{policy.display_name}' blocks "
"access from unknown or unsupported device platforms."
)
break
findings.append(report)
return findings

View File

@@ -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
from pydantic.v1 import BaseModel, validator
from prowler.lib.logger import logger
from prowler.providers.m365.lib.service.service import M365Service
@@ -998,6 +998,19 @@ class PlatformConditions(BaseModel):
include_platforms: List[str] = []
exclude_platforms: List[str] = []
@validator("include_platforms", "exclude_platforms", pre=True)
def normalize_platforms(cls, values):
if not values:
return []
normalized = []
for platform in values:
value = getattr(platform, "value", platform)
if isinstance(value, str) and value:
normalized.append(value.lower())
return normalized
class TransferMethod(Enum):
"""Transfer methods for authentication flows in Conditional Access policies."""

View File

@@ -0,0 +1,702 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
PlatformConditions,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms"
KNOWN_PLATFORMS = ["android", "iOS", "windows", "macOS", "linux"]
def _make_session_controls() -> SessionControls:
"""Return a minimal SessionControls instance for testing."""
return SessionControls(
persistent_browser=PersistentBrowser(is_enabled=False, mode="always"),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
)
class Test_entra_conditional_access_policy_block_unknown_device_platforms:
"""Tests for the entra_conditional_access_policy_block_unknown_device_platforms check."""
def test_no_conditional_access_policies(self):
"""Test FAIL when there are no Conditional Access policies."""
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
entra_client.conditional_access_policies = {}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks access from unknown or unsupported device platforms."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_disabled(self):
"""Test FAIL when the only matching policy is disabled."""
policy_id = str(uuid4())
display_name = "Block Unknown Platforms"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=KNOWN_PLATFORMS,
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks access from unknown or unsupported device platforms."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_enabled_for_reporting_only(self):
"""Test FAIL when the matching policy is only in report-only mode."""
policy_id = str(uuid4())
display_name = "Block Unknown Platforms"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=KNOWN_PLATFORMS,
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Conditional Access Policy 'Block Unknown Platforms' reports blocking unknown or unsupported device platforms but does not enforce it."
)
assert result[0].resource_name == "Block Unknown Platforms"
assert result[0].resource_id == policy_id
assert result[0].location == "global"
def test_policy_no_platform_conditions(self):
"""Test FAIL when the policy has no platform conditions configured."""
policy_id = str(uuid4())
display_name = "Block Unknown Platforms"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=None,
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks access from unknown or unsupported device platforms."
)
def test_policy_does_not_include_all_platforms(self):
"""Test FAIL when the policy includes specific platforms instead of all."""
policy_id = str(uuid4())
display_name = "Block Specific Platforms"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["android", "iOS"],
exclude_platforms=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
def test_policy_missing_excluded_known_platforms(self):
"""Test FAIL when the policy includes all platforms but does not exclude all known ones."""
policy_id = str(uuid4())
display_name = "Incomplete Platform Exclusion"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
# Only exclude 3 of 5 known platforms
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=["android", "iOS", "windows"],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
def test_policy_no_block_grant_control(self):
"""Test FAIL when the policy has correct platform conditions but does not block."""
policy_id = str(uuid4())
display_name = "MFA Unknown Platforms"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=KNOWN_PLATFORMS,
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
def test_policy_limited_to_specific_users_and_apps(self):
"""Test PASS when a scoped policy still blocks unknown device platforms."""
policy_id = str(uuid4())
display_name = "Scoped Unknown Platform Block"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=[
"00000000-0000-0000-0000-000000000001"
],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=["11111111-1111-1111-1111-111111111111"],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=KNOWN_PLATFORMS,
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Conditional Access Policy 'Scoped Unknown Platform Block' blocks access from unknown or unsupported device platforms."
)
def test_policy_enabled_and_compliant(self):
"""Test PASS when an enabled policy blocks unknown device platforms correctly."""
policy_id = str(uuid4())
display_name = "Block Unknown Platforms"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=KNOWN_PLATFORMS,
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Conditional Access Policy 'Block Unknown Platforms' blocks access from unknown or unsupported device platforms."
)
assert result[0].resource_name == "Block Unknown Platforms"
assert result[0].resource_id == policy_id
assert result[0].location == "global"
def test_mixed_policies_report_only_and_enabled(self):
"""Test PASS when both report-only and enabled compliant policies exist."""
report_policy_id = str(uuid4())
enabled_policy_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_block_unknown_device_platforms.entra_conditional_access_policy_block_unknown_device_platforms import (
entra_conditional_access_policy_block_unknown_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
base_conditions = Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
platform_conditions=PlatformConditions(
include_platforms=["all"],
exclude_platforms=KNOWN_PLATFORMS,
),
)
grant_controls = GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
)
entra_client.conditional_access_policies = {
report_policy_id: ConditionalAccessPolicy(
id=report_policy_id,
display_name="Report Only Policy",
conditions=base_conditions,
grant_controls=grant_controls,
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
),
enabled_policy_id: ConditionalAccessPolicy(
id=enabled_policy_id,
display_name="Enforced Block Policy",
conditions=base_conditions,
grant_controls=grant_controls,
session_controls=_make_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
),
}
check = entra_conditional_access_policy_block_unknown_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_name == "Enforced Block Policy"
assert result[0].resource_id == enabled_policy_id