feat(entra): add new check entra_users_mfa_enabled (#7228)

This commit is contained in:
Hugo Pereira Brito
2025-03-31 17:54:52 +02:00
committed by Pepe Fagoaga
parent a11b338994
commit 71f8bcefa8
4 changed files with 395 additions and 0 deletions
@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_users_mfa_enabled",
"CheckTitle": "Ensure multifactor authentication is enabled for all users.",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "Conditional Access Policy",
"Description": "Ensure that multifactor authentication (MFA) is enabled for all users to enhance security and reduce the risk of unauthorized access.",
"Risk": "Without multifactor authentication (MFA), users are at a higher risk of account compromise due to credential theft, phishing, or brute-force attacks. A single-factor authentication method, such as passwords, is often insufficient to protect against modern cyber threats.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-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 All users (and do not exclude any user). Under Target resources include All cloud apps and do not create any exclusions. Under Grant select Grant Access and check Require multifactor authentication. Click Select at the bottom of the pane. 4. Under Enable policy set it to Report Only until the organization is ready to enable it. 5. Click Create.",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable multifactor authentication for all users in the Microsoft 365 tenant. Ensure users register at least one strong second-factor authentication method, such as Microsoft Authenticator, SMS codes, or phone calls. Educate users on the importance of MFA and provide clear instructions for enrollment to minimize disruptions.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,77 @@
from typing import List
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 (
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
)
class entra_users_mfa_enabled(Check):
"""
Ensure multifactor authentication is enabled for all users.
This check verifies that at least one Conditional Access Policy in Microsoft Entra, which is in an enabled state,
requires multifactor authentication for all users.
The check fails if no enabled policy is found that requires MFA for all users.
"""
def execute(self) -> List[CheckReportMicrosoft365]:
"""
Execute the admin MFA requirement check for all users.
Iterates over the Conditional Access Policies retrieved from the Entra client and generates a report
indicating whether MFA is enforced for users in all users.
Returns:
List[CheckReportMicrosoft365]: A list containing a single report with the result 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 enforces MFA for all users."
)
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if "All" not in policy.conditions.user_conditions.included_users:
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue
if (
ConditionalAccessGrantControl.MFA
in policy.grant_controls.built_in_controls
):
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource=entra_client.conditional_access_policies,
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 MFA requirement for all users but does not enforce it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces MFA for all users."
break
findings.append(report)
return findings
@@ -0,0 +1,288 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.microsoft365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicy,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_entra_users_mfa_enabled:
def test_no_conditional_access_policies(self):
"""No conditional access policies configured: expected FAIL."""
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_users_mfa_enabled.entra_users_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_users_mfa_enabled.entra_users_mfa_enabled import (
entra_users_mfa_enabled,
)
entra_client.conditional_access_policies = {}
check = entra_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for all users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_disabled(self):
"""Policy in DISABLED state: expected to be ignored and return FAIL."""
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_users_mfa_enabled.entra_users_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_users_mfa_enabled.entra_users_mfa_enabled import (
entra_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Disabled Policy",
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.DISABLED,
)
}
check = entra_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for all users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_mfa_enabled_for_report(self):
"""
Valid policy:
- State enabled for reporting only
- Applies to administrative roles via 'All' in included_users
- Application conditions include "All"
- MFA is configured in grant_controls
Expected FAIL due to is only for reporting.
"""
policy_id = str(uuid4())
display_name = "Invalid MFA Policy"
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_users_mfa_enabled.entra_users_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_users_mfa_enabled.entra_users_mfa_enabled import (
entra_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_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=[],
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_FOR_REPORTING,
)
}
check = entra_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
expected_status_extended = f"Conditional Access Policy '{display_name}' reports MFA requirement for all users but does not enforce it."
assert result[0].status_extended == expected_status_extended
assert result[0].resource == entra_client.conditional_access_policies
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_valid_through_roles(self):
"""
Valid policy:
- State enabled (ENABLED)
- Applies to administrative roles
- Application conditions include "All"
- MFA is configured in grant_controls
Expected PASS.
"""
policy_id = str(uuid4())
display_name = "Valid MFA Policy"
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_users_mfa_enabled.entra_users_mfa_enabled.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_users_mfa_enabled.entra_users_mfa_enabled import (
entra_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_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=[],
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_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
expected_status_extended = f"Conditional Access Policy '{display_name}' enforces MFA for all users."
assert result[0].status_extended == expected_status_extended
assert result[0].resource == entra_client.conditional_access_policies
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id