Skip to content

Commit 75d01ef

Browse files
andoniafHugoPBrito
andauthored
feat(m365): add entra_conditional_access_policy_emergency_access_exclusion security check (#9903)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
1 parent e688e60 commit 75d01ef

File tree

7 files changed

+955
-2
lines changed

7 files changed

+955
-2
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
100100

101101
### 🚀 Added
102102

103+
- `entra_emergency_access_exclusion` check for M365 provider [(#9903)](https://github.com/prowler-cloud/prowler/pull/9903)
103104
- `defender_zap_for_teams_enabled` check for M365 provider [(#9838)](https://github.com/prowler-cloud/prowler/pull/9838)
104105
- `compute_instance_suspended_without_persistent_disks` check for GCP provider [(#9747)](https://github.com/prowler-cloud/prowler/pull/9747)
105106
- `codebuild_project_webhook_filters_use_anchored_patterns` check for AWS provider to detect CodeBreach vulnerability [(#9840)](https://github.com/prowler-cloud/prowler/pull/9840)

prowler/compliance/m365/cis_4.0_m365.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
{
3232
"Id": "1.1.2",
3333
"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.",
34-
"Checks": [],
34+
"Checks": [
35+
"entra_emergency_access_exclusion"
36+
],
3537
"Attributes": [
3638
{
3739
"Section": "1 Microsoft 365 admin center",

prowler/compliance/m365/cis_6.0_m365.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
{
3232
"Id": "1.1.2",
3333
"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.",
34-
"Checks": [],
34+
"Checks": [
35+
"entra_emergency_access_exclusion"
36+
],
3537
"Attributes": [
3638
{
3739
"Section": "1 Microsoft 365 admin center",

prowler/providers/m365/services/entra/entra_emergency_access_exclusion/__init__.py

Whitespace-only changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "entra_emergency_access_exclusion",
4+
"CheckTitle": "Emergency access exclusions prevent lockout from Conditional Access policies",
5+
"CheckType": [],
6+
"ServiceName": "entra",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "high",
10+
"ResourceType": "Conditional Access Policy",
11+
"ResourceGroup": "IAM",
12+
"Description": "This check verifies that at least one **emergency access** (break glass) account or group is excluded from all **Conditional Access policies**. Emergency access accounts provide a fallback mechanism when normal administrative access is blocked due to misconfigured policies.",
13+
"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.",
14+
"RelatedUrl": "",
15+
"AdditionalURLs": [
16+
"https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access",
17+
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-block-access"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "",
22+
"NativeIaC": "",
23+
"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.",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"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.",
28+
"Url": "https://hub.prowler.com/check/entra_emergency_access_exclusion"
29+
}
30+
},
31+
"Categories": [
32+
"identity-access",
33+
"e3"
34+
],
35+
"DependsOn": [],
36+
"RelatedTo": [
37+
"entra_legacy_authentication_blocked",
38+
"entra_managed_device_required_for_authentication"
39+
],
40+
"Notes": ""
41+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from collections import Counter
2+
3+
from prowler.lib.check.models import Check, CheckReportM365
4+
from prowler.providers.m365.services.entra.entra_client import entra_client
5+
from prowler.providers.m365.services.entra.entra_service import (
6+
ConditionalAccessPolicyState,
7+
)
8+
9+
10+
class entra_emergency_access_exclusion(Check):
11+
"""Check if at least one emergency access account or group is excluded from all Conditional Access policies.
12+
13+
This check ensures that the tenant has at least one emergency/break glass account
14+
or account exclusion group that is excluded from all Conditional Access policies.
15+
This prevents accidental lockout scenarios where misconfigured CA policies could
16+
block all administrative access to the tenant.
17+
18+
- PASS: At least one user or group is excluded from all enabled Conditional Access policies,
19+
or there are no enabled policies.
20+
- FAIL: No user or group is excluded from all enabled Conditional Access policies.
21+
"""
22+
23+
def execute(self) -> list[CheckReportM365]:
24+
"""Execute the check for emergency access account exclusions.
25+
26+
Returns:
27+
list[CheckReportM365]: A list containing the result of the check.
28+
"""
29+
findings = []
30+
31+
# Get all enabled CA policies (excluding disabled ones)
32+
enabled_policies = [
33+
policy
34+
for policy in entra_client.conditional_access_policies.values()
35+
if policy.state != ConditionalAccessPolicyState.DISABLED
36+
]
37+
38+
# If there are no enabled policies, there's nothing to exclude from
39+
if not enabled_policies:
40+
report = CheckReportM365(
41+
metadata=self.metadata(),
42+
resource={},
43+
resource_name="Conditional Access Policies",
44+
resource_id="conditionalAccessPolicies",
45+
)
46+
report.status = "PASS"
47+
report.status_extended = "No enabled Conditional Access policies found. Emergency access exclusions are not required."
48+
findings.append(report)
49+
return findings
50+
51+
total_policy_count = len(enabled_policies)
52+
53+
# Count how many policies exclude each user
54+
excluded_users_counter = Counter()
55+
for policy in enabled_policies:
56+
user_conditions = policy.conditions.user_conditions
57+
if user_conditions:
58+
for user_id in user_conditions.excluded_users:
59+
excluded_users_counter[user_id] += 1
60+
61+
# Count how many policies exclude each group
62+
excluded_groups_counter = Counter()
63+
for policy in enabled_policies:
64+
user_conditions = policy.conditions.user_conditions
65+
if user_conditions:
66+
for group_id in user_conditions.excluded_groups:
67+
excluded_groups_counter[group_id] += 1
68+
69+
# Find users excluded from ALL policies
70+
users_excluded_from_all = [
71+
user_id
72+
for user_id, count in excluded_users_counter.items()
73+
if count == total_policy_count
74+
]
75+
76+
# Find groups excluded from ALL policies
77+
groups_excluded_from_all = [
78+
group_id
79+
for group_id, count in excluded_groups_counter.items()
80+
if count == total_policy_count
81+
]
82+
83+
has_emergency_exclusion = bool(
84+
users_excluded_from_all or groups_excluded_from_all
85+
)
86+
87+
for policy in enabled_policies:
88+
report = CheckReportM365(
89+
metadata=self.metadata(),
90+
resource=policy,
91+
resource_name=policy.display_name,
92+
resource_id=policy.id,
93+
)
94+
95+
if has_emergency_exclusion:
96+
report.status = "PASS"
97+
exclusion_details = []
98+
if users_excluded_from_all:
99+
exclusion_details.append(f"{len(users_excluded_from_all)} user(s)")
100+
if groups_excluded_from_all:
101+
exclusion_details.append(
102+
f"{len(groups_excluded_from_all)} group(s)"
103+
)
104+
report.status_extended = f"Conditional Access Policy '{policy.display_name}' has {' and '.join(exclusion_details)} excluded as emergency access across all {total_policy_count} enabled policies."
105+
else:
106+
report.status = "FAIL"
107+
report.status_extended = f"Conditional Access Policy '{policy.display_name}' does not have any user or group excluded as emergency access from all enabled Conditional Access policies."
108+
109+
findings.append(report)
110+
111+
return findings

0 commit comments

Comments
 (0)