From 71f8bcefa80321213eb23866be5e487680a1b96e Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:54:52 +0200 Subject: [PATCH] feat(entra): add new check `entra_users_mfa_enabled` (#7228) --- .../entra/entra_users_mfa_enabled/__init__.py | 0 .../entra_users_mfa_enabled.metadata.json | 30 ++ .../entra_users_mfa_enabled.py | 77 +++++ .../entra_users_mfa_enabled_test.py | 288 ++++++++++++++++++ 4 files changed, 395 insertions(+) create mode 100644 prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/__init__.py create mode 100644 prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json create mode 100644 prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.py create mode 100644 tests/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled_test.py diff --git a/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/__init__.py b/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json b/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json new file mode 100644 index 0000000000..75f82197c5 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.metadata.json @@ -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": "" +} diff --git a/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.py b/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.py new file mode 100644 index 0000000000..8159c3ebc4 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled.py @@ -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 diff --git a/tests/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled_test.py b/tests/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled_test.py new file mode 100644 index 0000000000..7552cea160 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_users_mfa_enabled/entra_users_mfa_enabled_test.py @@ -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