mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-06 16:58:19 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d54b9b57d |
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
+42
@@ -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."
|
||||
}
|
||||
+137
@@ -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
|
||||
+855
@@ -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
|
||||
Reference in New Issue
Block a user