From 56d7c24dd494c48be1b81220e95a4f35d9dbd2f6 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Mon, 23 Feb 2026 17:04:33 +0100 Subject: [PATCH] feat(m365): add entra_sign_in_frequency_for_non_corporate_devices security check Add new security check entra_sign_in_frequency_for_non_corporate_devices for m365 provider. Includes check implementation, metadata, and unit tests. --- prowler/CHANGELOG.md | 1 + prowler/compliance/m365/cis_4.0_m365.json | 3 +- prowler/compliance/m365/cis_6.0_m365.json | 3 +- .../compliance/m365/iso27001_2022_m365.json | 3 + .../m365/prowler_threatscore_m365.json | 3 +- .../__init__.py | 0 ...cy_for_non_corporate_devices.metadata.json | 37 ++ ..._in_frequency_for_non_corporate_devices.py | 60 ++ .../__init__.py | 0 ...requency_for_non_corporate_devices_test.py | 562 ++++++++++++++++++ 10 files changed, 669 insertions(+), 3 deletions(-) create mode 100644 prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/__init__.py create mode 100644 prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.metadata.json create mode 100644 prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.py create mode 100644 tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/__init__.py create mode 100644 tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b593b33cc1..0793c058a6 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added +- `entra_sign_in_frequency_for_non_corporate_devices` check for m365 provider [(#10139)](https://github.com/prowler-cloud/prowler/pull/10139) - `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080) - `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087) - `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033) diff --git a/prowler/compliance/m365/cis_4.0_m365.json b/prowler/compliance/m365/cis_4.0_m365.json index 3906cbc845..3b5fb7bee7 100644 --- a/prowler/compliance/m365/cis_4.0_m365.json +++ b/prowler/compliance/m365/cis_4.0_m365.json @@ -1189,7 +1189,8 @@ "Id": "5.2.2.4", "Description": "In complex deployments, organizations might have a need to restrict authentication sessions. Conditional Access policies allow for the targeting of specific user accounts. Some scenarios might include:- Resource access from an unmanaged or shared device- Access to sensitive information from an external network- High-privileged users- Business-critical applicationsEnsure Sign-in frequency periodic reauthentication does not exceed `4 hours` for E3 tenants, or `24 hours` for E5 tenants using Privileged Identity Management.Ensure `Persistent browser session` is set to `Never persistent`**Note:** This CA policy can be added to the previous CA policy in this benchmark \"Ensure multifactor authentication is enabled for all users in administrative roles\"", "Checks": [ - "entra_admin_users_sign_in_frequency_enabled" + "entra_admin_users_sign_in_frequency_enabled", + "entra_sign_in_frequency_for_non_corporate_devices" ], "Attributes": [ { diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index b1da0cc60d..d14cf1b857 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -1580,7 +1580,8 @@ "Id": "5.2.2.11", "Description": "Sign-in frequency defines the time period before a user is asked to sign in again when attempting to access a resource. Ensure sign-in frequency for Intune Enrollment is set to 'Every time'.", "Checks": [ - "entra_admin_users_sign_in_frequency_enabled" + "entra_admin_users_sign_in_frequency_enabled", + "entra_sign_in_frequency_for_non_corporate_devices" ], "Attributes": [ { diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index ac9c677f39..9a35dc2595 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -243,6 +243,7 @@ "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", + "entra_sign_in_frequency_for_non_corporate_devices", "entra_users_mfa_enabled", "exchange_organization_modern_authentication_enabled", "exchange_transport_config_smtp_auth_disabled", @@ -616,6 +617,7 @@ "entra_users_mfa_enabled", "entra_managed_device_required_for_mfa_registration", "entra_admin_users_phishing_resistant_mfa_enabled", + "entra_sign_in_frequency_for_non_corporate_devices", "entra_users_mfa_capable" ] }, @@ -687,6 +689,7 @@ "entra_admin_users_mfa_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", + "entra_sign_in_frequency_for_non_corporate_devices", "entra_users_mfa_enabled", "entra_identity_protection_sign_in_risk_enabled" ] diff --git a/prowler/compliance/m365/prowler_threatscore_m365.json b/prowler/compliance/m365/prowler_threatscore_m365.json index 512c401c8c..a6a44a66a5 100644 --- a/prowler/compliance/m365/prowler_threatscore_m365.json +++ b/prowler/compliance/m365/prowler_threatscore_m365.json @@ -805,7 +805,8 @@ "Id": "1.3.8", "Description": "Ensure Sign-in frequency is enabled and browser sessions are not persistent for Administrative users", "Checks": [ - "entra_admin_users_sign_in_frequency_enabled" + "entra_admin_users_sign_in_frequency_enabled", + "entra_sign_in_frequency_for_non_corporate_devices" ], "Attributes": [ { diff --git a/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/__init__.py b/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.metadata.json b/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.metadata.json new file mode 100644 index 0000000000..8dec557c8e --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_sign_in_frequency_for_non_corporate_devices", + "CheckTitle": "Conditional Access Policy enforces sign-in frequency for non-corporate devices", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Conditional Access Policy", + "ResourceGroup": "IAM", + "Description": "At least one Conditional Access Policy targets **all users** and **all cloud apps** with **sign-in frequency** controls enabled, reducing the session lifetime on unmanaged or non-corporate devices.", + "Risk": "Without sign-in frequency controls, sessions on non-corporate or unmanaged devices can remain active indefinitely. Attackers who steal a session token from such a device can maintain access for an extended period, increasing the risk of **data exfiltration** and **lateral movement**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com/).\n2. Go to **Protection** > **Conditional Access** > **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **All users**.\n5. Under **Target resources**, select **All cloud apps**.\n6. Under **Session**, enable **Sign-in frequency** and set it to a value appropriate for your organization (e.g., 1 hour for unmanaged devices).\n7. Set the policy state to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create a Conditional Access Policy that enforces a short sign-in frequency for non-corporate devices. This limits the window of exposure from stolen tokens on unmanaged endpoints and aligns with **zero trust** principles.", + "Url": "https://hub.prowler.com/check/entra_sign_in_frequency_for_non_corporate_devices" + } + }, + "Categories": [ + "e3", + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.py b/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.py new file mode 100644 index 0000000000..0cfbec5843 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices.py @@ -0,0 +1,60 @@ +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, + SignInFrequencyInterval, +) + + +class entra_sign_in_frequency_for_non_corporate_devices(Check): + """Check if at least one Conditional Access Policy enforces sign-in frequency for non-corporate devices.""" + + def execute(self) -> list[CheckReportM365]: + """Execute the check for sign-in frequency enforcement on non-corporate devices. + + Returns: + list[CheckReportM365]: A list containing the check result. + """ + 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 + or "All" + not in policy.conditions.user_conditions.included_users + or "All" + not in policy.conditions.application_conditions.included_applications + or not policy.session_controls.sign_in_frequency.is_enabled + or policy.session_controls.sign_in_frequency.interval + != SignInFrequencyInterval.TIME_BASED + ): + 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}' reports sign-in frequency for non-corporate devices but does not enforce it." + 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 diff --git a/tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/__init__.py b/tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices_test.py b/tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices_test.py new file mode 100644 index 0000000000..e3c5e56968 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_sign_in_frequency_for_non_corporate_devices/entra_sign_in_frequency_for_non_corporate_devices_test.py @@ -0,0 +1,562 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + SignInFrequencyType, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_entra_sign_in_frequency_for_non_corporate_devices: + def test_entra_no_conditional_access_policies(self): + 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_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + + entra_client.conditional_access_policies = {} + + check = entra_sign_in_frequency_for_non_corporate_devices() + 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" + assert result[0].location == "global" + + def test_entra_policy_disabled(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + 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=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + 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_sign_in_frequency_for_non_corporate_devices() + 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" + assert result[0].location == "global" + + def test_entra_policy_sign_in_frequency_disabled(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + 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=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser( + is_enabled=False, mode="always" + ), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=None, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_sign_in_frequency_for_non_corporate_devices() + 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" + assert result[0].location == "global" + + def test_entra_policy_not_targeting_all_users(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["All"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["some-user-id"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + 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_sign_in_frequency_for_non_corporate_devices() + 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 == {} + + def test_entra_policy_not_targeting_all_applications(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["some-app-id"], + excluded_applications=[], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + 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_sign_in_frequency_for_non_corporate_devices() + 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 == {} + + def test_entra_policy_every_time_interval_skipped(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=id, + display_name="Test", + 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=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + 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_sign_in_frequency_for_non_corporate_devices() + 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 == {} + + def test_entra_policy_enabled_for_reporting(self): + 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_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=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=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + 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_sign_in_frequency_for_non_corporate_devices() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' reports sign-in frequency for non-corporate devices but does not enforce it." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global" + + def test_entra_policy_enabled_and_enforcing(self): + 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_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_sign_in_frequency_for_non_corporate_devices.entra_sign_in_frequency_for_non_corporate_devices import ( + entra_sign_in_frequency_for_non_corporate_devices, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + id: ConditionalAccessPolicy( + id=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=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[], operator=GrantControlOperator.AND + ), + 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_sign_in_frequency_for_non_corporate_devices() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy '{display_name}' enforces sign-in frequency for non-corporate devices." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == id + assert result[0].location == "global"