mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(entra): add new check entra_users_mfa_capable (#7734)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
2a61610fec
commit
4e84507130
@@ -247,6 +247,7 @@ Prowler for M365 requires two types of permission scopes to be set (if you want
|
||||
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
|
||||
- `Sites.Read.All`: Required for SharePoint service.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
|
||||
- **Powershell Modules Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles:
|
||||
- `Global Reader` (recommended): this allows you to read all roles needed.
|
||||
|
||||
@@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Update the compliance list supported for each provider from docs. [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
|
||||
- Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
|
||||
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- Add new check `entra_users_mfa_capable`. [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734)
|
||||
- Add new check `admincenter_organization_customer_lockbox_enabled`. [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732)
|
||||
- Add new check `admincenter_external_calendar_sharing_disabled`. [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733)
|
||||
- Add GitHub provider. [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787)
|
||||
|
||||
@@ -1421,7 +1421,9 @@
|
||||
{
|
||||
"Id": "5.2.3.4",
|
||||
"Description": "Microsoft defines Multifactor authentication capable as being registered and enabled for a strong authentication method. The method must also be allowed by the authentication methods policy.Ensure all member users are `MFA capable`.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"entra_users_mfa_capable"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "5 Microsoft Entra admin center",
|
||||
|
||||
@@ -377,6 +377,19 @@ class Entra(M365Service):
|
||||
for member in members:
|
||||
user_roles_map.setdefault(member.id, []).append(role_template_id)
|
||||
|
||||
try:
|
||||
registration_details_list = (
|
||||
await self.client.reports.authentication_methods.user_registration_details.get()
|
||||
)
|
||||
registration_details = {
|
||||
detail.id: detail for detail in registration_details_list.value
|
||||
}
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
registration_details = {}
|
||||
|
||||
for user in users_list.value:
|
||||
users[user.id] = User(
|
||||
id=user.id,
|
||||
@@ -385,6 +398,11 @@ class Entra(M365Service):
|
||||
True if (user.on_premises_sync_enabled) else False
|
||||
),
|
||||
directory_roles_ids=user_roles_map.get(user.id, []),
|
||||
is_mfa_capable=(
|
||||
registration_details.get(user.id, {}).is_mfa_capable
|
||||
if registration_details.get(user.id, None) is not None
|
||||
else False
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -563,6 +581,7 @@ class User(BaseModel):
|
||||
name: str
|
||||
on_premises_sync_enabled: bool
|
||||
directory_roles_ids: List[str] = []
|
||||
is_mfa_capable: bool = False
|
||||
|
||||
|
||||
class InvitationsFrom(Enum):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_users_mfa_capable",
|
||||
"CheckTitle": "Ensure all users are MFA capable",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "critical",
|
||||
"ResourceType": "Conditional Access Policy",
|
||||
"Description": "Ensure all users are being registered and enabled for multifactor authentication.",
|
||||
"Risk": "Users who are not MFA capable are more vulnerable to account compromise, as they may rely solely on single-factor authentication (typically a password), which can be easily phished or cracked.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "Remediation steps will depend on the status of the personnel in question or configuration of Conditional Access policies. Administrators should review each user identified on a case-by-case basis.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Ensure all member users are MFA capable by registering and enabling a strong authentication method that complies with the organization's authentication policy. Regularly review user status to detect gaps in MFA deployment and correct misconfigurations.",
|
||||
"Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"e3"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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_users_mfa_capable(Check):
|
||||
"""
|
||||
Ensure all users are MFA capable.
|
||||
|
||||
This check verifies if users are MFA capable.
|
||||
|
||||
The check fails if any user is not MFA capable.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""
|
||||
Execute the admin MFA capable check for all users.
|
||||
|
||||
Iterates over the users retrieved from the Entra client and generates a report
|
||||
indicating if users are MFA capable.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: A list containing a single report with the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
for user in entra_client.users.values():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="Users",
|
||||
resource_id="users",
|
||||
)
|
||||
|
||||
if not user.is_mfa_capable:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"User {user.name} is not MFA capable."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"User {user.name} is MFA capable."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,139 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import User
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_users_mfa_capable:
|
||||
def test_user_not_mfa_capable(self):
|
||||
"""User is not MFA capable: 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_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import (
|
||||
entra_users_mfa_capable,
|
||||
)
|
||||
|
||||
user_id = str(uuid4())
|
||||
entra_client.users = {
|
||||
user_id: User(
|
||||
id=user_id,
|
||||
name="Test User",
|
||||
on_premises_sync_enabled=False,
|
||||
directory_roles_ids=[],
|
||||
is_mfa_capable=False,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_users_mfa_capable()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].status_extended == "User Test User is not MFA capable."
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Users"
|
||||
assert result[0].resource_id == "users"
|
||||
|
||||
def test_user_mfa_capable(self):
|
||||
"""User is MFA capable: expected PASS."""
|
||||
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_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import (
|
||||
entra_users_mfa_capable,
|
||||
)
|
||||
|
||||
user_id = str(uuid4())
|
||||
entra_client.users = {
|
||||
user_id: User(
|
||||
id=user_id,
|
||||
name="Test User",
|
||||
on_premises_sync_enabled=False,
|
||||
directory_roles_ids=[],
|
||||
is_mfa_capable=True,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_users_mfa_capable()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == "User Test User is MFA capable."
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Users"
|
||||
assert result[0].resource_id == "users"
|
||||
|
||||
def test_multiple_users(self):
|
||||
"""Multiple users with different MFA capabilities: expected mixed results."""
|
||||
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_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_users_mfa_capable.entra_users_mfa_capable import (
|
||||
entra_users_mfa_capable,
|
||||
)
|
||||
|
||||
user1_id = str(uuid4())
|
||||
user2_id = str(uuid4())
|
||||
entra_client.users = {
|
||||
user1_id: User(
|
||||
id=user1_id,
|
||||
name="Test User 1",
|
||||
on_premises_sync_enabled=False,
|
||||
directory_roles_ids=[],
|
||||
is_mfa_capable=True,
|
||||
),
|
||||
user2_id: User(
|
||||
id=user2_id,
|
||||
name="Test User 2",
|
||||
on_premises_sync_enabled=False,
|
||||
directory_roles_ids=[],
|
||||
is_mfa_capable=False,
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_users_mfa_capable()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
# First user (MFA capable)
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == "User Test User 1 is MFA capable."
|
||||
# Second user (not MFA capable)
|
||||
assert result[1].status == "FAIL"
|
||||
assert result[1].status_extended == "User Test User 2 is not MFA capable."
|
||||
@@ -121,18 +121,21 @@ async def mock_entra_get_users(_):
|
||||
name="User 1",
|
||||
directory_roles_ids=[AdminRoles.GLOBAL_ADMINISTRATOR.value],
|
||||
on_premises_sync_enabled=True,
|
||||
is_mfa_capable=True,
|
||||
),
|
||||
"user-2": User(
|
||||
id="user-2",
|
||||
name="User 2",
|
||||
directory_roles_ids=[AdminRoles.GLOBAL_ADMINISTRATOR.value],
|
||||
on_premises_sync_enabled=False,
|
||||
is_mfa_capable=False,
|
||||
),
|
||||
"user-3": User(
|
||||
id="user-3",
|
||||
name="User 3",
|
||||
directory_roles_ids=[AdminRoles.GLOBAL_ADMINISTRATOR.value],
|
||||
on_premises_sync_enabled=True,
|
||||
is_mfa_capable=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -278,12 +281,14 @@ class Test_Entra_Service:
|
||||
assert entra_client.users["user-1"].directory_roles_ids == [
|
||||
AdminRoles.GLOBAL_ADMINISTRATOR.value
|
||||
]
|
||||
assert entra_client.users["user-1"].is_mfa_capable
|
||||
assert entra_client.users["user-1"].on_premises_sync_enabled
|
||||
assert entra_client.users["user-2"].id == "user-2"
|
||||
assert entra_client.users["user-2"].name == "User 2"
|
||||
assert entra_client.users["user-2"].directory_roles_ids == [
|
||||
AdminRoles.GLOBAL_ADMINISTRATOR.value
|
||||
]
|
||||
assert not entra_client.users["user-2"].is_mfa_capable
|
||||
assert not entra_client.users["user-2"].on_premises_sync_enabled
|
||||
assert entra_client.users["user-3"].id == "user-3"
|
||||
assert entra_client.users["user-3"].name == "User 3"
|
||||
@@ -291,3 +296,4 @@ class Test_Entra_Service:
|
||||
AdminRoles.GLOBAL_ADMINISTRATOR.value
|
||||
]
|
||||
assert entra_client.users["user-3"].on_premises_sync_enabled
|
||||
assert not entra_client.users["user-3"].is_mfa_capable
|
||||
|
||||
Reference in New Issue
Block a user