From 11ff5126074236dca0977b7ae07c5ac3cffd5a43 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Tue, 3 Mar 2026 13:27:42 +0100 Subject: [PATCH] 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. --- prowler/CHANGELOG.md | 1 + .../compliance/m365/iso27001_2022_m365.json | 4 + .../m365/prowler_threatscore_m365.json | 2 + .../__init__.py | 0 ...olicy_unknown_device_blocked.metadata.json | 39 ++ ...al_access_policy_unknown_device_blocked.py | 79 +++ .../m365/services/entra/entra_service.py | 30 ++ ...cess_policy_unknown_device_blocked_test.py | 458 ++++++++++++++++++ 8 files changed, 613 insertions(+) create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/__init__.py create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.metadata.json create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.py create mode 100644 tests/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 683da84487..9240e90002 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -39,6 +39,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - OpenStack compute service with 7 security checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944) - OpenStack image service with 6 security checks [(#10096)](https://github.com/prowler-cloud/prowler/pull/10096) - IaC `--provider-uid` flag to specify the provider UID for the IaC provider [(#10233)](https://github.com/prowler-cloud/prowler/pull/10233) +- `entra_conditional_access_policy_unknown_device_blocked` check for M365 provider [(#10235)](https://github.com/prowler-cloud/prowler/pull/10235) ### 🔄 Changed diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index b1a3b27301..cf24622373 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -242,6 +242,7 @@ "entra_admin_users_sign_in_frequency_enabled", "entra_default_app_management_policy_enabled", "entra_all_apps_conditional_access_coverage", + "entra_conditional_access_policy_unknown_device_blocked", "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", @@ -616,6 +617,7 @@ "defenderxdr_endpoint_privileged_user_exposed_credentials", "entra_admin_users_phishing_resistant_mfa_enabled", "entra_conditional_access_policy_app_enforced_restrictions", + "entra_conditional_access_policy_unknown_device_blocked", "entra_managed_device_required_for_authentication", "entra_managed_device_required_for_mfa_registration", "entra_users_mfa_capable", @@ -670,6 +672,7 @@ "Checks": [ "entra_admin_portals_access_restriction", "entra_conditional_access_policy_app_enforced_restrictions", + "entra_conditional_access_policy_unknown_device_blocked", "entra_policy_guest_users_access_restrictions", "sharepoint_external_sharing_restricted" ] @@ -690,6 +693,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_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", diff --git a/prowler/compliance/m365/prowler_threatscore_m365.json b/prowler/compliance/m365/prowler_threatscore_m365.json index c88788ca54..ce12046d65 100644 --- a/prowler/compliance/m365/prowler_threatscore_m365.json +++ b/prowler/compliance/m365/prowler_threatscore_m365.json @@ -99,6 +99,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": [ @@ -117,6 +118,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": [ diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.metadata.json new file mode 100644 index 0000000000..7dc314355e --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.metadata.json @@ -0,0 +1,39 @@ +{ + "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": "Conditional Access Policy", + "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-condition-device-platforms", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-unknown-unsupported-device" + ], + "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": [ + "entra_managed_device_required_for_authentication" + ], + "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." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.py new file mode 100644 index 0000000000..d248498372 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked.py @@ -0,0 +1,79 @@ +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, + ConditionalAccessPolicyState, +) + +KNOWN_PLATFORMS = {"android", "iOS", "windows", "macOS", "linux"} + + +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 + + if ( + "all" + not in policy.conditions.platform_conditions.included_platforms + ): + continue + + if not KNOWN_PLATFORMS.issubset( + set(policy.conditions.platform_conditions.excluded_platforms) + ): + 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 diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index bb24865711..56195587bb 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -256,6 +256,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( @@ -798,10 +820,18 @@ class ClientAppType(Enum): OTHER_CLIENTS = "other" +class PlatformConditions(BaseModel): + """Model representing device platform conditions for Conditional Access policies.""" + + included_platforms: List[str] = [] + excluded_platforms: List[str] = [] + + 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] = [] diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked_test.py new file mode 100644 index 0000000000..7c5b6c3eae --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_unknown_device_blocked/entra_conditional_access_policy_unknown_device_blocked_test.py @@ -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"