Compare commits

...

11 Commits

Author SHA1 Message Date
Hugo P.Brito
42204d9372 fix(m365): remove quotes around policy name in status messages 2026-04-08 15:24:49 +01:00
Hugo P.Brito
77959a114e docs(sdk): add PR link to changelog entry 2026-04-08 14:43:11 +01:00
Hugo P.Brito
cdbfb07d64 docs(sdk): add changelog entry for platform normalization refactor 2026-04-08 14:42:37 +01:00
Hugo P.Brito
dfce18cc04 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
2026-04-08 14:42:28 +01:00
Hugo P.Brito
b2f34d2df0 docs(sdk): move unknown_device_blocked changelog entry to 5.23.0 2026-04-08 13:25:28 +01:00
Hugo P.Brito
db6828ec28 Merge remote-tracking branch 'origin/master' into HEAD 2026-04-08 13:25:22 +01:00
Hugo P.Brito
505ff94166 fix(m365): correct metadata for unknown device blocked check
- Set ResourceType to NotDefined (no individual resource assessed)
- Replace broken AdditionalURLs with canonical Microsoft Learn links
- Clear RelatedTo (referenced check does not exist)
2026-04-08 13:25:22 +01:00
Hugo P.Brito
85195096be fix(m365): move KNOWN_PLATFORMS to entra_service and normalize case
Move the constant from the check module to the service module so it
can be reused, and switch to a lowercase frozenset for
case-insensitive comparison.
2026-04-08 13:25:17 +01:00
Hugo P.Brito
a491a50afd Merge remote-tracking branch 'origin/master' into HEAD
# Conflicts:
#	prowler/CHANGELOG.md
#	prowler/compliance/m365/iso27001_2022_m365.json
#	prowler/providers/m365/services/entra/entra_service.py
2026-04-08 11:50:37 +01:00
Hugo P.Brito
6451740b27 fix(sdk): preserve entra platform condition compatibility
- Keep platform condition aliases compatible with existing checks
- Normalize platform values before evaluating unknown-device blocks
- Update tests to cover mixed casing and alias-based input
2026-04-07 15:28:49 +01:00
HugoPBrito
11ff512607 feat(m365): add entra_conditional_access_policy_unknown_device_blocked security check
Add new security check entra_conditional_access_policy_unknown_device_blocked for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-03-03 13:28:35 +01:00
9 changed files with 639 additions and 70 deletions

View File

