feat(azure): add entra_authentication_methods_policy_strong_auth_enforced check (#11039)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
s1ns3nz0
2026-06-23 01:18:22 +09:00
committed by GitHub
parent bdd44a0dce
commit 29329f6203
8 changed files with 393 additions and 0 deletions
+1
View File
@@ -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)
@@ -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",
@@ -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"
@@ -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."
}
@@ -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
@@ -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] = []
@@ -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