diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index ac417d2ecc..265a197ed5 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -13,7 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - CloudTrail Timeline abstraction for querying resource modification history [(#9101)](https://github.com/prowler-cloud/prowler/pull/9101) - Cloudflare `--account-id` filter argument [(#9894)](https://github.com/prowler-cloud/prowler/pull/9894) - `rds_instance_extended_support` check for AWS provider [(#9865)](https://github.com/prowler-cloud/prowler/pull/9865) -- `entra_conditional_access_policy_enforce_sign_in_frequency` check for Azure provider [(#9915)](https://github.com/prowler-cloud/prowler/pull/9915) +- `entra_conditional_access_policy_enforce_sign_in_frequency` check for M365 provider [(#9915)](https://github.com/prowler-cloud/prowler/pull/9915) ### Changed diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py b/prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py deleted file mode 100644 index 1654f593e1..0000000000 --- a/prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py +++ /dev/null @@ -1,133 +0,0 @@ -from prowler.lib.check.models import Check, Check_Report_Azure -from prowler.providers.azure.services.entra.entra_client import entra_client - - -class entra_conditional_access_policy_enforce_sign_in_frequency(Check): - """ - Ensure at least one Conditional Access policy enforces sign-in frequency for non-corporate devices. - - This check evaluates whether the tenant has a Conditional Access policy that: - - Is enabled - - Targets all users and all applications - - Has sign-in frequency enabled with time-based interval - - Targets non-corporate devices via device filter - - - PASS: A policy meeting all criteria exists. - - FAIL: No policy enforces sign-in frequency for non-corporate devices. - """ - - def execute(self) -> list[Check_Report_Azure]: - """Execute the check logic. - - Returns: - A list of reports containing the result of the check. - """ - findings = [] - - for ( - tenant_name, - conditional_access_policies, - ) in entra_client.conditional_access_policy.items(): - compliant_policy = None - - for policy in conditional_access_policies.values(): - # Check if policy is enabled - if policy.state != "enabled": - continue - - # Check if sign-in frequency is enabled with time-based interval - if not policy.sign_in_frequency: - continue - if not policy.sign_in_frequency.is_enabled: - continue - if ( - policy.sign_in_frequency.frequency_interval - and "timebased" - not in policy.sign_in_frequency.frequency_interval.lower() - ): - continue - - # Check if targets all users - if "All" not in policy.users.get("include", []): - continue - - # Check if targets all applications - if "All" not in policy.target_resources.get("include", []): - continue - - # Check if device filter targets non-corporate devices - if not self._targets_non_corporate_devices(policy.device_filter): - continue - - # Policy meets all criteria - compliant_policy = policy - break - - if compliant_policy: - report = Check_Report_Azure( - metadata=self.metadata(), resource=compliant_policy - ) - report.subscription = f"Tenant: {tenant_name}" - report.status = "PASS" - report.status_extended = f"Conditional Access Policy '{compliant_policy.name}' enforces sign-in frequency for non-corporate devices." - else: - report = Check_Report_Azure( - metadata=self.metadata(), - resource=conditional_access_policies, - ) - report.subscription = f"Tenant: {tenant_name}" - report.resource_name = "Conditional Access Policy" - report.resource_id = "Conditional Access Policy" - report.status = "FAIL" - report.status_extended = "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - - findings.append(report) - - return findings - - def _targets_non_corporate_devices(self, device_filter) -> bool: - """Check if the device filter targets non-corporate devices. - - Non-corporate devices are identified by targeting devices that are: - - Not Hybrid Azure AD joined (device.trustType != "ServerAD") - - Not compliant (device.isCompliant != True) - - Args: - device_filter: The device filter from the conditional access policy. - - Returns: - True if the filter targets non-corporate devices, False otherwise. - """ - if not device_filter or not device_filter.rule: - return False - - rule = device_filter.rule.lower() - mode = device_filter.mode.lower() if device_filter.mode else "" - - # Include mode: should target non-corporate devices directly - # Rule pattern: device.trustType -ne "ServerAD" or device.isCompliant -ne True - if mode == "filtermode.include" or mode == "include": - # Check for targeting non-compliant OR non-hybrid-joined devices - targets_non_compliant = ( - "device.iscompliant" in rule and "-ne" in rule and "true" in rule - ) - targets_non_hybrid = ( - "device.trusttype" in rule and "-ne" in rule and "serverad" in rule - ) - if targets_non_compliant or targets_non_hybrid: - return True - - # Exclude mode: should exclude corporate devices (equivalent to targeting non-corporate) - # Rule pattern: device.trustType -eq "ServerAD" and device.isCompliant -eq True - elif mode == "filtermode.exclude" or mode == "exclude": - # Check for excluding compliant AND hybrid-joined devices - excludes_compliant = ( - "device.iscompliant" in rule and "-eq" in rule and "true" in rule - ) - excludes_hybrid = ( - "device.trusttype" in rule and "-eq" in rule and "serverad" in rule - ) - if excludes_compliant or excludes_hybrid: - return True - - return False diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/__init__.py similarity index 100% rename from prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/__init__.py rename to prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/__init__.py diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json similarity index 65% rename from prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json rename to prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json index 83cb36517f..387f9e83b1 100644 --- a/prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json @@ -1,19 +1,18 @@ { - "Provider": "azure", + "Provider": "m365", "CheckID": "entra_conditional_access_policy_enforce_sign_in_frequency", - "CheckTitle": "Conditional Access policy enforces sign-in frequency for non-corporate devices", + "CheckTitle": "Ensure that Conditional Access policy enforces sign-in frequency for non-corporate devices", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "#microsoft.graph.conditionalAccess", + "ResourceType": "Conditional Access Policy", "ResourceGroup": "IAM", - "Description": "This check verifies that at least one **Conditional Access policy** enforces sign-in frequency controls for non-corporate devices.\n\nSign-in frequency controls require users to reauthenticate at regular intervals when accessing resources from unmanaged devices, limiting the risk of persistent sessions on untrusted endpoints.", + "Description": "This check verifies that at least one Conditional Access policy enforces sign-in frequency controls for non-corporate devices. Sign-in frequency controls require users to reauthenticate at regular intervals when accessing resources from unmanaged devices, limiting the risk of persistent sessions on untrusted endpoints.", "Risk": "Without sign-in frequency controls for non-corporate devices:\n\n- **Session hijacking** - Long-lived sessions on unmanaged devices increase exposure to credential theft\n- **Unauthorized access** - Shared or compromised devices may retain access indefinitely\n- **Data leakage** - Sensitive resources remain accessible without periodic verification", - "RelatedUrl": "", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime", "AdditionalURLs": [ - "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime", "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime" ], "Remediation": { @@ -24,7 +23,7 @@ "Terraform": "" }, "Recommendation": { - "Text": "Configure Conditional Access policies to enforce **sign-in frequency** for sessions originating from non-corporate devices. This limits persistent access from unmanaged endpoints and requires users to periodically reauthenticate, reducing the risk of unauthorized access through shared or compromised devices.", + "Text": "Configure Conditional Access policies to enforce sign-in frequency for sessions originating from non-corporate devices. This limits persistent access from unmanaged endpoints and requires users to periodically reauthenticate, reducing the risk of unauthorized access through shared or compromised devices.", "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_enforce_sign_in_frequency" } }, @@ -32,8 +31,6 @@ "identity-access" ], "DependsOn": [], - "RelatedTo": [ - "entra_conditional_access_policy_require_mfa_for_management_api" - ], + "RelatedTo": [], "Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. The device filter targets devices that are not Hybrid Azure AD joined or not compliant with Intune policies." } diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py new file mode 100644 index 0000000000..a92eaacb36 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py @@ -0,0 +1,121 @@ +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 ( + ConditionalAccessPolicyState, + DeviceFilter, + DeviceFilterMode, + SignInFrequencyInterval, +) + + +class entra_conditional_access_policy_enforce_sign_in_frequency(Check): + """Check if at least one Conditional Access policy enforces sign-in frequency for non-corporate devices. + + This check evaluates whether the tenant has a Conditional Access policy that: + - Is enabled + - Targets all users and all applications + - Has sign-in frequency enabled with time-based interval + - Targets non-corporate devices via device filter + """ + + 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 enforces sign-in frequency for non-corporate devices." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if not policy.session_controls.sign_in_frequency.is_enabled: + continue + + if ( + policy.session_controls.sign_in_frequency.interval + != SignInFrequencyInterval.TIME_BASED + ): + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + if not self._targets_non_corporate_devices(policy.conditions.device_filter): + 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 enforce sign-in frequency for non-corporate devices but is in report-only mode." + else: + report.status = "PASS" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces sign-in frequency for non-corporate devices." + break + + findings.append(report) + return findings + + def _targets_non_corporate_devices(self, device_filter: DeviceFilter) -> bool: + """Check if the device filter targets non-corporate devices. + + Non-corporate devices are identified by targeting devices that are: + - Not Hybrid Azure AD joined (device.trustType != "ServerAD") + - Not compliant (device.isCompliant != True) + + Args: + device_filter: The device filter from the conditional access policy. + + Returns: + True if the filter targets non-corporate devices, False otherwise. + """ + if not device_filter or not device_filter.rule: + return False + + rule = device_filter.rule.lower() + mode = device_filter.mode + + # Include mode: should target non-corporate devices directly + if mode == DeviceFilterMode.INCLUDE: + targets_non_compliant = ( + "device.iscompliant" in rule and "-ne" in rule and "true" in rule + ) + targets_non_hybrid = ( + "device.trusttype" in rule and "-ne" in rule and "serverad" in rule + ) + if targets_non_compliant or targets_non_hybrid: + return True + + # Exclude mode: should exclude corporate devices (equivalent to targeting non-corporate) + elif mode == DeviceFilterMode.EXCLUDE: + excludes_compliant = ( + "device.iscompliant" in rule and "-eq" in rule and "true" in rule + ) + excludes_hybrid = ( + "device.trusttype" in rule and "-eq" in rule and "serverad" in rule + ) + if excludes_compliant or excludes_hybrid: + return True + + return False diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index 4addbd07a2..56ea3a4bc1 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -235,6 +235,28 @@ class Entra(M365Service): [], ) ], + device_filter=( + DeviceFilter( + mode=( + DeviceFilterMode( + policy.conditions.devices.device_filter.mode.value.lower() + ) + if policy.conditions.devices + and policy.conditions.devices.device_filter + and policy.conditions.devices.device_filter.mode + else None + ), + rule=( + policy.conditions.devices.device_filter.rule + if policy.conditions.devices + and policy.conditions.devices.device_filter + else None + ), + ) + if policy.conditions.devices + and policy.conditions.devices.device_filter + else None + ), ), grant_controls=GrantControls( built_in_controls=( @@ -503,12 +525,23 @@ class ClientAppType(Enum): OTHER_CLIENTS = "other" +class DeviceFilterMode(Enum): + INCLUDE = "include" + EXCLUDE = "exclude" + + +class DeviceFilter(BaseModel): + mode: Optional[DeviceFilterMode] + rule: Optional[str] + + class Conditions(BaseModel): application_conditions: Optional[ApplicationsConditions] user_conditions: Optional[UsersConditions] client_app_types: Optional[List[ClientAppType]] user_risk_levels: List[RiskLevel] = [] sign_in_risk_levels: List[RiskLevel] = [] + device_filter: Optional[DeviceFilter] = None class PersistentBrowser(BaseModel): diff --git a/tests/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py b/tests/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py deleted file mode 100644 index 8a4b8f273a..0000000000 --- a/tests/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py +++ /dev/null @@ -1,931 +0,0 @@ -from unittest import mock -from uuid import uuid4 - -from tests.providers.azure.azure_fixtures import DOMAIN, set_mocked_azure_provider - - -class Test_entra_conditional_access_policy_enforce_sign_in_frequency: - def test_entra_no_subscriptions(self): - """Test when no tenants/subscriptions exist - should return empty list.""" - entra_client = mock.MagicMock - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - - entra_client.conditional_access_policy = {} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 0 - - def test_entra_tenant_no_policies(self): - """Test when tenant exists but has no policies - should FAIL.""" - entra_client = mock.MagicMock - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - - entra_client.conditional_access_policy = {DOMAIN: {}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_disabled(self): - """Test when policy exists but is disabled - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="disabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_no_sign_in_frequency(self): - """Test when policy has no sign-in frequency configured - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=None, - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_sign_in_frequency_disabled(self): - """Test when policy has sign-in frequency but it's disabled - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=False, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_sign_in_frequency_not_timebased(self): - """Test when policy has sign-in frequency but not time-based - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="everyTime", - type=None, - value=None, - ), - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_no_all_users(self): - """Test when policy doesn't target all users - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["specific-user-id"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_no_all_apps(self): - """Test when policy doesn't target all applications - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["specific-app-id"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_no_device_filter(self): - """Test when policy has no device filter - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=None, - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_device_filter_wrong_rule(self): - """Test when policy has device filter but doesn't target non-corporate devices - should FAIL.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Test Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule="device.displayName -contains 'Windows'", - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Conditional Access Policy" - assert result[0].resource_id == "Conditional Access Policy" - assert ( - result[0].status_extended - == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_compliant_include_non_compliant_devices(self): - """Test compliant policy using include mode targeting non-compliant devices - should PASS.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Sign-in Frequency Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule="device.isCompliant -ne True", - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Sign-in Frequency Policy" - assert result[0].resource_id == policy_id - assert ( - result[0].status_extended - == "Conditional Access Policy 'Sign-in Frequency Policy' enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_compliant_include_non_hybrid_devices(self): - """Test compliant policy using include mode targeting non-hybrid AD joined devices - should PASS.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Sign-in Frequency Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule='device.trustType -ne "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Sign-in Frequency Policy" - assert result[0].resource_id == policy_id - assert ( - result[0].status_extended - == "Conditional Access Policy 'Sign-in Frequency Policy' enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_compliant_exclude_compliant_devices(self): - """Test compliant policy using exclude mode excluding compliant devices - should PASS.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Sign-in Frequency Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="exclude", - rule="device.isCompliant -eq True", - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Sign-in Frequency Policy" - assert result[0].resource_id == policy_id - assert ( - result[0].status_extended - == "Conditional Access Policy 'Sign-in Frequency Policy' enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_compliant_exclude_hybrid_devices(self): - """Test compliant policy using exclude mode excluding hybrid AD joined devices - should PASS.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Sign-in Frequency Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="exclude", - rule='device.trustType -eq "ServerAD"', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Sign-in Frequency Policy" - assert result[0].resource_id == policy_id - assert ( - result[0].status_extended - == "Conditional Access Policy 'Sign-in Frequency Policy' enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_compliant_filtermode_include(self): - """Test compliant policy using filterMode.include mode - should PASS.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Sign-in Frequency Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="filterMode.include", - rule='device.trustType -ne "ServerAD" -or device.isCompliant -ne True', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Sign-in Frequency Policy" - assert result[0].resource_id == policy_id - assert ( - result[0].status_extended - == "Conditional Access Policy 'Sign-in Frequency Policy' enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_policy_compliant_filtermode_exclude(self): - """Test compliant policy using filterMode.exclude mode - should PASS.""" - entra_client = mock.MagicMock - policy_id = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - policy = ConditionalAccessPolicy( - id=policy_id, - name="Sign-in Frequency Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="filterMode.exclude", - rule='device.trustType -eq "ServerAD" -and device.isCompliant -eq True', - ), - ) - - entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Sign-in Frequency Policy" - assert result[0].resource_id == policy_id - assert ( - result[0].status_extended - == "Conditional Access Policy 'Sign-in Frequency Policy' enforces sign-in frequency for non-corporate devices." - ) - - def test_entra_tenant_multiple_policies_one_compliant(self): - """Test when one policy is compliant among multiple policies - should PASS.""" - entra_client = mock.MagicMock - policy_id_1 = str(uuid4()) - policy_id_2 = str(uuid4()) - - with ( - mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_azure_provider(), - ), - mock.patch( - "prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", - new=entra_client, - ), - ): - from prowler.providers.azure.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( - entra_conditional_access_policy_enforce_sign_in_frequency, - ) - from prowler.providers.azure.services.entra.entra_service import ( - ConditionalAccessPolicy, - DeviceFilter, - SignInFrequencySessionControl, - ) - - # Non-compliant policy (disabled) - policy_1 = ConditionalAccessPolicy( - id=policy_id_1, - name="Disabled Policy", - state="disabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule="device.isCompliant -ne True", - ), - ) - - # Compliant policy - policy_2 = ConditionalAccessPolicy( - id=policy_id_2, - name="Compliant Policy", - state="enabled", - users={"include": ["All"]}, - target_resources={"include": ["All"]}, - access_controls={"grant": []}, - sign_in_frequency=SignInFrequencySessionControl( - is_enabled=True, - frequency_interval="timeBased", - type="hours", - value=1, - ), - device_filter=DeviceFilter( - mode="include", - rule="device.isCompliant -ne True", - ), - ) - - entra_client.conditional_access_policy = { - DOMAIN: {policy_id_1: policy_1, policy_id_2: policy_2} - } - - check = entra_conditional_access_policy_enforce_sign_in_frequency() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert result[0].subscription == f"Tenant: {DOMAIN}" - assert result[0].resource_name == "Compliant Policy" - assert result[0].resource_id == policy_id_2 - assert ( - result[0].status_extended - == "Conditional Access Policy 'Compliant Policy' enforces sign-in frequency for non-corporate devices." - ) diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py new file mode 100644 index 0000000000..f0cbec32be --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py @@ -0,0 +1,1010 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessPolicyState, + Conditions, + DeviceFilter, + DeviceFilterMode, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + SignInFrequencyType, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_entra_conditional_access_policy_enforce_sign_in_frequency: + def test_entra_no_conditional_access_policies(self): + """Test when no policies exist - should FAIL.""" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + + entra_client.conditional_access_policies = {} + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + + def test_entra_policy_disabled(self): + """Test when policy exists but is disabled - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule='device.trustType -ne "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_no_sign_in_frequency(self): + """Test when policy has no sign-in frequency configured - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule='device.trustType -ne "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_sign_in_frequency_not_timebased(self): + """Test when policy has sign-in frequency but not time-based - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule='device.trustType -ne "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=None, + type=None, + interval=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_no_all_users(self): + """Test when policy doesn't target all users - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=["specific-user-id"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule='device.trustType -ne "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_no_all_apps(self): + """Test when policy doesn't target all applications - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=["specific-app-id"], + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule='device.trustType -ne "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_no_device_filter(self): + """Test when policy has no device filter - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=None, + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_device_filter_wrong_rule(self): + """Test when policy has device filter but doesn't target non-corporate devices - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Test Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule="device.displayName -contains 'Windows'", + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_compliant_include_non_compliant_devices(self): + """Test compliant policy using include mode targeting non-compliant devices - should PASS.""" + policy_id = str(uuid4()) + display_name = "Sign-in Frequency Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule="device.isCompliant -ne True", + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_compliant_include_non_hybrid_devices(self): + """Test compliant policy using include mode targeting non-hybrid AD joined devices - should PASS.""" + policy_id = str(uuid4()) + display_name = "Sign-in Frequency Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule='device.trustType -ne "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_compliant_exclude_compliant_devices(self): + """Test compliant policy using exclude mode excluding compliant devices - should PASS.""" + policy_id = str(uuid4()) + display_name = "Sign-in Frequency Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.EXCLUDE, + rule="device.isCompliant -eq True", + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_compliant_exclude_hybrid_devices(self): + """Test compliant policy using exclude mode excluding hybrid AD joined devices - should PASS.""" + policy_id = str(uuid4()) + display_name = "Sign-in Frequency Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.EXCLUDE, + rule='device.trustType -eq "ServerAD"', + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' enforces sign-in frequency for non-corporate devices." + ) + + def test_entra_policy_report_only_mode(self): + """Test when policy is in report-only mode - should FAIL.""" + policy_id = str(uuid4()) + display_name = "Sign-in Frequency Policy" + 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( + "prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_enforce_sign_in_frequency.entra_conditional_access_policy_enforce_sign_in_frequency import ( + entra_conditional_access_policy_enforce_sign_in_frequency, + ) + 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=[], + device_filter=DeviceFilter( + mode=DeviceFilterMode.INCLUDE, + rule="device.isCompliant -ne True", + ), + ), + grant_controls=GrantControls( + built_in_controls=[], + operator=GrantControlOperator.AND, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=True, + frequency=1, + type=SignInFrequencyType.HOURS, + interval=SignInFrequencyInterval.TIME_BASED, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_conditional_access_policy_enforce_sign_in_frequency() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' is configured to enforce sign-in frequency for non-corporate devices but is in report-only mode." + )