From f389ee3d1d53324e5ae58eadca8fa47fea9192a3 Mon Sep 17 00:00:00 2001 From: "Andoni A." <14891798+andoniaf@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:51:41 +0100 Subject: [PATCH] feat(m365): add entra_conditional_access_policy_enforce_sign_in_frequency security check Move check from Azure provider to M365 provider and add device filter support to the M365 entra service. The check validates that at least one Conditional Access policy enforces sign-in frequency for non-corporate devices. Changes: - Add DeviceFilter and DeviceFilterMode models to M365 entra_service - Add device_filter to Conditions model in M365 entra_service - Update _get_conditional_access_policies to fetch device filter data - Create check in M365 provider with proper M365 patterns - Remove Azure check (wrong provider) - Update CHANGELOG to reflect M365 provider --- prowler/CHANGELOG.md | 2 +- ...access_policy_enforce_sign_in_frequency.py | 133 --- .../__init__.py | 0 ...cy_enforce_sign_in_frequency.metadata.json | 17 +- ...access_policy_enforce_sign_in_frequency.py | 121 ++ .../m365/services/entra/entra_service.py | 33 + ...s_policy_enforce_sign_in_frequency_test.py | 931 --------------- ...s_policy_enforce_sign_in_frequency_test.py | 1010 +++++++++++++++++ 8 files changed, 1172 insertions(+), 1075 deletions(-) delete mode 100644 prowler/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py rename prowler/providers/{azure => m365}/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/__init__.py (100%) rename prowler/providers/{azure => m365}/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.metadata.json (65%) create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency.py delete mode 100644 tests/providers/azure/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py create mode 100644 tests/providers/m365/services/entra/entra_conditional_access_policy_enforce_sign_in_frequency/entra_conditional_access_policy_enforce_sign_in_frequency_test.py 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." + )