mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
dfca97633e
commit
e96ea54f3b
@@ -34,6 +34,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
|
||||
- `entra_conditional_access_policy_require_mfa_for_management_api` check for M365 provider [(#10150)](https://github.com/prowler-cloud/prowler/pull/10150)
|
||||
- OpenStack provider multiple regions support [(#10135)](https://github.com/prowler-cloud/prowler/pull/10135)
|
||||
- `entra_break_glass_account_fido2_security_key_registered` check for m365 provider [(#10213)](https://github.com/prowler-cloud/prowler/pull/10213)
|
||||
- `entra_default_app_management_policy_enabled` check for M365 provider [(#9898)](https://github.com/prowler-cloud/prowler/pull/9898)
|
||||
- OpenStack networking service with 6 security checks [(#9970)](https://github.com/prowler-cloud/prowler/pull/9970)
|
||||
- OpenStack block storage service with 7 security checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"Id": "1.1.2",
|
||||
"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"
|
||||
],
|
||||
"Attributes": [
|
||||
@@ -1214,7 +1215,8 @@
|
||||
"Id": "5.2.2.5",
|
||||
"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.",
|
||||
"Checks": [
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled"
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"Id": "1.1.2",
|
||||
"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"
|
||||
],
|
||||
"Attributes": [
|
||||
@@ -1446,7 +1447,8 @@
|
||||
"Id": "5.2.2.5",
|
||||
"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.",
|
||||
"Checks": [
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled"
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
"defenderxdr_endpoint_privileged_user_exposed_credentials",
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_default_app_management_policy_enabled",
|
||||
"entra_all_apps_conditional_access_coverage",
|
||||
"entra_legacy_authentication_blocked",
|
||||
@@ -649,6 +650,7 @@
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
"entra_policy_guest_invite_only_for_admin_roles",
|
||||
@@ -690,6 +692,7 @@
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_all_apps_conditional_access_coverage",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_identity_protection_sign_in_risk_enabled",
|
||||
"entra_managed_device_required_for_authentication",
|
||||
"entra_seamless_sso_disabled",
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"Id": "1.1.2",
|
||||
"Description": "Ensure multifactor authentication is enabled for all users in administrative roles",
|
||||
"Checks": [
|
||||
"entra_admin_users_mfa_enabled"
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -81,7 +82,8 @@
|
||||
"Id": "1.1.5",
|
||||
"Description": "Ensure 'Phishing-resistant MFA strength' is required for Administrators",
|
||||
"Checks": [
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled"
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_break_glass_account_fido2_security_key_registered",
|
||||
"CheckTitle": "Break glass account has a FIDO2 security key registered for phishing-resistant authentication",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"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.",
|
||||
"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.",
|
||||
"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/authentication/concept-authentication-passwordless#fido2-security-keys"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"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).",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"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.",
|
||||
"Url": "https://hub.prowler.com/check/entra_break_glass_account_fido2_security_key_registered"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access",
|
||||
"e3"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"entra_emergency_access_exclusion"
|
||||
],
|
||||
"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."
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
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 (
|
||||
ConditionalAccessPolicyState,
|
||||
)
|
||||
|
||||
|
||||
class entra_break_glass_account_fido2_security_key_registered(Check):
|
||||
"""Ensure that break glass accounts have FIDO2 security keys registered.
|
||||
|
||||
This check identifies break glass (emergency access) accounts by finding users
|
||||
excluded from all enabled Conditional Access policies, then verifies each has
|
||||
at least one FIDO2 security key registered as an authentication method.
|
||||
|
||||
- PASS: The break glass account has a FIDO2 security key (fido2SecurityKey) registered.
|
||||
- MANUAL: The account has a device-bound passkey but it cannot be confirmed as FIDO2,
|
||||
or no break glass accounts could be identified.
|
||||
- FAIL: The break glass account does not have a FIDO2 security key registered.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the check for FIDO2 registration on break glass accounts.
|
||||
|
||||
Returns:
|
||||
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="Break Glass Accounts",
|
||||
resource_id="breakGlassAccounts",
|
||||
)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = "No enabled Conditional Access policies found. Break glass accounts cannot be identified to verify FIDO2 registration."
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
total_policy_count = len(enabled_policies)
|
||||
|
||||
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
|
||||
|
||||
break_glass_user_ids = [
|
||||
user_id
|
||||
for user_id, count in excluded_users_counter.items()
|
||||
if count == total_policy_count
|
||||
]
|
||||
|
||||
if not break_glass_user_ids:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Break Glass Accounts",
|
||||
resource_id="breakGlassAccounts",
|
||||
)
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = "No break glass accounts identified. No users are excluded from all enabled Conditional Access policies."
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
for user_id in break_glass_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,
|
||||
)
|
||||
|
||||
auth_methods = set(user.authentication_methods)
|
||||
has_fido2 = "fido2SecurityKey" in auth_methods
|
||||
has_passkey_device_bound = "passKeyDeviceBound" in auth_methods
|
||||
|
||||
if has_fido2:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Break glass account {user.name} has a FIDO2 security key registered."
|
||||
elif has_passkey_device_bound:
|
||||
report.status = "MANUAL"
|
||||
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."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Break glass account {user.name} does not have a FIDO2 security key registered."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+12
-3
@@ -95,10 +95,19 @@ class entra_emergency_access_exclusion(Check):
|
||||
report.status = "PASS"
|
||||
exclusion_details = []
|
||||
if users_excluded_from_all:
|
||||
exclusion_details.append(f"{len(users_excluded_from_all)} user(s)")
|
||||
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:
|
||||
exclusion_details.append(f"{len(groups_excluded_from_all)} group(s)")
|
||||
report.status_extended = f"{' and '.join(exclusion_details)} excluded as emergency access across all {total_policy_count} enabled Conditional Access policies."
|
||||
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."
|
||||
|
||||
@@ -590,6 +590,7 @@ class Entra(M365Service):
|
||||
|
||||
while users_response:
|
||||
for user in getattr(users_response, "value", []) or []:
|
||||
reg_info = registration_details.get(user.id, {})
|
||||
users[user.id] = User(
|
||||
id=user.id,
|
||||
name=user.display_name,
|
||||
@@ -597,10 +598,13 @@ class Entra(M365Service):
|
||||
True if (user.on_premises_sync_enabled) else False
|
||||
),
|
||||
directory_roles_ids=user_roles_map.get(user.id, []),
|
||||
is_mfa_capable=(registration_details.get(user.id, False)),
|
||||
is_mfa_capable=reg_info.get("is_mfa_capable", False),
|
||||
account_enabled=not self.user_accounts_status.get(
|
||||
user.id, {}
|
||||
).get("AccountDisabled", False),
|
||||
authentication_methods=reg_info.get(
|
||||
"authentication_methods", []
|
||||
),
|
||||
)
|
||||
|
||||
next_link = getattr(users_response, "odata_next_link", None)
|
||||
@@ -614,6 +618,16 @@ class Entra(M365Service):
|
||||
return users
|
||||
|
||||
async def _get_user_registration_details(self):
|
||||
"""Retrieve user authentication method registration details.
|
||||
|
||||
Fetches registration details from the Microsoft Graph API, including
|
||||
MFA capability and the specific authentication methods each user has registered.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping user IDs to their registration details,
|
||||
where each value is a dict with 'is_mfa_capable' (bool) and
|
||||
'authentication_methods' (list of str).
|
||||
"""
|
||||
registration_details = {}
|
||||
try:
|
||||
registration_builder = (
|
||||
@@ -623,9 +637,14 @@ class Entra(M365Service):
|
||||
|
||||
while registration_response:
|
||||
for detail in getattr(registration_response, "value", []) or []:
|
||||
registration_details.update(
|
||||
{detail.id: getattr(detail, "is_mfa_capable", False)}
|
||||
)
|
||||
registration_details[detail.id] = {
|
||||
"is_mfa_capable": getattr(detail, "is_mfa_capable", False),
|
||||
"authentication_methods": [
|
||||
str(method)
|
||||
for method in getattr(detail, "methods_registered", [])
|
||||
or []
|
||||
],
|
||||
}
|
||||
|
||||
next_link = getattr(registration_response, "odata_next_link", None)
|
||||
if not next_link:
|
||||
@@ -1030,12 +1049,26 @@ class AdminRoles(Enum):
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
"""Model representing a Microsoft Entra ID user.
|
||||
|
||||
Attributes:
|
||||
id: The user's unique identifier.
|
||||
name: The user's display name.
|
||||
on_premises_sync_enabled: Whether the user is synced from on-premises directory.
|
||||
directory_roles_ids: List of directory role template IDs assigned to the user.
|
||||
is_mfa_capable: Whether the user has registered a strong authentication method for MFA.
|
||||
account_enabled: Whether the user account is enabled.
|
||||
authentication_methods: List of authentication method types registered by the user
|
||||
(e.g., 'fido2SecurityKey', 'microsoftAuthenticatorPush', 'mobilePhone').
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
on_premises_sync_enabled: bool
|
||||
directory_roles_ids: List[str] = []
|
||||
is_mfa_capable: bool = False
|
||||
account_enabled: bool = True
|
||||
authentication_methods: List[str] = []
|
||||
|
||||
|
||||
class InvitationsFrom(Enum):
|
||||
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered"
|
||||
|
||||
|
||||
def _make_policy(policy_id, excluded_users=None, excluded_groups=None, state=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=["All"],
|
||||
excluded_users=excluded_users or [],
|
||||
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=state or ConditionalAccessPolicyState.ENABLED,
|
||||
)
|
||||
|
||||
|
||||
class Test_entra_break_glass_account_fido2_security_key_registered:
|
||||
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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
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 == "Break Glass Accounts"
|
||||
assert result[0].resource_id == "breakGlassAccounts"
|
||||
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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: _make_policy(
|
||||
policy_id, state=ConditionalAccessPolicyState.DISABLED
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
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_break_glass_accounts_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
# 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_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "No break glass accounts identified" in result[0].status_extended
|
||||
|
||||
def test_break_glass_user_with_fido2(self):
|
||||
"""Test PASS when break glass account has FIDO2 registered."""
|
||||
policy_id_1 = str(uuid4())
|
||||
policy_id_2 = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id_1: _make_policy(policy_id_1, excluded_users=[bg_user_id]),
|
||||
policy_id_2: _make_policy(policy_id_2, excluded_users=[bg_user_id]),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
bg_user_id: User(
|
||||
id=bg_user_id,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=[
|
||||
"fido2SecurityKey",
|
||||
"microsoftAuthenticatorPush",
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "BreakGlass1" in result[0].status_extended
|
||||
assert "FIDO2 security key registered" in result[0].status_extended
|
||||
assert result[0].resource_name == "BreakGlass1"
|
||||
assert result[0].resource_id == bg_user_id
|
||||
|
||||
def test_break_glass_user_without_fido2(self):
|
||||
"""Test FAIL when break glass account lacks FIDO2."""
|
||||
policy_id_1 = str(uuid4())
|
||||
policy_id_2 = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id_1: _make_policy(policy_id_1, excluded_users=[bg_user_id]),
|
||||
policy_id_2: _make_policy(policy_id_2, excluded_users=[bg_user_id]),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
bg_user_id: User(
|
||||
id=bg_user_id,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=["mobilePhone", "email"],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "BreakGlass1" in result[0].status_extended
|
||||
assert (
|
||||
"does not have a FIDO2 security key registered"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_break_glass_user_with_empty_authentication_methods(self):
|
||||
"""Test FAIL when break glass account has no authentication methods."""
|
||||
policy_id = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: _make_policy(policy_id, excluded_users=[bg_user_id]),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
bg_user_id: User(
|
||||
id=bg_user_id,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=[],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
"does not have a FIDO2 security key registered"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
def test_break_glass_user_with_passkey_device_bound(self):
|
||||
"""Test MANUAL when break glass account has passKeyDeviceBound but not fido2SecurityKey."""
|
||||
policy_id_1 = str(uuid4())
|
||||
policy_id_2 = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id_1: _make_policy(policy_id_1, excluded_users=[bg_user_id]),
|
||||
policy_id_2: _make_policy(policy_id_2, excluded_users=[bg_user_id]),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
bg_user_id: User(
|
||||
id=bg_user_id,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=["passKeyDeviceBound"],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "BreakGlass1" in result[0].status_extended
|
||||
assert "device-bound passkey registered" in result[0].status_extended
|
||||
assert "cannot be confirmed" in result[0].status_extended
|
||||
|
||||
def test_multiple_break_glass_users_mixed_results(self):
|
||||
"""Test mixed results when one BG user has FIDO2 and another does not."""
|
||||
policy_id_1 = str(uuid4())
|
||||
policy_id_2 = str(uuid4())
|
||||
bg_user_id_1 = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id_1: _make_policy(
|
||||
policy_id_1, excluded_users=[bg_user_id_1, bg_user_id_2]
|
||||
),
|
||||
policy_id_2: _make_policy(
|
||||
policy_id_2, excluded_users=[bg_user_id_1, bg_user_id_2]
|
||||
),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
bg_user_id_1: User(
|
||||
id=bg_user_id_1,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=["fido2SecurityKey"],
|
||||
),
|
||||
bg_user_id_2: User(
|
||||
id=bg_user_id_2,
|
||||
name="BreakGlass2",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=["mobilePhone"],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
statuses = {r.resource_name: r.status for r in result}
|
||||
assert statuses["BreakGlass1"] == "PASS"
|
||||
assert statuses["BreakGlass2"] == "FAIL"
|
||||
|
||||
def test_break_glass_user_not_in_users_dict(self):
|
||||
"""Test that a user excluded from all policies but not in users dict is skipped."""
|
||||
policy_id = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id: _make_policy(policy_id, excluded_users=[bg_user_id]),
|
||||
}
|
||||
|
||||
# User not present in the users dict
|
||||
entra_client.users = {}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_disabled_policies_ignored(self):
|
||||
"""Test that disabled policies are not considered for identifying break glass accounts."""
|
||||
policy_id_enabled = str(uuid4())
|
||||
policy_id_disabled = str(uuid4())
|
||||
bg_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_break_glass_account_fido2_security_key_registered.entra_break_glass_account_fido2_security_key_registered import (
|
||||
entra_break_glass_account_fido2_security_key_registered,
|
||||
)
|
||||
|
||||
# User excluded from the enabled policy but not the disabled one
|
||||
entra_client.conditional_access_policies = {
|
||||
policy_id_enabled: _make_policy(
|
||||
policy_id_enabled, excluded_users=[bg_user_id]
|
||||
),
|
||||
policy_id_disabled: _make_policy(
|
||||
policy_id_disabled,
|
||||
excluded_users=[],
|
||||
state=ConditionalAccessPolicyState.DISABLED,
|
||||
),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
bg_user_id: User(
|
||||
id=bg_user_id,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=["fido2SecurityKey"],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_break_glass_account_fido2_security_key_registered()
|
||||
result = check.execute()
|
||||
|
||||
# Only 1 enabled policy and user is excluded from it → break glass user identified
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_name == "BreakGlass1"
|
||||
+70
-5
@@ -8,10 +8,12 @@ from prowler.providers.m365.services.entra.entra_service import (
|
||||
Conditions,
|
||||
GrantControlOperator,
|
||||
GrantControls,
|
||||
Group,
|
||||
PersistentBrowser,
|
||||
SessionControls,
|
||||
SignInFrequency,
|
||||
SignInFrequencyInterval,
|
||||
User,
|
||||
UsersConditions,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
@@ -334,12 +336,23 @@ class Test_entra_emergency_access_exclusion:
|
||||
),
|
||||
}
|
||||
|
||||
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 (
|
||||
"1 user(s) excluded as emergency access across all 2 enabled Conditional Access policies"
|
||||
"excluded from all 2 enabled Conditional Access policies"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
@@ -445,12 +458,23 @@ class Test_entra_emergency_access_exclusion:
|
||||
),
|
||||
}
|
||||
|
||||
entra_client.users = {}
|
||||
entra_client.groups = [
|
||||
Group(
|
||||
id=emergency_group_id,
|
||||
name="BreakGlassGroup",
|
||||
groupTypes=[],
|
||||
membershipRule=None,
|
||||
),
|
||||
]
|
||||
|
||||
check = entra_emergency_access_exclusion()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "BreakGlassGroup" in result[0].status_extended
|
||||
assert (
|
||||
"1 group(s) excluded as emergency access across all 2 enabled Conditional Access policies"
|
||||
"excluded from all 2 enabled Conditional Access policies"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
@@ -557,12 +581,31 @@ class Test_entra_emergency_access_exclusion:
|
||||
),
|
||||
}
|
||||
|
||||
entra_client.users = {
|
||||
emergency_user_id: User(
|
||||
id=emergency_user_id,
|
||||
name="BreakGlass1",
|
||||
on_premises_sync_enabled=False,
|
||||
authentication_methods=[],
|
||||
),
|
||||
}
|
||||
entra_client.groups = [
|
||||
Group(
|
||||
id=emergency_group_id,
|
||||
name="BreakGlassGroup",
|
||||
groupTypes=[],
|
||||
membershipRule=None,
|
||||
),
|
||||
]
|
||||
|
||||
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 "BreakGlassGroup" in result[0].status_extended
|
||||
assert (
|
||||
"1 user(s) and 1 group(s) excluded as emergency access across all 2 enabled Conditional Access policies"
|
||||
"excluded from all 2 enabled Conditional Access policies"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
@@ -668,14 +711,25 @@ class Test_entra_emergency_access_exclusion:
|
||||
),
|
||||
}
|
||||
|
||||
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 result[0].resource_name == "Conditional Access Policies"
|
||||
assert result[0].resource_id == "conditionalAccessPolicies"
|
||||
assert "BreakGlass1" in result[0].status_extended
|
||||
assert (
|
||||
"1 user(s) excluded as emergency access across all 1 enabled Conditional Access policies"
|
||||
"excluded from all 1 enabled Conditional Access policies"
|
||||
in result[0].status_extended
|
||||
)
|
||||
|
||||
@@ -780,12 +834,23 @@ class Test_entra_emergency_access_exclusion:
|
||||
),
|
||||
}
|
||||
|
||||
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 (
|
||||
"1 user(s) excluded as emergency access across all 2 enabled Conditional Access policies"
|
||||
"excluded from all 2 enabled Conditional Access policies"
|
||||
in result[0].status_extended
|
||||
)
|
||||
assert result[0].resource_name == "Conditional Access Policies"
|
||||
|
||||
@@ -486,8 +486,16 @@ class Test_Entra_Service:
|
||||
|
||||
registration_details_response = SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(id="user-1", is_mfa_capable=True),
|
||||
SimpleNamespace(id="user-6", is_mfa_capable=True),
|
||||
SimpleNamespace(
|
||||
id="user-1",
|
||||
is_mfa_capable=True,
|
||||
methods_registered=["fido2SecurityKey"],
|
||||
),
|
||||
SimpleNamespace(
|
||||
id="user-6",
|
||||
is_mfa_capable=True,
|
||||
methods_registered=["mobilePhone"],
|
||||
),
|
||||
],
|
||||
odata_next_link=None,
|
||||
)
|
||||
@@ -520,19 +528,31 @@ class Test_Entra_Service:
|
||||
assert users["user-6"].account_enabled is False
|
||||
assert users["user-1"].is_mfa_capable is True
|
||||
assert users["user-2"].is_mfa_capable is False
|
||||
assert users["user-1"].authentication_methods == ["fido2SecurityKey"]
|
||||
assert users["user-6"].authentication_methods == ["mobilePhone"]
|
||||
assert users["user-2"].authentication_methods == []
|
||||
|
||||
def test__get_user_registration_details_handles_pagination(self):
|
||||
entra_service = Entra.__new__(Entra)
|
||||
|
||||
registration_response_page_one = SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(id="user-1", is_mfa_capable=True),
|
||||
SimpleNamespace(
|
||||
id="user-1",
|
||||
is_mfa_capable=True,
|
||||
methods_registered=[
|
||||
"fido2SecurityKey",
|
||||
"microsoftAuthenticatorPush",
|
||||
],
|
||||
),
|
||||
],
|
||||
odata_next_link="next-link",
|
||||
)
|
||||
registration_response_page_two = SimpleNamespace(
|
||||
value=[
|
||||
SimpleNamespace(id="user-2", is_mfa_capable=False),
|
||||
SimpleNamespace(
|
||||
id="user-2", is_mfa_capable=False, methods_registered=[]
|
||||
),
|
||||
],
|
||||
odata_next_link=None,
|
||||
)
|
||||
@@ -557,7 +577,19 @@ class Test_Entra_Service:
|
||||
entra_service._get_user_registration_details()
|
||||
)
|
||||
|
||||
assert registration_details == {"user-1": True, "user-2": False}
|
||||
assert registration_details == {
|
||||
"user-1": {
|
||||
"is_mfa_capable": True,
|
||||
"authentication_methods": [
|
||||
"fido2SecurityKey",
|
||||
"microsoftAuthenticatorPush",
|
||||
],
|
||||
},
|
||||
"user-2": {
|
||||
"is_mfa_capable": False,
|
||||
"authentication_methods": [],
|
||||
},
|
||||
}
|
||||
registration_builder.get.assert_awaited()
|
||||
registration_builder.with_url.assert_called_once_with("next-link")
|
||||
registration_builder_next.get.assert_awaited()
|
||||
|
||||
Reference in New Issue
Block a user