Skip to content

Commit e96ea54

Browse files
feat(m365): add entra_break_glass_users_fido2_security_key_registered security check (#10213)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
1 parent dfca976 commit e96ea54

File tree

13 files changed

+816
-21
lines changed

13 files changed

+816
-21
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
3434
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
3535
- `entra_conditional_access_policy_require_mfa_for_management_api` check for M365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150)
3636
- OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135)
37+
- `entra_break_glass_account_fido2_security_key_registered` check for m365 provider [(#10213)](https://github.com/prowler-cloud/prowler/pull/10213)
3738
- `entra_default_app_management_policy_enabled` check for M365 provider [(#9898)](https://github.com/prowler-cloud/prowler/pull/9898)
3839
- OpenStack networking service with 6 security checks [(#9970)](https://github.com/prowler-cloud/prowler/pull/9970)
3940
- OpenStack block storage service with 7 security checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
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.",
3434
"Checks": [
35+
"entra_break_glass_account_fido2_security_key_registered",
3536
"entra_emergency_access_exclusion"
3637
],
3738
"Attributes": [
@@ -1214,7 +1215,8 @@
12141215
"Id": "5.2.2.5",
12151216
"Description": "Authentication strength is a Conditional Access control that allows administrators to specify which combination of authentication methods can be used to access a resource. For example, they can make only phishing-resistant authentication methods available to access a sensitive resource. But to access a non-sensitive resource, they can allow less secure multifactor authentication (MFA) combinations, such as password + SMS.Microsoft has 3 built-in authentication strengths. MFA strength, Passwordless MFA strength, and Phishing-resistant MFA strength. Ensure administrator roles are using a CA policy with `Phishing-resistant MFA strength`.Administrators can then enroll using one of 3 methods:- FIDO2 Security Key- Windows Hello for Business- Certificate-based authentication (Multi-Factor)**Note:** Additional steps to configure methods such as FIDO2 keys are not covered here but can be found in related MS articles in the references section. The Conditional Access policy only ensures 1 of the 3 methods is used.**Warning:** Administrators should be pre-registered for a strong authentication mechanism before this Conditional Access Policy is enforced. Additionally, as stated elsewhere in the CIS Benchmark a break-glass administrator account should be excluded from this policy to ensure unfettered access in the case of an emergency.",
12161217
"Checks": [
1217-
"entra_admin_users_phishing_resistant_mfa_enabled"
1218+
"entra_admin_users_phishing_resistant_mfa_enabled",
1219+
"entra_break_glass_account_fido2_security_key_registered"
12181220
],
12191221
"Attributes": [
12201222
{

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
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.",
3434
"Checks": [
35+
"entra_break_glass_account_fido2_security_key_registered",
3536
"entra_emergency_access_exclusion"
3637
],
3738
"Attributes": [
@@ -1446,7 +1447,8 @@
14461447
"Id": "5.2.2.5",
14471448
"Description": "Authentication strength is a Conditional Access control that allows administrators to specify which combination of authentication methods can be used to access a resource. Ensure administrator roles are using a CA policy with Phishing-resistant MFA strength.",
14481449
"Checks": [
1449-
"entra_admin_users_phishing_resistant_mfa_enabled"
1450+
"entra_admin_users_phishing_resistant_mfa_enabled",
1451+
"entra_break_glass_account_fido2_security_key_registered"
14501452
],
14511453
"Attributes": [
14521454
{

prowler/compliance/m365/iso27001_2022_m365.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@
240240
"defenderxdr_endpoint_privileged_user_exposed_credentials",
241241
"entra_admin_users_mfa_enabled",
242242
"entra_admin_users_sign_in_frequency_enabled",
243+
"entra_break_glass_account_fido2_security_key_registered",
243244
"entra_default_app_management_policy_enabled",
244245
"entra_all_apps_conditional_access_coverage",
245246
"entra_legacy_authentication_blocked",
@@ -649,6 +650,7 @@
649650
"entra_admin_users_mfa_enabled",
650651
"entra_admin_users_phishing_resistant_mfa_enabled",
651652
"entra_admin_users_sign_in_frequency_enabled",
653+
"entra_break_glass_account_fido2_security_key_registered",
652654
"entra_app_registration_no_unused_privileged_permissions",
653655
"entra_policy_ensure_default_user_cannot_create_tenants",
654656
"entra_policy_guest_invite_only_for_admin_roles",
@@ -690,6 +692,7 @@
690692
"entra_admin_users_mfa_enabled",
691693
"entra_admin_users_sign_in_frequency_enabled",
692694
"entra_all_apps_conditional_access_coverage",
695+
"entra_break_glass_account_fido2_security_key_registered",
693696
"entra_identity_protection_sign_in_risk_enabled",
694697
"entra_managed_device_required_for_authentication",
695698
"entra_seamless_sso_disabled",

prowler/compliance/m365/prowler_threatscore_m365.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"Id": "1.1.2",
2828
"Description": "Ensure multifactor authentication is enabled for all users in administrative roles",
2929
"Checks": [
30-
"entra_admin_users_mfa_enabled"
30+
"entra_admin_users_mfa_enabled",
31+
"entra_break_glass_account_fido2_security_key_registered"
3132
],
3233
"Attributes": [
3334
{
@@ -81,7 +82,8 @@
8182
"Id": "1.1.5",
8283
"Description": "Ensure 'Phishing-resistant MFA strength' is required for Administrators",
8384
"Checks": [
84-
"entra_admin_users_phishing_resistant_mfa_enabled"
85+
"entra_admin_users_phishing_resistant_mfa_enabled",
86+
"entra_break_glass_account_fido2_security_key_registered"
8587
],
8688
"Attributes": [
8789
{

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"Provider": "m365",
3+
"CheckID": "entra_break_glass_account_fido2_security_key_registered",
4+
"CheckTitle": "Break glass account has a FIDO2 security key registered for phishing-resistant authentication",
5+
"CheckType": [],
6+
"ServiceName": "entra",
7+
"SubServiceName": "",
8+
"ResourceIdTemplate": "",
9+
"Severity": "critical",
10+
"ResourceType": "NotDefined",
11+
"ResourceGroup": "IAM",
12+
"Description": "Break glass (emergency access) accounts should have at least one **FIDO2 security key** registered as their authentication method. These accounts are identified as users excluded from all enabled Conditional Access policies.",
13+
"Risk": "Without FIDO2 security keys, break glass accounts rely on weaker authentication methods vulnerable to **phishing, credential theft, and man-in-the-middle attacks**. Compromised emergency access accounts could grant an attacker unrestricted tenant access, bypassing all Conditional Access protections.",
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/authentication/concept-authentication-passwordless#fido2-security-keys"
18+
],
19+
"Remediation": {
20+
"Code": {
21+
"CLI": "",
22+
"NativeIaC": "",
23+
"Other": "1. Navigate to Microsoft Entra admin center > Users.\n2. Select the break glass account.\n3. Go to Authentication methods > Add authentication method.\n4. Select FIDO2 Security Key and follow the registration steps.\n5. Store the physical FIDO2 key in a secure location (e.g., physical safe).",
24+
"Terraform": ""
25+
},
26+
"Recommendation": {
27+
"Text": "Register at least one **FIDO2 security key** for each break glass account. Store the physical keys in a secure, offline location such as a safe. Use phishing-resistant authentication to protect emergency access accounts from credential-based attacks.",
28+
"Url": "https://hub.prowler.com/check/entra_break_glass_account_fido2_security_key_registered"
29+
}
30+
},
31+
"Categories": [
32+
"identity-access",
33+
"e3"
34+
],
35+
"DependsOn": [],
36+
"RelatedTo": [
37+
"entra_emergency_access_exclusion"
38+
],
39+
"Notes": "Break glass accounts are identified as users excluded from all enabled Conditional Access policies. This check requires the entra_emergency_access_exclusion check to pass first for meaningful results."
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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_break_glass_account_fido2_security_key_registered(Check):
11+
"""Ensure that break glass accounts have FIDO2 security keys registered.
12+
13+
This check identifies break glass (emergency access) accounts by finding users
14+
excluded from all enabled Conditional Access policies, then verifies each has
15+
at least one FIDO2 security key registered as an authentication method.
16+
17+
- PASS: The break glass account has a FIDO2 security key (fido2SecurityKey) registered.
18+
- MANUAL: The account has a device-bound passkey but it cannot be confirmed as FIDO2,
19+
or no break glass accounts could be identified.
20+
- FAIL: The break glass account does not have a FIDO2 security key registered.
21+
"""
22+
23+
def execute(self) -> list[CheckReportM365]:
24+
"""Execute the check for FIDO2 registration on break glass accounts.
25+
26+
Returns:
27+
A list of reports containing the result of the check.
28+
"""
29+
findings = []
30+
31+
enabled_policies = [
32+
policy
33+
for policy in entra_client.conditional_access_policies.values()
34+
if policy.state != ConditionalAccessPolicyState.DISABLED
35+
]
36+
37+
if not enabled_policies:
38+
report = CheckReportM365(
39+
metadata=self.metadata(),
40+
resource={},
41+
resource_name="Break Glass Accounts",
42+
resource_id="breakGlassAccounts",
43+
)
44+
report.status = "MANUAL"
45+
report.status_extended = "No enabled Conditional Access policies found. Break glass accounts cannot be identified to verify FIDO2 registration."
46+
findings.append(report)
47+
return findings
48+
49+
total_policy_count = len(enabled_policies)
50+
51+
excluded_users_counter = Counter()
52+
for policy in enabled_policies:
53+
user_conditions = policy.conditions.user_conditions
54+
if user_conditions:
55+
for user_id in user_conditions.excluded_users:
56+
excluded_users_counter[user_id] += 1
57+
58+
break_glass_user_ids = [
59+
user_id
60+
for user_id, count in excluded_users_counter.items()
61+
if count == total_policy_count
62+
]
63+
64+
if not break_glass_user_ids:
65+
report = CheckReportM365(
66+
metadata=self.metadata(),
67+
resource={},
68+
resource_name="Break Glass Accounts",
69+
resource_id="breakGlassAccounts",
70+
)
71+
report.status = "MANUAL"
72+
report.status_extended = "No break glass accounts identified. No users are excluded from all enabled Conditional Access policies."
73+
findings.append(report)
74+
return findings
75+
76+
for user_id in break_glass_user_ids:
77+
user = entra_client.users.get(user_id)
78+
if not user:
79+
continue
80+
81+
report = CheckReportM365(
82+
metadata=self.metadata(),
83+
resource=user,
84+
resource_name=user.name,
85+
resource_id=user.id,
86+
)
87+
88+
auth_methods = set(user.authentication_methods)
89+
has_fido2 = "fido2SecurityKey" in auth_methods
90+
has_passkey_device_bound = "passKeyDeviceBound" in auth_methods
91+
92+
if has_fido2:
93+
report.status = "PASS"
94+
report.status_extended = f"Break glass account {user.name} has a FIDO2 security key registered."
95+
elif has_passkey_device_bound:
96+
report.status = "MANUAL"
97+
report.status_extended = f"Break glass account {user.name} has a device-bound passkey registered, but it cannot be confirmed whether it is a FIDO2 security key."
98+
else:
99+
report.status = "FAIL"
100+
report.status_extended = f"Break glass account {user.name} does not have a FIDO2 security key registered."
101+
102+
findings.append(report)
103+
104+
return findings

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,19 @@ def execute(self) -> list[CheckReportM365]:
9595
report.status = "PASS"
9696
exclusion_details = []
9797
if users_excluded_from_all:
98-
exclusion_details.append(f"{len(users_excluded_from_all)} user(s)")
98+
user_names = []
99+
for user_id in users_excluded_from_all:
100+
user = entra_client.users.get(user_id)
101+
user_names.append(user.name if user else user_id)
102+
exclusion_details.append(f"user(s): {', '.join(user_names)}")
99103
if groups_excluded_from_all:
100-
exclusion_details.append(f"{len(groups_excluded_from_all)} group(s)")
101-
report.status_extended = f"{' and '.join(exclusion_details)} excluded as emergency access across all {total_policy_count} enabled Conditional Access policies."
104+
group_names = []
105+
groups_by_id = {g.id: g for g in entra_client.groups}
106+
for group_id in groups_excluded_from_all:
107+
group = groups_by_id.get(group_id)
108+
group_names.append(group.name if group else group_id)
109+
exclusion_details.append(f"group(s): {', '.join(group_names)}")
110+
report.status_extended = f"Emergency access {' and '.join(exclusion_details)} excluded from all {total_policy_count} enabled Conditional Access policies."
102111
else:
103112
report.status = "FAIL"
104113
report.status_extended = f"No user or group is excluded as emergency access from all {total_policy_count} enabled Conditional Access policies."

prowler/providers/m365/services/entra/entra_service.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -590,17 +590,21 @@ async def fetch_role_members(directory_role):
590590

591591
while users_response:
592592
for user in getattr(users_response, "value", []) or []:
593+
reg_info = registration_details.get(user.id, {})
593594
users[user.id] = User(
594595
id=user.id,
595596
name=user.display_name,
596597
on_premises_sync_enabled=(
597598
True if (user.on_premises_sync_enabled) else False
598599
),
599600
directory_roles_ids=user_roles_map.get(user.id, []),
600-
is_mfa_capable=(registration_details.get(user.id, False)),
601+
is_mfa_capable=reg_info.get("is_mfa_capable", False),
601602
account_enabled=not self.user_accounts_status.get(
602603
user.id, {}
603604
).get("AccountDisabled", False),
605+
authentication_methods=reg_info.get(
606+
"authentication_methods", []
607+
),
604608
)
605609

606610
next_link = getattr(users_response, "odata_next_link", None)
@@ -614,6 +618,16 @@ async def fetch_role_members(directory_role):
614618
return users
615619

616620
async def _get_user_registration_details(self):
621+
"""Retrieve user authentication method registration details.
622+
623+
Fetches registration details from the Microsoft Graph API, including
624+
MFA capability and the specific authentication methods each user has registered.
625+
626+
Returns:
627+
dict: A dictionary mapping user IDs to their registration details,
628+
where each value is a dict with 'is_mfa_capable' (bool) and
629+
'authentication_methods' (list of str).
630+
"""
617631
registration_details = {}
618632
try:
619633
registration_builder = (
@@ -623,9 +637,14 @@ async def _get_user_registration_details(self):
623637

624638
while registration_response:
625639
for detail in getattr(registration_response, "value", []) or []:
626-
registration_details.update(
627-
{detail.id: getattr(detail, "is_mfa_capable", False)}
628-
)
640+
registration_details[detail.id] = {
641+
"is_mfa_capable": getattr(detail, "is_mfa_capable", False),
642+
"authentication_methods": [
643+
str(method)
644+
for method in getattr(detail, "methods_registered", [])
645+
or []
646+
],
647+
}
629648

630649
next_link = getattr(registration_response, "odata_next_link", None)
631650
if not next_link:
@@ -1030,12 +1049,26 @@ class AdminRoles(Enum):
10301049

10311050

10321051
class User(BaseModel):
1052+
"""Model representing a Microsoft Entra ID user.
1053+
1054+
Attributes:
1055+
id: The user's unique identifier.
1056+
name: The user's display name.
1057+
on_premises_sync_enabled: Whether the user is synced from on-premises directory.
1058+
directory_roles_ids: List of directory role template IDs assigned to the user.
1059+
is_mfa_capable: Whether the user has registered a strong authentication method for MFA.
1060+
account_enabled: Whether the user account is enabled.
1061+
authentication_methods: List of authentication method types registered by the user
1062+
(e.g., 'fido2SecurityKey', 'microsoftAuthenticatorPush', 'mobilePhone').
1063+
"""
1064+
10331065
id: str
10341066
name: str
10351067
on_premises_sync_enabled: bool
10361068
directory_roles_ids: List[str] = []
10371069
is_mfa_capable: bool = False
10381070
account_enabled: bool = True
1071+
authentication_methods: List[str] = []
10391072

10401073

10411074
class InvitationsFrom(Enum):

0 commit comments

Comments
 (0)