mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
refactor(m365): scope entra_emergency_access_exclusion to Block-grant policies (#10849)
This commit is contained in:
committed by
GitHub
parent
9cedbd3582
commit
80482da1cb
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
+4
-4
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+76
-85
@@ -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
|
||||
|
||||
+329
-21
@@ -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."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user