Compare commits

...

1 Commits

Author SHA1 Message Date
Hugo P.Brito 2d54b9b57d feat(m365): add entra_emergency_access_users_not_blocked security check
Add new security check entra_emergency_access_users_not_blocked for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-04-22 14:21:51 +01:00
8 changed files with 1042 additions and 2 deletions
+1
View File
@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- SARIF output format for the IaC provider, enabling GitHub Code Scanning integration via `--output-formats sarif` [(#10626)](https://github.com/prowler-cloud/prowler/pull/10626)
- `entra_emergency_access_users_not_blocked` check for m365 provider [(#10849)](https://github.com/prowler-cloud/prowler/pull/10849)
---
+2 -1
View File
@@ -33,7 +33,8 @@
"Description": "Emergency access or \"break glass\" accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. These emergencies could be due to several things, including:- Technical failures of a cellular provider or Microsoft related service such as MFA.- The last remaining Global Administrator account is inaccessible.Ensure two `Emergency Access` accounts have been defined.**Note:** Microsoft provides several recommendations for these accounts and how to configure them. For more information on this, please refer to the references section. The CIS Benchmark outlines the more critical things to consider.",
"Checks": [
"entra_break_glass_account_fido2_security_key_registered",
"entra_emergency_access_exclusion"
"entra_emergency_access_exclusion",
"entra_emergency_access_users_not_blocked"
],
"Attributes": [
{
+2 -1
View File
@@ -33,7 +33,8 @@
"Description": "Emergency access or 'break glass' accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. Ensure two Emergency Access accounts have been defined.",
"Checks": [
"entra_break_glass_account_fido2_security_key_registered",
"entra_emergency_access_exclusion"
"entra_emergency_access_exclusion",
"entra_emergency_access_users_not_blocked"
],
"Attributes": [
{
@@ -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_users_not_blocked",
"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_users_not_blocked",
"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_users_not_blocked",
"entra_identity_protection_sign_in_risk_enabled",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -0,0 +1,42 @@
{
"Provider": "m365",
"CheckID": "entra_emergency_access_users_not_blocked",
"CheckTitle": "Conditional Access policies ensure emergency access users are not blocked from tenant access",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra **Conditional Access** policies are verified to ensure that emergency access (break glass) accounts are not blocked. Emergency access accounts must remain accessible to prevent tenant lockout during misconfiguration or outage scenarios.",
"Risk": "If emergency access accounts are blocked by Conditional Access policies, a **tenant lockout** could occur where no administrator can sign in to perform recovery. This creates a critical availability risk that may require Microsoft support intervention to resolve.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-block-access",
"https://maester.dev/docs/tests/MT.1034"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Entra admin center > Protection > Conditional Access > Policies.\n2. For each policy that has a Block grant control, add the emergency access accounts to the exclusion list under Users > Exclude.\n3. Verify that no policy with Block access applies to emergency access accounts.\n4. Test that emergency access accounts can successfully sign in.",
"Terraform": ""
},
"Recommendation": {
"Text": "Exclude all emergency access (break glass) accounts from every Conditional Access policy, especially those that block access. Regularly test that these accounts can sign in successfully and monitor their usage for anomalies.",
"Url": "https://hub.prowler.com/check/entra_emergency_access_users_not_blocked"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [
"entra_emergency_access_exclusion",
"entra_break_glass_account_fido2_security_key_registered"
],
"Notes": "Emergency access users are identified as users excluded from all enabled Conditional Access policies. This check is the Prowler equivalent of Maester test MT.1034."
}
@@ -0,0 +1,137 @@
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_users_not_blocked(Check):
"""Ensure that emergency access users are not blocked by any Conditional Access policy.
This check identifies emergency access (break glass) accounts by finding users
excluded from all enabled non-blocking Conditional Access policies, then verifies
that no enabled policy with a block grant control would apply to them.
- PASS: The emergency access user is not blocked by any Conditional Access policy.
- FAIL: The emergency access user is blocked by one or more Conditional Access policies.
- MANUAL: No emergency access users could be identified from the current policies.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check for emergency access users not blocked by CA policies.
Returns:
list[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
enabled_policies = [
policy
for policy in entra_client.conditional_access_policies.values()
if policy.state != ConditionalAccessPolicyState.DISABLED
]
if not enabled_policies:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Emergency Access Users",
resource_id="emergencyAccessUsers",
)
report.status = "MANUAL"
report.status_extended = "No enabled Conditional Access policies found. Emergency access users cannot be identified to verify they are not blocked."
findings.append(report)
return findings
# Separate blocking from non-blocking policies
non_blocking_policies = [
policy
for policy in enabled_policies
if ConditionalAccessGrantControl.BLOCK
not in policy.grant_controls.built_in_controls
]
blocking_policies = [
policy
for policy in enabled_policies
if ConditionalAccessGrantControl.BLOCK
in policy.grant_controls.built_in_controls
]
# Identify emergency access users as those excluded from all non-blocking policies.
# If there are no non-blocking policies, fall back to all enabled policies.
identification_policies = (
non_blocking_policies if non_blocking_policies else enabled_policies
)
total_identification_count = len(identification_policies)
excluded_users_counter = Counter()
for policy in identification_policies:
user_conditions = policy.conditions.user_conditions
if user_conditions:
for user_id in user_conditions.excluded_users:
excluded_users_counter[user_id] += 1
emergency_access_user_ids = [
user_id
for user_id, count in excluded_users_counter.items()
if count == total_identification_count
]
if not emergency_access_user_ids:
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Emergency Access Users",
resource_id="emergencyAccessUsers",
)
report.status = "MANUAL"
report.status_extended = "No emergency access users identified. No users are excluded from all enabled Conditional Access policies."
findings.append(report)
return findings
for user_id in emergency_access_user_ids:
user = entra_client.users.get(user_id)
if not user:
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.id,
)
policies_blocking_user = []
for policy in blocking_policies:
user_conditions = policy.conditions.user_conditions
if not user_conditions:
continue
is_excluded = user.id in user_conditions.excluded_users
if is_excluded:
continue
is_included = (
"All" in user_conditions.included_users
or user.id in user_conditions.included_users
)
if is_included:
policies_blocking_user.append(policy.display_name)
if policies_blocking_user:
report.status = "FAIL"
policy_names = ", ".join(policies_blocking_user)
report.status_extended = f"Emergency access user {user.name} is blocked by Conditional Access policies: {policy_names}."
else:
report.status = "PASS"
report.status_extended = f"Emergency access user {user.name} is not blocked by any Conditional Access policy."
findings.append(report)
return findings
@@ -0,0 +1,855 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
User,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked"
def _make_policy(
policy_id,
excluded_users=None,
excluded_groups=None,
state=None,
grant_controls_list=None,
included_users=None,
):
"""Create a ConditionalAccessPolicy for testing."""
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
return ConditionalAccessPolicy(
id=policy_id,
display_name=f"Policy {policy_id[:8]}",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=excluded_groups or [],
included_users=included_users or ["All"],
excluded_users=excluded_users or [],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=grant_controls_list
or [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=state or ConditionalAccessPolicyState.ENABLED,
)
class Test_entra_emergency_access_users_not_blocked:
def test_no_conditional_access_policies(self):
"""Test MANUAL when there are no Conditional Access policies."""
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
"No enabled Conditional Access policies found"
in result[0].status_extended
)
assert result[0].resource == {}
assert result[0].resource_name == "Emergency Access Users"
assert result[0].resource_id == "emergencyAccessUsers"
assert result[0].location == "global"
def test_all_policies_disabled(self):
"""Test MANUAL when all Conditional Access policies are disabled."""
policy_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id: _make_policy(
policy_id, state=ConditionalAccessPolicyState.DISABLED
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
"No enabled Conditional Access policies found"
in result[0].status_extended
)
def test_no_emergency_access_users_identified(self):
"""Test MANUAL when no user is excluded from all CA policies."""
policy_id_1 = str(uuid4())
policy_id_2 = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
# User-1 excluded from policy 1, user-2 from policy 2 -- no one excluded from all
entra_client.conditional_access_policies = {
policy_id_1: _make_policy(policy_id_1, excluded_users=["user-1"]),
policy_id_2: _make_policy(policy_id_2, excluded_users=["user-2"]),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
"No emergency access users identified" in result[0].status_extended
)
def test_emergency_user_not_blocked_excluded_from_blocking_policy(self):
"""Test PASS when emergency access user is excluded from all policies including blocking ones."""
policy_id_mfa = str(uuid4())
policy_id_block = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_block: _make_policy(
policy_id_block,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "EmergencyAccess1" in result[0].status_extended
assert "is not blocked" in result[0].status_extended
assert result[0].resource_name == "EmergencyAccess1"
assert result[0].resource_id == ea_user_id
def test_emergency_user_blocked_by_policy(self):
"""Test FAIL when emergency access user is excluded from non-blocking policies but not from a blocking one."""
policy_id_mfa_1 = str(uuid4())
policy_id_mfa_2 = str(uuid4())
policy_id_block = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
# User excluded from both MFA policies (identified as emergency access)
# but NOT excluded from the blocking policy (will be blocked)
entra_client.conditional_access_policies = {
policy_id_mfa_1: _make_policy(
policy_id_mfa_1,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_mfa_2: _make_policy(
policy_id_mfa_2,
excluded_users=[ea_user_id],
grant_controls_list=[
ConditionalAccessGrantControl.COMPLIANT_DEVICE
],
),
policy_id_block: _make_policy(
policy_id_block,
excluded_users=[],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "EmergencyAccess1" in result[0].status_extended
assert "is blocked" in result[0].status_extended
assert f"Policy {policy_id_block[:8]}" in result[0].status_extended
def test_emergency_user_blocked_by_multiple_policies(self):
"""Test FAIL with multiple blocking policies listed in the status message."""
policy_id_mfa = str(uuid4())
policy_id_block_1 = str(uuid4())
policy_id_block_2 = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_block_1: _make_policy(
policy_id_block_1,
excluded_users=[],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
policy_id_block_2: _make_policy(
policy_id_block_2,
excluded_users=[],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "EmergencyAccess1" in result[0].status_extended
assert f"Policy {policy_id_block_1[:8]}" in result[0].status_extended
assert f"Policy {policy_id_block_2[:8]}" in result[0].status_extended
def test_emergency_user_no_blocking_policies(self):
"""Test PASS when there are no blocking policies at all."""
policy_id_1 = str(uuid4())
policy_id_2 = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id_1: _make_policy(
policy_id_1,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_2: _make_policy(
policy_id_2,
excluded_users=[ea_user_id],
grant_controls_list=[
ConditionalAccessGrantControl.COMPLIANT_DEVICE
],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=[],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is not blocked" in result[0].status_extended
def test_multiple_emergency_users_mixed_results(self):
"""Test mixed results when one user is excluded from blocking policy and another is not."""
policy_id_mfa = str(uuid4())
policy_id_block = str(uuid4())
ea_user_id_1 = str(uuid4())
ea_user_id_2 = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
# Both users excluded from MFA policy (identified as emergency access)
# Only user 1 excluded from blocking policy
entra_client.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id_1, ea_user_id_2],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_block: _make_policy(
policy_id_block,
excluded_users=[ea_user_id_1],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id_1: User(
id=ea_user_id_1,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
ea_user_id_2: User(
id=ea_user_id_2,
name="EmergencyAccess2",
on_premises_sync_enabled=False,
authentication_methods=["mobilePhone"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 2
statuses = {r.resource_name: r.status for r in result}
assert statuses["EmergencyAccess1"] == "PASS"
assert statuses["EmergencyAccess2"] == "FAIL"
def test_emergency_user_not_in_users_dict(self):
"""Test that an emergency user excluded from all policies but not in users dict is skipped."""
policy_id = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id: _make_policy(policy_id, excluded_users=[ea_user_id]),
}
entra_client.users = {}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 0
def test_disabled_policies_ignored(self):
"""Test that disabled policies are not considered for identifying emergency access users."""
policy_id_enabled = str(uuid4())
policy_id_disabled = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id_enabled: _make_policy(
policy_id_enabled, excluded_users=[ea_user_id]
),
policy_id_disabled: _make_policy(
policy_id_disabled,
excluded_users=[],
state=ConditionalAccessPolicyState.DISABLED,
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_name == "EmergencyAccess1"
def test_emergency_user_with_reporting_only_blocking_policy(self):
"""Test that report-only blocking policies are still evaluated since they are not disabled."""
policy_id_mfa = str(uuid4())
policy_id_reporting_block = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_reporting_block: _make_policy(
policy_id_reporting_block,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is not blocked" in result[0].status_extended
def test_only_blocking_policies_fallback_identification(self):
"""Test identification fallback when all policies are blocking policies."""
policy_id_block_1 = str(uuid4())
policy_id_block_2 = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
# All policies are blocking -- fallback to using all policies for identification
entra_client.conditional_access_policies = {
policy_id_block_1: _make_policy(
policy_id_block_1,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
policy_id_block_2: _make_policy(
policy_id_block_2,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
# User is excluded from all (blocking) policies → identified and not blocked
assert len(result) == 1
assert result[0].status == "PASS"
assert "is not blocked" in result[0].status_extended
def test_emergency_user_blocked_by_specific_user_inclusion(self):
"""Test FAIL when blocking policy includes the emergency user by specific ID (not 'All')."""
policy_id_mfa = str(uuid4())
policy_id_block = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
entra_client.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_block: _make_policy(
policy_id_block,
excluded_users=[],
included_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert "EmergencyAccess1" in result[0].status_extended
assert "is blocked" in result[0].status_extended
def test_emergency_user_not_included_in_blocking_policy(self):
"""Test PASS when blocking policy targets specific users that don't include the emergency user."""
policy_id_mfa = str(uuid4())
policy_id_block = str(uuid4())
ea_user_id = str(uuid4())
other_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
# Blocking policy targets a different user, not the emergency access user
entra_client.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_block: _make_policy(
policy_id_block,
excluded_users=[],
included_users=[other_user_id],
grant_controls_list=[ConditionalAccessGrantControl.BLOCK],
),
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is not blocked" in result[0].status_extended
def test_blocking_policy_with_no_user_conditions(self):
"""Test PASS when a blocking policy has no user conditions (skipped in evaluation)."""
policy_id_mfa = str(uuid4())
policy_id_block = str(uuid4())
ea_user_id = str(uuid4())
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE_PATH}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_emergency_access_users_not_blocked.entra_emergency_access_users_not_blocked import (
entra_emergency_access_users_not_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
# Create a blocking policy with no user_conditions
blocking_policy_no_conditions = ConditionalAccessPolicy(
id=policy_id_block,
display_name=f"Policy {policy_id_block[:8]}",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=None,
),
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.conditional_access_policies = {
policy_id_mfa: _make_policy(
policy_id_mfa,
excluded_users=[ea_user_id],
grant_controls_list=[ConditionalAccessGrantControl.MFA],
),
policy_id_block: blocking_policy_no_conditions,
}
entra_client.users = {
ea_user_id: User(
id=ea_user_id,
name="EmergencyAccess1",
on_premises_sync_enabled=False,
authentication_methods=["fido2SecurityKey"],
),
}
check = entra_emergency_access_users_not_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is not blocked" in result[0].status_extended