mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(m365): add entra_authentication_method_sms_voice_disabled security check (#10212)
This commit is contained in:
committed by
GitHub
parent
012fd84cb0
commit
548a137046
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `entra_authentication_method_sms_voice_disabled` check for m365 provider [(#10212)](https://github.com/prowler-cloud/prowler/pull/10212)
|
||||
- `Google Workspace` provider support with Directory service including 1 security check [(#10022)](https://github.com/prowler-cloud/prowler/pull/10022)
|
||||
- `entra_conditional_access_policy_app_enforced_restrictions` check for M365 provider [(#10058)](https://github.com/prowler-cloud/prowler/pull/10058)
|
||||
- `entra_app_registration_no_unused_privileged_permissions` check for M365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_authentication_method_sms_voice_disabled",
|
||||
"CheckTitle": "SMS and Voice authentication methods are disabled in the tenant",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "SMS and Voice authentication methods should be disabled in the tenant's authentication methods policy. These methods are vulnerable to **SIM-swapping**, **interception**, and **social engineering** attacks, and are deprecated by NIST SP 800-63B as out-of-band authenticators.",
|
||||
"Risk": "Enabled SMS or Voice authentication allows attackers to bypass MFA through **SIM-swapping** or **SS7 protocol interception**, gaining unauthorized access to accounts. These methods lack cryptographic binding to the device, making them significantly weaker than phishing-resistant alternatives.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-phone-options",
|
||||
"https://pages.nist.gov/800-63-3/sp800-63b.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Protection** > **Authentication methods** > **Policies**\n3. Select **SMS** and set its status to **Disabled**, then click **Save**\n4. Select **Voice call** and set its status to **Disabled**, then click **Save**\n5. Ensure users have alternative phishing-resistant MFA methods configured (e.g., FIDO2, Microsoft Authenticator)",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Disable SMS and Voice authentication methods and adopt **phishing-resistant** alternatives such as FIDO2 security keys or Microsoft Authenticator. Use Authentication Strengths in Conditional Access policies to enforce only strong MFA methods across the tenant.",
|
||||
"Url": "https://hub.prowler.com/check/entra_authentication_method_sms_voice_disabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
|
||||
class entra_authentication_method_sms_voice_disabled(Check):
|
||||
"""
|
||||
Ensure that SMS and Voice authentication methods are disabled in Microsoft Entra.
|
||||
|
||||
This check verifies that the tenant's authentication methods policy has both SMS and
|
||||
Voice methods disabled, as they are vulnerable to SIM-swapping, interception, and
|
||||
social engineering attacks. NIST SP 800-63B deprecates SMS as an out-of-band
|
||||
authenticator.
|
||||
|
||||
- PASS: Both SMS and Voice authentication methods are disabled.
|
||||
- FAIL: SMS and/or Voice authentication methods are enabled.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""Execute the SMS and Voice authentication method check.
|
||||
|
||||
Evaluates the authentication method configurations from the Entra client
|
||||
and checks whether both SMS and Voice methods are disabled.
|
||||
|
||||
Returns:
|
||||
A list with a single report containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
configs = entra_client.authentication_method_configurations
|
||||
|
||||
sms_config = configs.get("Sms")
|
||||
voice_config = configs.get("Voice")
|
||||
|
||||
if sms_config or voice_config:
|
||||
sms_enabled = sms_config and sms_config.state == "enabled"
|
||||
voice_enabled = voice_config and voice_config.state == "enabled"
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=sms_config or voice_config,
|
||||
resource_name="SMS and Voice Authentication Methods",
|
||||
resource_id=entra_client.tenant_domain,
|
||||
)
|
||||
|
||||
if sms_enabled and voice_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"SMS and Voice authentication methods are enabled in the tenant."
|
||||
)
|
||||
elif sms_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"SMS authentication method is enabled in the tenant."
|
||||
)
|
||||
elif voice_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Voice authentication method is enabled in the tenant."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"SMS and Voice authentication methods are disabled in the tenant."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -35,6 +35,7 @@ class Entra(M365Service):
|
||||
users (dict): Dictionary of users.
|
||||
user_accounts_status (dict): Dictionary of user account statuses.
|
||||
oauth_apps (dict): Dictionary of OAuth applications from Defender XDR.
|
||||
authentication_method_configurations (dict): Dictionary of authentication method configurations.
|
||||
"""
|
||||
|
||||
def __init__(self, provider: M365Provider):
|
||||
@@ -81,6 +82,7 @@ class Entra(M365Service):
|
||||
self._get_default_app_management_policy(),
|
||||
self._get_oauth_apps(),
|
||||
self._get_directory_sync_settings(),
|
||||
self._get_authentication_method_configurations(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -93,6 +95,9 @@ class Entra(M365Service):
|
||||
self.default_app_management_policy = attributes[6]
|
||||
self.oauth_apps: Optional[Dict[str, OAuthApp]] = attributes[7]
|
||||
self.directory_sync_settings, self.directory_sync_error = attributes[8]
|
||||
self.authentication_method_configurations: Dict[
|
||||
str, AuthenticationMethodConfiguration
|
||||
] = attributes[9]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
if created_loop:
|
||||
@@ -756,6 +761,41 @@ OAuthAppInfo
|
||||
|
||||
return oauth_apps
|
||||
|
||||
async def _get_authentication_method_configurations(self):
|
||||
"""Retrieve authentication method configurations from Microsoft Entra.
|
||||
|
||||
Fetches the authentication methods policy and extracts the configuration
|
||||
state for each authentication method (e.g., SMS, Voice, FIDO2, etc.).
|
||||
|
||||
Returns:
|
||||
Dict[str, AuthenticationMethodConfiguration]: Dictionary of authentication
|
||||
method configurations keyed by method ID (e.g., 'sms', 'voice').
|
||||
"""
|
||||
logger.info("Entra - Getting authentication method configurations...")
|
||||
authentication_method_configurations = {}
|
||||
try:
|
||||
policy = await self.client.policies.authentication_methods_policy.get()
|
||||
for config in (
|
||||
getattr(policy, "authentication_method_configurations", []) or []
|
||||
):
|
||||
method_id = getattr(config, "id", "")
|
||||
if method_id:
|
||||
authentication_method_configurations[method_id] = (
|
||||
AuthenticationMethodConfiguration(
|
||||
id=method_id,
|
||||
state=(
|
||||
getattr(config, "state", None).value
|
||||
if getattr(config, "state", None)
|
||||
else "disabled"
|
||||
),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return authentication_method_configurations
|
||||
|
||||
|
||||
class ConditionalAccessPolicyState(Enum):
|
||||
ENABLED = "enabled"
|
||||
@@ -916,6 +956,21 @@ class DirectorySyncSettings(BaseModel):
|
||||
seamless_sso_enabled: bool = False
|
||||
|
||||
|
||||
class AuthenticationMethodConfiguration(BaseModel):
|
||||
"""Authentication method configuration from the authentication methods policy.
|
||||
|
||||
Represents the state of a specific authentication method (e.g., SMS, Voice,
|
||||
FIDO2) within the tenant's authentication methods policy.
|
||||
|
||||
Attributes:
|
||||
id: The authentication method identifier (e.g., 'sms', 'voice').
|
||||
state: The state of the authentication method ('enabled' or 'disabled').
|
||||
"""
|
||||
|
||||
id: str
|
||||
state: str = "disabled"
|
||||
|
||||
|
||||
class Group(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
AuthenticationMethodConfiguration,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_authentication_method_sms_voice_disabled:
|
||||
def test_no_configurations(self):
|
||||
"""
|
||||
Test when authentication_method_configurations is empty:
|
||||
The check should return an empty list of findings.
|
||||
"""
|
||||
entra_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled import (
|
||||
entra_authentication_method_sms_voice_disabled,
|
||||
)
|
||||
|
||||
entra_client.authentication_method_configurations = {}
|
||||
entra_client.tenant_domain = DOMAIN
|
||||
|
||||
check = entra_authentication_method_sms_voice_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_both_disabled(self):
|
||||
"""
|
||||
Test when both SMS and Voice are disabled:
|
||||
The check should return a single PASS finding.
|
||||
"""
|
||||
entra_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled import (
|
||||
entra_authentication_method_sms_voice_disabled,
|
||||
)
|
||||
|
||||
entra_client.authentication_method_configurations = {
|
||||
"Sms": AuthenticationMethodConfiguration(
|
||||
id="Sms",
|
||||
state="disabled",
|
||||
),
|
||||
"Voice": AuthenticationMethodConfiguration(
|
||||
id="Voice",
|
||||
state="disabled",
|
||||
),
|
||||
}
|
||||
entra_client.tenant_domain = DOMAIN
|
||||
|
||||
check = entra_authentication_method_sms_voice_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "SMS and Voice authentication methods are disabled in the tenant."
|
||||
)
|
||||
assert result[0].resource_id == DOMAIN
|
||||
assert result[0].resource_name == "SMS and Voice Authentication Methods"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_both_enabled(self):
|
||||
"""
|
||||
Test when both SMS and Voice are enabled:
|
||||
The check should return a single FAIL finding.
|
||||
"""
|
||||
entra_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled import (
|
||||
entra_authentication_method_sms_voice_disabled,
|
||||
)
|
||||
|
||||
entra_client.authentication_method_configurations = {
|
||||
"Sms": AuthenticationMethodConfiguration(
|
||||
id="Sms",
|
||||
state="enabled",
|
||||
),
|
||||
"Voice": AuthenticationMethodConfiguration(
|
||||
id="Voice",
|
||||
state="enabled",
|
||||
),
|
||||
}
|
||||
entra_client.tenant_domain = DOMAIN
|
||||
|
||||
check = entra_authentication_method_sms_voice_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "SMS and Voice authentication methods are enabled in the tenant."
|
||||
)
|
||||
assert result[0].resource_id == DOMAIN
|
||||
assert result[0].resource_name == "SMS and Voice Authentication Methods"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_sms_enabled_voice_disabled(self):
|
||||
"""
|
||||
Test when SMS is enabled and Voice is disabled:
|
||||
The check should return a single FAIL finding.
|
||||
"""
|
||||
entra_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled import (
|
||||
entra_authentication_method_sms_voice_disabled,
|
||||
)
|
||||
|
||||
entra_client.authentication_method_configurations = {
|
||||
"Sms": AuthenticationMethodConfiguration(
|
||||
id="Sms",
|
||||
state="enabled",
|
||||
),
|
||||
"Voice": AuthenticationMethodConfiguration(
|
||||
id="Voice",
|
||||
state="disabled",
|
||||
),
|
||||
}
|
||||
entra_client.tenant_domain = DOMAIN
|
||||
|
||||
check = entra_authentication_method_sms_voice_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "SMS authentication method is enabled in the tenant."
|
||||
)
|
||||
assert result[0].resource_id == DOMAIN
|
||||
assert result[0].resource_name == "SMS and Voice Authentication Methods"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_sms_disabled_voice_enabled(self):
|
||||
"""
|
||||
Test when SMS is disabled and Voice is enabled:
|
||||
The check should return a single FAIL finding.
|
||||
"""
|
||||
entra_client = mock.MagicMock
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_authentication_method_sms_voice_disabled.entra_authentication_method_sms_voice_disabled import (
|
||||
entra_authentication_method_sms_voice_disabled,
|
||||
)
|
||||
|
||||
entra_client.authentication_method_configurations = {
|
||||
"Sms": AuthenticationMethodConfiguration(
|
||||
id="Sms",
|
||||
state="disabled",
|
||||
),
|
||||
"Voice": AuthenticationMethodConfiguration(
|
||||
id="Voice",
|
||||
state="enabled",
|
||||
),
|
||||
}
|
||||
entra_client.tenant_domain = DOMAIN
|
||||
|
||||
check = entra_authentication_method_sms_voice_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Voice authentication method is enabled in the tenant."
|
||||
)
|
||||
assert result[0].resource_id == DOMAIN
|
||||
assert result[0].resource_name == "SMS and Voice Authentication Methods"
|
||||
assert result[0].location == "global"
|
||||
Reference in New Issue
Block a user