mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
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:
@@ -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"
|
||||
|
||||
+37
@@ -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."
|
||||
}
|
||||
+63
@@ -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] = []
|
||||
|
||||
+205
@@ -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
|
||||
Reference in New Issue
Block a user