feat(entra): add new check entra_admin_users_phishing_resistant_mfa_enabled (#7211)

Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Hugo Pereira Brito
2025-03-31 17:52:28 +02:00
committed by GitHub
parent b8ce09ec34
commit 7ba99f22cd
6 changed files with 454 additions and 0 deletions
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_admin_users_phishing_resistant_mfa_enabled",
"CheckTitle": "Ensure phishing-resistant MFA strength is required for all administrator accounts",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Conditional Access Policy",
"Description": "Ensure ",
"Risk": "Administrators using weaker MFA methods, such as SMS or push notifications, are vulnerable to phishing attacks and MFA fatigue attacks. Attackers can intercept codes or trick users into approving fraudulent authentication requests, leading to unauthorized access to critical systems.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center https://entra.microsoft.com. 2. Click expand Protection > Conditional Access select Policies. 3. Click New policy. Under Users include Select users and groups and check Directory roles. At a minimum, include the directory roles listed below in this section of the document. Under Target resources include All cloud apps and do not create any exclusions. Under Grant select Grant Access and check Require authentication strength and set Phishing-resistant MFA in the dropdown box. Click Select. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.",
"Terraform": ""
},
"Recommendation": {
"Text": "Require phishing-resistant MFA strength for all administrator accounts through Conditional Access policies. Enforce the use of FIDO2 security keys, Windows Hello for Business, or certificate-based authentication. Ensure administrators are pre-registered for these methods before enforcement to prevent lockouts. Maintain a break-glass account exempt from this policy for emergency access.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa#create-a-conditional-access-policy"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,67 @@
from prowler.lib.check.models import Check, CheckReportMicrosoft365
from prowler.providers.microsoft365.services.entra.entra_client import entra_client
from prowler.providers.microsoft365.services.entra.entra_service import (
AdminRoles,
AuthenticationStrength,
ConditionalAccessPolicyState,
)
class entra_admin_users_phishing_resistant_mfa_enabled(Check):
"""Check if Conditional Access policies require Phishing-resistant MFA strength for admin users."""
def execute(self) -> list[CheckReportMicrosoft365]:
"""Execute the check to ensure that Conditional Access policies require Phishing-resistant MFA strength for admin users.
Returns:
list[CheckReportMicrosoft365]: A list containing the results of the check.
"""
findings = []
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource={},
resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = "No Conditional Access Policy requires Phishing-resistant MFA strength for admin users."
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if not (
{role.value for role in AdminRoles}.issuperset(
policy.conditions.user_conditions.included_roles
)
):
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
or policy.conditions.application_conditions.excluded_applications != []
):
continue
if (
policy.grant_controls.authentication_strength is not None
and policy.grant_controls.authentication_strength
== AuthenticationStrength.PHISHING_RESISTANT_MFA
):
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name,
resource_id=policy.id,
)
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports Phishing-resistant MFA strength for admin users but does not require it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires Phishing-resistant MFA strength for admin users."
break
findings.append(report)
return findings
@@ -214,6 +214,15 @@ class Entra(Microsoft365Service):
getattr(policy.grant_controls, "operator", "AND")
)
),
authentication_strength=(
AuthenticationStrength(
policy.grant_controls.authentication_strength.display_name
)
if policy.grant_controls is not None
and policy.grant_controls.authentication_strength
is not None
else None
),
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
@@ -413,9 +422,16 @@ class GrantControlOperator(Enum):
OR = "OR"
class AuthenticationStrength(Enum):
MFA = "Multifactor authentication"
PASSWORDLESS_MFA = "Passwordless MFA"
PHISHING_RESISTANT_MFA = "Phishing-resistant MFA"
class GrantControls(BaseModel):
built_in_controls: List[ConditionalAccessGrantControl]
operator: GrantControlOperator
authentication_strength: Optional[AuthenticationStrength]
class ConditionalAccessPolicy(BaseModel):
@@ -0,0 +1,338 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.microsoft365.services.entra.entra_service import (
ApplicationsConditions,
AuthenticationStrength,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_entra_admin_users_phishing_resistant_mfa_enabled:
def test_entra_no_conditional_access_policies(self):
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
entra_admin_users_phishing_resistant_mfa_enabled,
)
entra_client.conditional_access_policies = {}
check = entra_admin_users_phishing_resistant_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires Phishing-resistant MFA strength for admin users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_entra_phishing_resistant_mfa_strength_disabled(self):
id = str(uuid4())
display_name = "Test"
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
entra_admin_users_phishing_resistant_mfa_enabled,
)
from prowler.providers.microsoft365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name=display_name,
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=[
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
"158c047a-c907-4556-b7ef-446551a6b5f7",
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
"29232cdf-9323-42fd-ade2-1d097af3e4de",
"62e90394-69f5-4237-9190-012177145e10",
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
"966707d0-3269-4727-9be2-8c3a10f19b9d",
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
"e8611ab8-c189-46e8-94e1-60213ab1f814",
"194ae4cb-b126-40b2-bd5b-6091b380977d",
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
"fe930be7-5e62-47db-91af-98c3a49a38b1",
],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.AND,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
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.DISABLED,
)
}
check = entra_admin_users_phishing_resistant_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires Phishing-resistant MFA strength for admin users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_entra_phishing_resistant_mfa_strength_enabled_for_reporting(self):
id = str(uuid4())
display_name = "Test"
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
entra_admin_users_phishing_resistant_mfa_enabled,
)
from prowler.providers.microsoft365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name=display_name,
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=[
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
"158c047a-c907-4556-b7ef-446551a6b5f7",
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
"29232cdf-9323-42fd-ade2-1d097af3e4de",
"62e90394-69f5-4237-9190-012177145e10",
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
"966707d0-3269-4727-9be2-8c3a10f19b9d",
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
"e8611ab8-c189-46e8-94e1-60213ab1f814",
"194ae4cb-b126-40b2-bd5b-6091b380977d",
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
"fe930be7-5e62-47db-91af-98c3a49a38b1",
],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.AND,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
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_FOR_REPORTING,
)
}
check = entra_admin_users_phishing_resistant_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' reports Phishing-resistant MFA strength for admin users but does not require it."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"
def test_entra_phishing_resistant_mfa_strength_enabled(self):
id = str(uuid4())
display_name = "Test"
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_users_phishing_resistant_mfa_enabled.entra_admin_users_phishing_resistant_mfa_enabled import (
entra_admin_users_phishing_resistant_mfa_enabled,
)
from prowler.providers.microsoft365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=id,
display_name=display_name,
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=[
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
"158c047a-c907-4556-b7ef-446551a6b5f7",
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
"29232cdf-9323-42fd-ade2-1d097af3e4de",
"62e90394-69f5-4237-9190-012177145e10",
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
"966707d0-3269-4727-9be2-8c3a10f19b9d",
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
"e8611ab8-c189-46e8-94e1-60213ab1f814",
"194ae4cb-b126-40b2-bd5b-6091b380977d",
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
"fe930be7-5e62-47db-91af-98c3a49a38b1",
],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.AND,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
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_admin_users_phishing_resistant_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' requires Phishing-resistant MFA strength for admin users."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"
@@ -4,6 +4,7 @@ from prowler.providers.microsoft365.models import Microsoft365IdentityInfo
from prowler.providers.microsoft365.services.entra.entra_service import (
AdminConsentPolicy,
ApplicationsConditions,
AuthenticationStrength,
AuthorizationPolicy,
AuthPolicyRoles,
ConditionalAccessGrantControl,
@@ -70,6 +71,7 @@ async def mock_entra_get_conditional_access_policies(_):
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
@@ -188,6 +190,7 @@ class Test_Entra_Service:
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.OR,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(