mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat(entra): add new check entra_users_mfa_enabled (#7228)
This commit is contained in:
committed by
Pepe Fagoaga
parent
a11b338994
commit
71f8bcefa8
+30
@@ -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": ""
|
||||
}
|
||||
+77
@@ -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
|
||||
+288
@@ -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
|
||||
Reference in New Issue
Block a user