diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced.py index d74c5efe69..62ac4d4993 100644 --- a/prowler/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced.py +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced.py @@ -1,3 +1,5 @@ +import re + 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 ( @@ -20,6 +22,18 @@ class entra_conditional_access_policy_sign_in_frequency_enforced(Check): non-corporate devices. """ + NON_CORPORATE_INCLUDE_PATTERNS = ( + r"device\.iscompliant\s*-ne\s*true", + r"device\.iscompliant\s*-eq\s*false", + r'device\.trusttype\s*-ne\s*"serverad"', + r"device\.trusttype\s*-ne\s*'serverad'", + ) + CORPORATE_EXCLUDE_PATTERNS = ( + r"device\.iscompliant\s*-eq\s*true", + r'device\.trusttype\s*-eq\s*"serverad"', + r"device\.trusttype\s*-eq\s*'serverad'", + ) + def _targets_all_users(self, included_users: list[str]) -> bool: """Check if the policy targets all users. @@ -61,18 +75,16 @@ class entra_conditional_access_policy_sign_in_frequency_enforced(Check): return False if not device_conditions.device_filter_rule: return False + rule = device_conditions.device_filter_rule.lower() if device_conditions.device_filter_mode == DeviceFilterMode.INCLUDE: - rule = device_conditions.device_filter_rule.lower() - return ( - "device.iscompliant" in rule or "device.trusttype" in rule - ) + return self._rule_matches_any(rule, self.NON_CORPORATE_INCLUDE_PATTERNS) elif device_conditions.device_filter_mode == DeviceFilterMode.EXCLUDE: - rule = device_conditions.device_filter_rule.lower() - return ( - "device.iscompliant" in rule or "device.trusttype" in rule - ) + return self._rule_matches_any(rule, self.CORPORATE_EXCLUDE_PATTERNS) return False + def _rule_matches_any(self, rule: str, patterns: tuple[str, ...]) -> bool: + return any(re.search(pattern, rule) for pattern in patterns) + def execute(self) -> list[CheckReportM365]: """Execute the check for sign-in frequency enforcement in Conditional Access policies. diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced_test.py index c9d9f14f78..cdd0f53587 100644 --- a/tests/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced_test.py +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_sign_in_frequency_enforced/entra_conditional_access_policy_sign_in_frequency_enforced_test.py @@ -147,7 +147,7 @@ class Test_entra_conditional_access_policy_sign_in_frequency_enforced: display_name="Disabled Policy", state=ConditionalAccessPolicyState.DISABLED, device_filter_mode=DeviceFilterMode.INCLUDE, - device_filter_rule="device.isCompliant -ne True -or device.trustType -ne \"ServerAD\"", + device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"', ) } @@ -187,7 +187,7 @@ class Test_entra_conditional_access_policy_sign_in_frequency_enforced: display_name=display_name, state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, device_filter_mode=DeviceFilterMode.INCLUDE, - device_filter_rule="device.isCompliant -ne True -or device.trustType -ne \"ServerAD\"", + device_filter_rule='device.isCompliant -ne True -or device.trustType -ne "ServerAD"', ) } @@ -509,6 +509,80 @@ class Test_entra_conditional_access_policy_sign_in_frequency_enforced: assert result[0].status == "FAIL" assert result[0].resource_name == "Conditional Access Policies" + def test_entra_policy_device_filter_include_corporate_devices(self): + """Test FAIL when include mode targets only corporate devices.""" + 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_sign_in_frequency_enforced.entra_conditional_access_policy_sign_in_frequency_enforced import ( + entra_conditional_access_policy_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Corporate Devices Policy", + device_filter_mode=DeviceFilterMode.INCLUDE, + device_filter_rule="device.isCompliant -eq True", + ) + } + + check = entra_conditional_access_policy_sign_in_frequency_enforced() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + + def test_entra_policy_device_filter_exclude_non_corporate_devices(self): + """Test FAIL when exclude mode excludes non-corporate devices.""" + 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_sign_in_frequency_enforced.entra_conditional_access_policy_sign_in_frequency_enforced import ( + entra_conditional_access_policy_sign_in_frequency_enforced, + ) + + entra_client.conditional_access_policies = { + policy_id: _make_policy( + policy_id=policy_id, + display_name="Exclude Unmanaged Devices Policy", + device_filter_mode=DeviceFilterMode.EXCLUDE, + device_filter_rule="device.isCompliant -eq False", + ) + } + + check = entra_conditional_access_policy_sign_in_frequency_enforced() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Conditional Access Policies" + def test_entra_multiple_policies_one_compliant(self): """Test PASS when at least one policy among multiple is compliant.""" policy_id_1 = str(uuid4())