From 80482da1cbfed298f5f2c564e169b53ce3d9dfaf Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Tue, 12 May 2026 10:40:46 +0100 Subject: [PATCH] refactor(m365): scope entra_emergency_access_exclusion to Block-grant policies (#10849) --- prowler/CHANGELOG.md | 4 + .../compliance/m365/iso27001_2022_m365.json | 3 + ...a_emergency_access_exclusion.metadata.json | 8 +- .../entra_emergency_access_exclusion.py | 161 ++++---- .../entra_emergency_access_exclusion_test.py | 350 ++++++++++++++++-- 5 files changed, 416 insertions(+), 110 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 3f78765753..a52c01a31d 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -17,6 +17,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_users_mfa_capable` no longer flags disabled guest users by requesting `accountEnabled` and `userType` from Microsoft Graph via `$select` and using Graph as the source of truth for `account_enabled` (EXO `Get-User` does not return guest users) [(#11002)](https://github.com/prowler-cloud/prowler/pull/11002) +### 🔄 Changed + +- `entra_emergency_access_exclusion` check for M365 provider now scopes the exclusion requirement to enabled Conditional Access policies with a `Block` grant control instead of every enabled policy, focusing on the lockout-relevant policy set [(#10849)](https://github.com/prowler-cloud/prowler/pull/10849) + --- ## [5.26.0] (Prowler v5.26.0) diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 1a3419a12e..de19167e1d 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -251,6 +251,7 @@ "entra_break_glass_account_fido2_security_key_registered", "entra_conditional_access_policy_mfa_enforced_for_guest_users", "entra_default_app_management_policy_enabled", + "entra_emergency_access_exclusion", "entra_all_apps_conditional_access_coverage", "entra_conditional_access_policy_device_registration_mfa_required", "entra_intune_enrollment_sign_in_frequency_every_time", @@ -671,6 +672,7 @@ "entra_admin_users_phishing_resistant_mfa_enabled", "entra_admin_users_sign_in_frequency_enabled", "entra_break_glass_account_fido2_security_key_registered", + "entra_emergency_access_exclusion", "entra_app_registration_no_unused_privileged_permissions", "entra_policy_ensure_default_user_cannot_create_tenants", "entra_policy_guest_invite_only_for_admin_roles", @@ -727,6 +729,7 @@ "entra_conditional_access_policy_device_code_flow_blocked", "entra_conditional_access_policy_directory_sync_account_excluded", "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", + "entra_emergency_access_exclusion", "entra_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", diff --git a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json index cd7efd39e3..7d594a4975 100644 --- a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json +++ b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.metadata.json @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **emergency access** (break glass) account or group excluded from all policies. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured policies.", - "Risk": "Without emergency access accounts excluded from Conditional Access policies, a misconfiguration could lock out all administrators from the tenant. This creates a **critical availability risk** where legitimate administrators cannot access or remediate issues in the environment.", + "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **emergency access** (break glass) account or group excluded from every enabled Conditional Access policy with a **Block** grant control. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured blocking policies.", + "Risk": "Without emergency access accounts excluded from every blocking Conditional Access policy, a misconfigured Block policy can lock out all administrators from the tenant. This creates a **critical availability risk** where legitimate administrators cannot access or remediate issues in the environment.", "RelatedUrl": "", "AdditionalURLs": [ "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access", @@ -20,11 +20,11 @@ "Code": { "CLI": "", "NativeIaC": "", - "Other": "1. Create dedicated emergency access accounts or a security group in Microsoft Entra admin center.\n2. Navigate to Protection > Conditional Access > Policies.\n3. For each Conditional Access policy, add the emergency access account or group to the exclusion list under Users > Exclude.\n4. Ensure the emergency accounts are protected with strong credentials and limited usage.", + "Other": "1. Create dedicated emergency access accounts or a security group in Microsoft Entra admin center.\n2. Navigate to Protection > Conditional Access > Policies.\n3. For every Conditional Access policy whose grant control is **Block**, add the emergency access account or group to the exclusion list under Users > Exclude.\n4. Ensure the emergency accounts are protected with strong credentials and limited usage.", "Terraform": "" }, "Recommendation": { - "Text": "Create and maintain at least two emergency access accounts that are excluded from all Conditional Access policies. Store credentials securely offline, monitor usage, and test access regularly. Follow **least privilege** principles for these accounts while ensuring they can recover tenant access when needed.", + "Text": "Create and maintain at least two emergency access accounts that are excluded from every Conditional Access policy with a **Block** grant control so they can never be denied access by a misconfiguration. Store credentials securely offline, monitor usage, and test access regularly. Follow **least privilege** principles for these accounts while ensuring they can recover tenant access when needed.", "Url": "https://hub.prowler.com/check/entra_emergency_access_exclusion" } }, diff --git a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py index 45f9bda05d..28ad2a3378 100644 --- a/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py +++ b/prowler/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion.py @@ -3,87 +3,38 @@ from collections import Counter from prowler.lib.check.models import Check, CheckReportM365 from prowler.providers.m365.services.entra.entra_client import entra_client from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessGrantControl, ConditionalAccessPolicyState, ) class entra_emergency_access_exclusion(Check): - """Check if at least one emergency access account or group is excluded from all Conditional Access policies. + """Check that at least one emergency access account or group is excluded + from every enabled Conditional Access policy with a `Block` grant control. - This check ensures that the tenant has at least one emergency/break glass account - or account exclusion group that is excluded from all Conditional Access policies. - This prevents accidental lockout scenarios where misconfigured CA policies could - block all administrative access to the tenant. + Emergency access (break glass) accounts are, by definition, accounts that + cannot be blocked by Conditional Access. Membership of an account in the + exclusion list of every enabled blocking policy is therefore the necessary + condition for it to act as a true emergency account: if any enabled + blocking policy applies to it, a misconfiguration of that policy can lock + out the tenant. - - PASS: At least one user or group is excluded from all enabled Conditional Access policies, - or there are no enabled policies. - - FAIL: No user or group is excluded from all enabled Conditional Access policies. + - PASS: At least one user or group is excluded from every enabled + Conditional Access policy with a `Block` grant control, or no + enabled blocking Conditional Access policy exists. + - FAIL: One or more enabled blocking Conditional Access policies exist and + no user or group is excluded from all of them. """ def execute(self) -> list[CheckReportM365]: - """Execute the check for emergency access account exclusions. + """Execute the check for emergency access account exclusions from + blocking Conditional Access policies. Returns: list[CheckReportM365]: A list containing the result of the check. """ findings = [] - # Get all enabled CA policies (excluding disabled ones) - enabled_policies = [ - policy - for policy in entra_client.conditional_access_policies.values() - if policy.state != ConditionalAccessPolicyState.DISABLED - ] - - # If there are no enabled policies, there's nothing to exclude from - if not enabled_policies: - report = CheckReportM365( - metadata=self.metadata(), - resource={}, - resource_name="Conditional Access Policies", - resource_id="conditionalAccessPolicies", - ) - report.status = "PASS" - report.status_extended = "No enabled Conditional Access policies found. Emergency access exclusions are not required." - findings.append(report) - return findings - - total_policy_count = len(enabled_policies) - - # Count how many policies exclude each user - excluded_users_counter = Counter() - for policy in enabled_policies: - user_conditions = policy.conditions.user_conditions - if user_conditions: - for user_id in user_conditions.excluded_users: - excluded_users_counter[user_id] += 1 - - # Count how many policies exclude each group - excluded_groups_counter = Counter() - for policy in enabled_policies: - user_conditions = policy.conditions.user_conditions - if user_conditions: - for group_id in user_conditions.excluded_groups: - excluded_groups_counter[group_id] += 1 - - # Find users excluded from ALL policies - users_excluded_from_all = [ - user_id - for user_id, count in excluded_users_counter.items() - if count == total_policy_count - ] - - # Find groups excluded from ALL policies - groups_excluded_from_all = [ - group_id - for group_id, count in excluded_groups_counter.items() - if count == total_policy_count - ] - - has_emergency_exclusion = bool( - users_excluded_from_all or groups_excluded_from_all - ) - report = CheckReportM365( metadata=self.metadata(), resource={}, @@ -91,27 +42,67 @@ class entra_emergency_access_exclusion(Check): resource_id="conditionalAccessPolicies", ) - if has_emergency_exclusion: - report.status = "PASS" - exclusion_details = [] - if users_excluded_from_all: - user_names = [] - for user_id in users_excluded_from_all: - user = entra_client.users.get(user_id) - user_names.append(user.name if user else user_id) - exclusion_details.append(f"user(s): {', '.join(user_names)}") - if groups_excluded_from_all: - group_names = [] - groups_by_id = {g.id: g for g in entra_client.groups} - for group_id in groups_excluded_from_all: - group = groups_by_id.get(group_id) - group_names.append(group.name if group else group_id) - exclusion_details.append(f"group(s): {', '.join(group_names)}") - report.status_extended = f"Emergency access {' and '.join(exclusion_details)} excluded from all {total_policy_count} enabled Conditional Access policies." - else: - report.status = "FAIL" - report.status_extended = f"No user or group is excluded as emergency access from all {total_policy_count} enabled Conditional Access policies." + blocking_policies = [ + policy + for policy in entra_client.conditional_access_policies.values() + if policy.state != ConditionalAccessPolicyState.DISABLED + and ConditionalAccessGrantControl.BLOCK + in policy.grant_controls.built_in_controls + ] + if not blocking_policies: + report.status = "PASS" + report.status_extended = "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." + findings.append(report) + return findings + + total_blocking_count = len(blocking_policies) + + excluded_users_counter = Counter() + excluded_groups_counter = Counter() + for policy in blocking_policies: + user_conditions = policy.conditions.user_conditions + if not user_conditions: + continue + for user_id in user_conditions.excluded_users: + excluded_users_counter[user_id] += 1 + for group_id in user_conditions.excluded_groups: + excluded_groups_counter[group_id] += 1 + + emergency_user_ids = [ + user_id + for user_id, count in excluded_users_counter.items() + if count == total_blocking_count + ] + emergency_group_ids = [ + group_id + for group_id, count in excluded_groups_counter.items() + if count == total_blocking_count + ] + + if not (emergency_user_ids or emergency_group_ids): + report.status = "FAIL" + report.status_extended = f"No user or group is excluded as emergency access from all {total_blocking_count} enabled Conditional Access policies with a Block grant control." + findings.append(report) + return findings + + exclusion_details = [] + if emergency_user_ids: + user_names = [] + for uid in emergency_user_ids: + user = entra_client.users.get(uid) + user_names.append(user.name if user else uid) + exclusion_details.append(f"user(s): {', '.join(user_names)}") + if emergency_group_ids: + groups_by_id = {g.id: g for g in entra_client.groups} + group_names = [] + for gid in emergency_group_ids: + group = groups_by_id.get(gid) + group_names.append(group.name if group else gid) + exclusion_details.append(f"group(s): {', '.join(group_names)}") + + report.status = "PASS" + report.status_extended = f"Emergency access {' and '.join(exclusion_details)} excluded from all {total_blocking_count} enabled Conditional Access policies with a Block grant control." findings.append(report) return findings diff --git a/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py b/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py index db2fd1d193..cbbd9c6886 100644 --- a/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py +++ b/tests/providers/m365/services/entra/entra_emergency_access_exclusion/entra_emergency_access_exclusion_test.py @@ -47,7 +47,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert ( result[0].status_extended - == "No enabled Conditional Access policies found. Emergency access exclusions are not required." + == "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." ) assert result[0].resource == {} assert result[0].resource_name == "Conditional Access Policies" @@ -98,7 +98,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -122,7 +122,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert ( result[0].status_extended - == "No enabled Conditional Access policies found. Emergency access exclusions are not required." + == "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." ) def test_entra_no_emergency_access_exclusion(self): @@ -172,7 +172,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -207,7 +207,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -230,7 +230,7 @@ class Test_entra_emergency_access_exclusion: assert len(result) == 1 assert result[0].status == "FAIL" assert ( - "No user or group is excluded as emergency access from all 2 enabled Conditional Access policies" + "No user or group is excluded as emergency access from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -283,7 +283,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -318,7 +318,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -352,7 +352,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert "BreakGlass1" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -405,7 +405,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -440,7 +440,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -474,7 +474,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert "BreakGlassGroup" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -528,7 +528,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -563,7 +563,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -605,7 +605,7 @@ class Test_entra_emergency_access_exclusion: assert "BreakGlass1" in result[0].status_extended assert "BreakGlassGroup" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" @@ -658,7 +658,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -693,7 +693,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -729,7 +729,7 @@ class Test_entra_emergency_access_exclusion: assert result[0].resource_id == "conditionalAccessPolicies" assert "BreakGlass1" in result[0].status_extended assert ( - "excluded from all 1 enabled Conditional Access policies" + "excluded from all 1 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) @@ -781,7 +781,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -816,7 +816,7 @@ class Test_entra_emergency_access_exclusion: ), ), grant_controls=GrantControls( - built_in_controls=[ConditionalAccessGrantControl.MFA], + built_in_controls=[ConditionalAccessGrantControl.BLOCK], operator=GrantControlOperator.AND, ), session_controls=SessionControls( @@ -850,8 +850,316 @@ class Test_entra_emergency_access_exclusion: assert result[0].status == "PASS" assert "BreakGlass1" in result[0].status_extended assert ( - "excluded from all 2 enabled Conditional Access policies" + "excluded from all 2 enabled Conditional Access policies with a Block grant control" in result[0].status_extended ) assert result[0].resource_name == "Conditional Access Policies" assert result[0].resource_id == "conditionalAccessPolicies" + + def test_entra_only_non_blocking_policies(self): + """PASS when the tenant has only non-blocking policies (no Block grant).""" + policy_id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion import ( + entra_emergency_access_exclusion, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="MFA for all", + 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=[ConditionalAccessGrantControl.MFA], + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_emergency_access_exclusion() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "No enabled Conditional Access policies with a Block grant control found. Emergency access exclusions are not required." + ) + + def test_entra_user_excluded_from_blocking_but_included_in_non_blocking(self): + """PASS when only Block-grant exclusions are required (non-blocking inclusion is ignored).""" + block_policy_id = str(uuid4()) + mfa_policy_id = str(uuid4()) + emergency_user_id = "emergency-access-user" + 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_emergency_access_exclusion.entra_emergency_access_exclusion.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion import ( + entra_emergency_access_exclusion, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + block_policy_id: ConditionalAccessPolicy( + id=block_policy_id, + display_name="Block legacy auth", + 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=[emergency_user_id], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + mfa_policy_id: ConditionalAccessPolicy( + id=mfa_policy_id, + display_name="MFA for all", + 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=[ConditionalAccessGrantControl.MFA], + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + entra_client.users = { + emergency_user_id: User( + id=emergency_user_id, + name="BreakGlass1", + on_premises_sync_enabled=False, + authentication_methods=[], + ), + } + entra_client.groups = [] + + check = entra_emergency_access_exclusion() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "BreakGlass1" in result[0].status_extended + assert ( + "excluded from all 1 enabled Conditional Access policies with a Block grant control" + in result[0].status_extended + ) + + def test_entra_user_excluded_only_from_subset_of_blocking_policies(self): + """FAIL when the user is excluded from one Block policy but not the other.""" + block_policy_id_1 = str(uuid4()) + block_policy_id_2 = str(uuid4()) + emergency_user_id = "emergency-access-user" + 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_emergency_access_exclusion.entra_emergency_access_exclusion.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_emergency_access_exclusion.entra_emergency_access_exclusion import ( + entra_emergency_access_exclusion, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + block_policy_id_1: ConditionalAccessPolicy( + id=block_policy_id_1, + display_name="Block 1", + 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=[emergency_user_id], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + block_policy_id_2: ConditionalAccessPolicy( + id=block_policy_id_2, + display_name="Block 2", + 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=[ConditionalAccessGrantControl.BLOCK], + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + entra_client.users = { + emergency_user_id: User( + id=emergency_user_id, + name="BreakGlass1", + on_premises_sync_enabled=False, + authentication_methods=[], + ), + } + entra_client.groups = [] + + check = entra_emergency_access_exclusion() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No user or group is excluded as emergency access from all 2 enabled Conditional Access policies with a Block grant control." + )