@@ -20,12 +20,14 @@ 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_unknown_device_blocked` check for M365 provider [(#10235)](https://github.com/prowler-cloud/prowler/pull/10235)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)
### 🔄 Changed
- 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)
- Platform normalization in Conditional Access checks moved to `PlatformConditions` model validator [(#10614)](https://github.com/prowler-cloud/prowler/pull/10614)
- Sensitive CLI flags now warn when values are passed directly, recommending environment variables instead [(#10532)](https://github.com/prowler-cloud/prowler/pull/10532)
### 🐞 Fixed

View File

@@ -247,6 +247,7 @@
"entra_break_glass_account_fido2_security_key_registered",
"entra_default_app_management_policy_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_conditional_access_policy_device_code_flow_blocked",
@@ -626,6 +627,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_unknown_device_blocked",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_managed_device_required_for_authentication",
@@ -684,6 +686,7 @@
"entra_admin_portals_access_restriction",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_block_elevated_insider_risk",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_policy_guest_users_access_restrictions",
@@ -706,6 +709,7 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_unknown_device_blocked",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_break_glass_account_fido2_security_key_registered",

View File

@@ -101,6 +101,7 @@
"Id": "1.1.6",
"Description": "Ensure a managed device is required for authentication",
"Checks": [
"entra_conditional_access_policy_unknown_device_blocked",
"entra_managed_device_required_for_authentication"
],
"Attributes": [
@@ -119,6 +120,7 @@
"Id": "1.1.7",
"Description": "Ensure a managed device is required for MFA registration",
"Checks": [
"entra_conditional_access_policy_unknown_device_blocked",
"entra_managed_device_required_for_mfa_registration"
],
"Attributes": [

View File

@@ -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

View File

@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_unknown_device_blocked",
"CheckTitle": "Conditional Access policy blocks access from unknown or unsupported device platforms",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Conditional Access policy that includes **all device platforms** and excludes the five known platforms (`android`, `iOS`, `windows`, `macOS`, `linux`) with a **block** grant control prevents sign-ins from unrecognized or unsupported devices.",
"Risk": "Without blocking unknown device platforms, attackers can sign in from **unmanaged or spoofed devices** that bypass compliance and security controls.\n\nThis increases the risk of **unauthorized access** and makes it harder to enforce device-based security policies.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-unknown-unsupported"
],
"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. Click **New policy**.\n4. Under **Users**, select **All users**.\n5. Under **Target resources**, select **All cloud apps**.\n6. Under **Conditions** > **Device platforms**, set **Include** to **Any device** and **Exclude** the five known platforms: Android, iOS, Windows, macOS, Linux.\n7. Under **Grant**, select **Block access**.\n8. Set the policy to **On** and click **Create**.",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that blocks access from unknown device platforms. Include all platforms and exclude the five known ones (`android`, `iOS`, `windows`, `macOS`, `linux`). Pair this with **device compliance** policies for defense in depth, since platform detection relies on user agent strings.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_unknown_device_blocked"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Device platform detection relies on user agent strings, which can be spoofed. This policy should be paired with device compliance policies for stronger security."
}

View File

@@ -0,0 +1,76 @@
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 (
KNOWN_PLATFORMS,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
)
class entra_conditional_access_policy_unknown_device_blocked(Check):
"""Check if at least one Conditional Access policy blocks unknown device platforms.
This check verifies that at least one enabled Conditional Access policy
blocks access from unknown or unsupported device platforms by including
all platforms and excluding the five known ones (android, iOS, windows,
macOS, linux) with a block grant control.
- PASS: An enabled CA policy blocks access from unknown/unsupported device platforms.
- FAIL: No CA policy restricts access from unrecognized device platforms.
"""
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 policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if not policy.conditions.platform_conditions:
continue
included = set(policy.conditions.platform_conditions.include_platforms)
excluded = set(policy.conditions.platform_conditions.exclude_platforms)
if "all" not in included:
continue
if not KNOWN_PLATFORMS.issubset(excluded):
continue
if (
ConditionalAccessGrantControl.BLOCK
not in policy.grant_controls.built_in_controls
):
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} is configured to block unknown device platforms but is only in report-only mode."
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, Field, validator
from prowler.lib.logger import logger
from prowler.providers.m365.lib.service.service import M365Service
@@ -273,6 +273,28 @@ class Entra(M365Service):
[],
)
],
platform_conditions=(
PlatformConditions(
included_platforms=[
platform
for platform in getattr(
policy.conditions.platforms,
"include_platforms",
[],
)
],
excluded_platforms=[
platform
for platform in getattr(
policy.conditions.platforms,
"exclude_platforms",
[],
)
],
)
if getattr(policy.conditions, "platforms", None)
else None
),
user_risk_levels=[
RiskLevel(risk_level)
for risk_level in getattr(
@@ -304,30 +326,6 @@ class Entra(M365Service):
and raw_insider_risk
else None
),
platform_conditions=PlatformConditions(
include_platforms=[
platform
for platform in (
getattr(
getattr(policy.conditions, "platforms", None),
"include_platforms",
[],
)
or []
)
],
exclude_platforms=[
platform
for platform in (
getattr(
getattr(policy.conditions, "platforms", None),
"exclude_platforms",
[],
)
or []
)
],
),
authentication_flows=self._parse_authentication_flows(
raw_auth_flows_map.get(policy.id)
),
@@ -995,8 +993,28 @@ class InsiderRiskLevel(Enum):
class PlatformConditions(BaseModel):
"""Model representing platform conditions for Conditional Access policies."""
include_platforms: List[str] = []
exclude_platforms: List[str] = []
include_platforms: List[str] = Field(
default_factory=list, alias="included_platforms"
)
exclude_platforms: List[str] = Field(
default_factory=list, alias="excluded_platforms"
)
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):
@@ -1016,10 +1034,10 @@ class Conditions(BaseModel):
application_conditions: Optional[ApplicationsConditions]
user_conditions: Optional[UsersConditions]
client_app_types: Optional[List[ClientAppType]]
platform_conditions: Optional[PlatformConditions] = None
user_risk_levels: List[RiskLevel] = []
sign_in_risk_levels: List[RiskLevel] = []
insider_risk_levels: Optional[InsiderRiskLevel] = None
platform_conditions: Optional[PlatformConditions] = None
authentication_flows: Optional[AuthenticationFlows] = None

View File

@@ -0,0 +1,458 @@
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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked"
KNOWN_PLATFORMS = ["android", "ios", "windows", "macos", "linux"]
def _default_session_controls():
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,
),
)
def _default_conditions(**overrides):
defaults = dict(
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=[],
platform_conditions=PlatformConditions(
included_platforms=["All"],
excluded_platforms=KNOWN_PLATFORMS,
),
user_risk_levels=[],
)
defaults.update(overrides)
return Conditions(**defaults)
def _default_grant_controls(**overrides):
defaults = dict(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=None,
)
defaults.update(overrides)
return GrantControls(**defaults)
class Test_entra_conditional_access_policy_unknown_device_blocked:
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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
entra_client.conditional_access_policies = {}
check = entra_conditional_access_policy_unknown_device_blocked()
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 Devices"
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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(),
grant_controls=_default_grant_controls(),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
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 Devices"
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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(),
grant_controls=_default_grant_controls(),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' is configured to block unknown device platforms but is only in report-only mode."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[policy_id].dict()
)
assert result[0].resource_name == display_name
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."""
policy_id = str(uuid4())
display_name = "Block Without 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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(platform_conditions=None),
grant_controls=_default_grant_controls(),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
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"
def test_policy_does_not_include_all_platforms(self):
"""Test FAIL when the policy does not include all platforms."""
policy_id = str(uuid4())
display_name = "Partial 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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(
platform_conditions=PlatformConditions(
included_platforms=["android", "ios"],
excluded_platforms=[],
)
),
grant_controls=_default_grant_controls(),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
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 == {}
def test_policy_missing_excluded_platforms(self):
"""Test FAIL when the policy does not exclude all five known platforms."""
policy_id = str(uuid4())
display_name = "Incomplete Exclusions"
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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(
platform_conditions=PlatformConditions(
included_platforms=["All"],
excluded_platforms=["android", "ios", "windows"],
)
),
grant_controls=_default_grant_controls(),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
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 == {}
def test_policy_no_block_grant_control(self):
"""Test FAIL when the policy does not use block as grant control."""
policy_id = str(uuid4())
display_name = "MFA Instead of 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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(),
grant_controls=_default_grant_controls(
built_in_controls=[ConditionalAccessGrantControl.MFA]
),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
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 == {}
def test_policy_enabled_and_compliant(self):
"""Test PASS when an enabled policy blocks unknown device platforms."""
policy_id = str(uuid4())
display_name = "Block Unknown Devices"
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_unknown_device_blocked.entra_conditional_access_policy_unknown_device_blocked import (
entra_conditional_access_policy_unknown_device_blocked,
)
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=_default_conditions(),
grant_controls=_default_grant_controls(),
session_controls=_default_session_controls(),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_unknown_device_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' blocks access from unknown or unsupported device platforms."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[policy_id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
assert result[0].location == "global"