From 29329f6203a4ca5e1c495bbcded387cc4aef85de Mon Sep 17 00:00:00 2001 From: s1ns3nz0 Date: Tue, 23 Jun 2026 01:18:22 +0900 Subject: [PATCH] feat(azure): add entra_authentication_methods_policy_strong_auth_enforced check (#11039) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 1 + .../compliance/azure/iso27001_2022_azure.json | 1 + .../azure/secnumcloud_3.2_azure.json | 1 + .../__init__.py | 0 ..._policy_strong_auth_enforced.metadata.json | 37 ++++ ...ion_methods_policy_strong_auth_enforced.py | 63 ++++++ .../azure/services/entra/entra_service.py | 85 ++++++++ ...ethods_policy_strong_auth_enforced_test.py | 205 ++++++++++++++++++ 8 files changed, 393 insertions(+) create mode 100644 prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/__init__.py create mode 100644 prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.metadata.json create mode 100644 prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.py create mode 100644 tests/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b2927bdd2c..05c2a87094 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `network_subnet_nsg_associated` check for Azure provider, verifying virtual network subnets have a network security group associated to enforce traffic filtering [(#11043)](https://github.com/prowler-cloud/prowler/pull/11043) - `network_vnet_ddos_protection_enabled` check for Azure provider, verifying virtual networks have Azure DDoS Network Protection enabled [(#11044)](https://github.com/prowler-cloud/prowler/pull/11044) - `entra_app_registration_credential_not_expired` check for Azure provider, verifying Entra ID app registration secrets and certificates are not expired, expiring within 30 days, or without an expiration date [(#11038)](https://github.com/prowler-cloud/prowler/pull/11038) +- `entra_authentication_methods_policy_strong_auth_enforced` check for Azure provider, verifying the Entra ID authentication methods policy enforces MFA registration and enables at least one strong method (Microsoft Authenticator, FIDO2, or X.509 certificate) [(#11039)](https://github.com/prowler-cloud/prowler/pull/11039) - `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027) - Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398) - Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602) diff --git a/prowler/compliance/azure/iso27001_2022_azure.json b/prowler/compliance/azure/iso27001_2022_azure.json index 04da31be78..ab17f7057c 100644 --- a/prowler/compliance/azure/iso27001_2022_azure.json +++ b/prowler/compliance/azure/iso27001_2022_azure.json @@ -1103,6 +1103,7 @@ } ], "Checks": [ + "entra_authentication_methods_policy_strong_auth_enforced", "entra_conditional_access_policy_require_mfa_for_management_app", "entra_non_privileged_user_has_mfa entra_privileged_user_has_mfa", "entra_user_with_vm_access_has_mfa", diff --git a/prowler/compliance/azure/secnumcloud_3.2_azure.json b/prowler/compliance/azure/secnumcloud_3.2_azure.json index ef6141d446..aedf2133d4 100644 --- a/prowler/compliance/azure/secnumcloud_3.2_azure.json +++ b/prowler/compliance/azure/secnumcloud_3.2_azure.json @@ -339,6 +339,7 @@ } ], "Checks": [ + "entra_authentication_methods_policy_strong_auth_enforced", "entra_non_privileged_user_has_mfa", "entra_privileged_user_has_mfa", "entra_security_defaults_enabled" diff --git a/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/__init__.py b/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.metadata.json b/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.metadata.json new file mode 100644 index 0000000000..11dfa6e85b --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "azure", + "CheckID": "entra_authentication_methods_policy_strong_auth_enforced", + "CheckTitle": "Strong authentication methods are enabled with registration enforcement", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "**Microsoft Entra ID** authentication methods policy is evaluated for **strong authentication enforcement**. The check verifies that at least one strong method (Microsoft Authenticator, FIDO2, or X.509 Certificate) is enabled and that the MFA registration campaign is active to prompt users to enroll.", + "Risk": "Without strong authentication methods, the tenant relies on **passwords alone** or weak factors like SMS/voice. Password-only authentication enables **credential stuffing**, **phishing**, and **brute force** attacks. Without registration enforcement, users may never enroll in MFA even when it is available.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods-manage", + "https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-nudge-authenticator-app" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Sign in to Microsoft Entra admin center\n2. Go to Protection > Authentication methods > Policies\n3. Enable Microsoft Authenticator and/or FIDO2 security keys\n4. Go to Protection > Authentication methods > Registration campaign\n5. Set State to Enabled\n6. Configure included users/groups\n7. Consider disabling weak methods (SMS, Voice) after users migrate", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **Microsoft Authenticator** and **FIDO2 security keys** as authentication methods. Activate the **MFA registration campaign** to prompt users to register. Disable weak methods (SMS, voice) after migration. Use **Conditional Access** to require strong authentication for all users.", + "Url": "https://hub.prowler.com/check/entra_authentication_methods_policy_strong_auth_enforced" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check reports a single finding per tenant and passes only when both the MFA registration campaign is enabled and at least one strong method is enabled. Microsoft recommends phishing-resistant methods (FIDO2, certificate-based) over app-based push notifications." +} diff --git a/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.py b/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.py new file mode 100644 index 0000000000..efdbcfa6a0 --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced.py @@ -0,0 +1,63 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.entra.entra_client import entra_client + +# Methods considered strong (phishing-resistant or app-based MFA) +STRONG_METHODS = {"microsoftAuthenticator", "fido2", "x509Certificate"} + + +class entra_authentication_methods_policy_strong_auth_enforced(Check): + """ + Ensure the Entra ID authentication methods policy enforces strong authentication. + + This check evaluates the tenant authentication methods policy and reports a single finding per tenant. Strong authentication is considered enforced only when BOTH conditions hold: + 1. The MFA registration campaign is enabled (users are prompted to register methods). + 2. At least one strong, phishing-resistant or app-based method (Microsoft Authenticator, FIDO2, or X.509 certificate) is enabled. + + - PASS: Both conditions hold. + - FAIL: One or both conditions are missing; the status extended names exactly what is missing. + """ + + def execute(self) -> Check_Report_Azure: + findings = [] + + for tenant_domain, policy in entra_client.authentication_methods_policy.items(): + if policy is None: + continue + + report = Check_Report_Azure(metadata=self.metadata(), resource=policy) + report.subscription = f"Tenant: {tenant_domain}" + report.resource_name = "Authentication Methods Policy" + report.resource_id = policy.id + + registration_enabled = policy.registration_enforcement_state == "enabled" + enabled_strong = [ + config.method_name + for config in policy.method_configurations + if config.state == "enabled" and config.method_name in STRONG_METHODS + ] + + if registration_enabled and enabled_strong: + report.status = "PASS" + report.status_extended = ( + f"Strong authentication is enforced for tenant {tenant_domain}: " + f"the MFA registration campaign is enabled and strong methods are " + f"enabled ({', '.join(enabled_strong)})." + ) + else: + issues = [] + if not registration_enabled: + issues.append("the MFA registration campaign is not enabled") + if not enabled_strong: + issues.append( + "no strong authentication methods (Microsoft Authenticator, " + "FIDO2, or X.509 Certificate) are enabled" + ) + report.status = "FAIL" + report.status_extended = ( + f"Strong authentication is not enforced for tenant " + f"{tenant_domain}: {'; '.join(issues)}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/entra/entra_service.py b/prowler/providers/azure/services/entra/entra_service.py index cda5070ec3..d77880b425 100644 --- a/prowler/providers/azure/services/entra/entra_service.py +++ b/prowler/providers/azure/services/entra/entra_service.py @@ -51,6 +51,7 @@ class Entra(AzureService): self._get_directory_roles(), self._get_conditional_access_policy(), self._get_app_registrations(), + self._get_authentication_methods_policy(), ) ) @@ -61,6 +62,7 @@ class Entra(AzureService): self.directory_roles = attributes[4] self.conditional_access_policy = attributes[5] self.app_registrations = attributes[6] + self.authentication_methods_policy = attributes[7] if created_loop: asyncio.set_event_loop(None) @@ -477,6 +479,77 @@ class Entra(AzureService): return app_registrations + async def _get_authentication_methods_policy(self): + logger.info("Entra - Getting authentication methods policy...") + auth_methods_policy = {} + try: + for tenant, client in self.clients.items(): + policy_response = ( + await client.policies.authentication_methods_policy.get() + ) + + if not policy_response: + auth_methods_policy[tenant] = None + continue + + # Parse registration enforcement + reg_enforcement = getattr( + policy_response, "registration_enforcement", None + ) + reg_campaign = ( + getattr( + reg_enforcement, + "authentication_methods_registration_campaign", + None, + ) + if reg_enforcement + else None + ) + registration_enforcement_state = ( + getattr(reg_campaign, "state", None) if reg_campaign else None + ) + + # Parse authentication method configurations + method_configs = [] + for config in ( + getattr( + policy_response, + "authentication_method_configurations", + [], + ) + or [] + ): + odata_type = getattr(config, "odata_type", "") or "" + # Extract method name from odata_type + # e.g. "#microsoft.graph.microsoftAuthenticatorAuthenticationMethodConfiguration" + method_name = ( + odata_type.split(".")[-1].replace( + "AuthenticationMethodConfiguration", "" + ) + if odata_type + else getattr(config, "id", "unknown") + ) + method_configs.append( + AuthMethodConfig( + id=getattr(config, "id", "") or "", + method_name=method_name, + state=getattr(config, "state", "disabled") or "disabled", + ) + ) + + auth_methods_policy[tenant] = AuthMethodsPolicy( + id=getattr(policy_response, "id", "") or "", + registration_enforcement_state=registration_enforcement_state, + method_configurations=method_configs, + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return auth_methods_policy + class User(BaseModel): id: str @@ -554,3 +627,15 @@ class AppRegistration(BaseModel): id: str name: str credentials: List[AppCredential] = [] + + +class AuthMethodConfig(BaseModel): + id: str = "" + method_name: str + state: str = "disabled" # "enabled" or "disabled" + + +class AuthMethodsPolicy(BaseModel): + id: str = "" + registration_enforcement_state: Optional[str] = None # "enabled" or "disabled" + method_configurations: List[AuthMethodConfig] = [] diff --git a/tests/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced_test.py b/tests/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced_test.py new file mode 100644 index 0000000000..e3102bf3e9 --- /dev/null +++ b/tests/providers/azure/services/entra/entra_authentication_methods_policy_strong_auth_enforced/entra_authentication_methods_policy_strong_auth_enforced_test.py @@ -0,0 +1,205 @@ +from unittest import mock + +from tests.providers.azure.azure_fixtures import DOMAIN, set_mocked_azure_provider + +CHECK_MODULE = ( + "prowler.providers.azure.services.entra." + "entra_authentication_methods_policy_strong_auth_enforced." + "entra_authentication_methods_policy_strong_auth_enforced" +) + + +class Test_entra_authentication_methods_policy_strong_auth_enforced: + def test_entra_no_tenants(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch(f"{CHECK_MODULE}.entra_client", new=entra_client), + ): + from prowler.providers.azure.services.entra.entra_authentication_methods_policy_strong_auth_enforced.entra_authentication_methods_policy_strong_auth_enforced import ( + entra_authentication_methods_policy_strong_auth_enforced, + ) + + entra_client.authentication_methods_policy = {} + + check = entra_authentication_methods_policy_strong_auth_enforced() + result = check.execute() + assert len(result) == 0 + + def test_entra_policy_none(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch(f"{CHECK_MODULE}.entra_client", new=entra_client), + ): + from prowler.providers.azure.services.entra.entra_authentication_methods_policy_strong_auth_enforced.entra_authentication_methods_policy_strong_auth_enforced import ( + entra_authentication_methods_policy_strong_auth_enforced, + ) + + entra_client.authentication_methods_policy = {DOMAIN: None} + + check = entra_authentication_methods_policy_strong_auth_enforced() + result = check.execute() + assert len(result) == 0 + + def test_entra_registration_enabled_strong_method_enabled(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch(f"{CHECK_MODULE}.entra_client", new=entra_client), + ): + from prowler.providers.azure.services.entra.entra_authentication_methods_policy_strong_auth_enforced.entra_authentication_methods_policy_strong_auth_enforced import ( + entra_authentication_methods_policy_strong_auth_enforced, + ) + from prowler.providers.azure.services.entra.entra_service import ( + AuthMethodConfig, + AuthMethodsPolicy, + ) + + policy = AuthMethodsPolicy( + id="authMethodsPolicy", + registration_enforcement_state="enabled", + method_configurations=[ + AuthMethodConfig( + id="MicrosoftAuthenticator", + method_name="microsoftAuthenticator", + state="enabled", + ), + AuthMethodConfig(id="Sms", method_name="sms", state="enabled"), + ], + ) + entra_client.authentication_methods_policy = {DOMAIN: policy} + + check = entra_authentication_methods_policy_strong_auth_enforced() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == "Authentication Methods Policy" + assert result[0].resource_id == "authMethodsPolicy" + assert "is enforced" in result[0].status_extended + assert "microsoftAuthenticator" in result[0].status_extended + + def test_entra_registration_disabled_no_strong_methods(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch(f"{CHECK_MODULE}.entra_client", new=entra_client), + ): + from prowler.providers.azure.services.entra.entra_authentication_methods_policy_strong_auth_enforced.entra_authentication_methods_policy_strong_auth_enforced import ( + entra_authentication_methods_policy_strong_auth_enforced, + ) + from prowler.providers.azure.services.entra.entra_service import ( + AuthMethodConfig, + AuthMethodsPolicy, + ) + + policy = AuthMethodsPolicy( + id="authMethodsPolicy", + registration_enforcement_state="disabled", + method_configurations=[ + AuthMethodConfig(id="Sms", method_name="sms", state="enabled"), + AuthMethodConfig(id="Voice", method_name="voice", state="enabled"), + ], + ) + entra_client.authentication_methods_policy = {DOMAIN: policy} + + check = entra_authentication_methods_policy_strong_auth_enforced() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "is not enforced" in result[0].status_extended + assert "registration campaign is not enabled" in result[0].status_extended + assert "no strong authentication methods" in result[0].status_extended + + def test_entra_registration_disabled_strong_method_enabled(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch(f"{CHECK_MODULE}.entra_client", new=entra_client), + ): + from prowler.providers.azure.services.entra.entra_authentication_methods_policy_strong_auth_enforced.entra_authentication_methods_policy_strong_auth_enforced import ( + entra_authentication_methods_policy_strong_auth_enforced, + ) + from prowler.providers.azure.services.entra.entra_service import ( + AuthMethodConfig, + AuthMethodsPolicy, + ) + + policy = AuthMethodsPolicy( + id="authMethodsPolicy", + registration_enforcement_state="disabled", + method_configurations=[ + AuthMethodConfig(id="Fido2", method_name="fido2", state="enabled"), + ], + ) + entra_client.authentication_methods_policy = {DOMAIN: policy} + + check = entra_authentication_methods_policy_strong_auth_enforced() + result = check.execute() + # Strong method present but registration campaign off -> not enforced + assert len(result) == 1 + assert result[0].status == "FAIL" + assert "registration campaign is not enabled" in result[0].status_extended + assert "no strong authentication methods" not in result[0].status_extended + + def test_entra_multiple_strong_methods(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch(f"{CHECK_MODULE}.entra_client", new=entra_client), + ): + from prowler.providers.azure.services.entra.entra_authentication_methods_policy_strong_auth_enforced.entra_authentication_methods_policy_strong_auth_enforced import ( + entra_authentication_methods_policy_strong_auth_enforced, + ) + from prowler.providers.azure.services.entra.entra_service import ( + AuthMethodConfig, + AuthMethodsPolicy, + ) + + policy = AuthMethodsPolicy( + id="authMethodsPolicy", + registration_enforcement_state="enabled", + method_configurations=[ + AuthMethodConfig( + id="MicrosoftAuthenticator", + method_name="microsoftAuthenticator", + state="enabled", + ), + AuthMethodConfig(id="Fido2", method_name="fido2", state="enabled"), + AuthMethodConfig( + id="x509", method_name="x509Certificate", state="enabled" + ), + ], + ) + entra_client.authentication_methods_policy = {DOMAIN: policy} + + check = entra_authentication_methods_policy_strong_auth_enforced() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert "microsoftAuthenticator" in result[0].status_extended + assert "fido2" in result[0].status_extended