fix(m365): only evaluate enabled users in entra_users_mfa_capable (#8544)

This commit is contained in:
Hugo Pereira Brito
2025-08-20 16:45:00 +02:00
committed by GitHub
parent 46c24055ee
commit ad3d4536fb
5 changed files with 131 additions and 13 deletions
+1
View File
@@ -28,6 +28,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533)
- GitHub App authentication for GitHub provider [(#8529)](https://github.com/prowler-cloud/prowler/pull/8529)
- List all accessible organizations in GitHub provider [(#8535)](https://github.com/prowler-cloud/prowler/pull/8535)
- Only evaluate enabled accounts in `entra_users_mfa_capable` check [(#8544)](https://github.com/prowler-cloud/prowler/pull/8544)
---
@@ -982,6 +982,20 @@ class M365PowerShell(PowerShellSession):
"""
return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True)
def get_user_account_status(self) -> dict:
"""
Get User Account Status.
Retrieves the current user account status settings for Exchange Online.
Returns:
dict: User account status settings in JSON format.
"""
return self.execute(
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json",
json_parse=True,
)
# This function is used to install the required M365 PowerShell modules in Docker containers
def initialize_m365_powershell_modules():
@@ -14,7 +14,10 @@ from prowler.providers.m365.m365_provider import M365Provider
class Entra(M365Service):
def __init__(self, provider: M365Provider):
super().__init__(provider)
if self.powershell:
self.powershell.connect_exchange_online()
self.user_accounts_status = self.powershell.get_user_account_status()
self.powershell.close()
loop = get_event_loop()
@@ -36,6 +39,7 @@ class Entra(M365Service):
self.groups = attributes[3]
self.organizations = attributes[4]
self.users = attributes[5]
self.user_accounts_status = {}
async def _get_authorization_policy(self):
logger.info("Entra - Getting authorization policy...")
@@ -405,6 +409,9 @@ class Entra(M365Service):
if registration_details.get(user.id, None) is not None
else False
),
account_enabled=not self.user_accounts_status.get(user.id, {}).get(
"AccountDisabled", False
),
)
except Exception as error:
logger.error(
@@ -585,6 +592,7 @@ class User(BaseModel):
on_premises_sync_enabled: bool
directory_roles_ids: List[str] = []
is_mfa_capable: bool = False
account_enabled: bool = True
class InvitationsFrom(Enum):
@@ -26,20 +26,21 @@ class entra_users_mfa_capable(Check):
findings = []
for user in entra_client.users.values():
report = CheckReportM365(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.id,
)
if user.account_enabled:
report = CheckReportM365(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.id,
)
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."
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)
findings.append(report)
return findings
@@ -34,6 +34,7 @@ class Test_entra_users_mfa_capable:
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=False,
account_enabled=True,
)
}
@@ -75,6 +76,7 @@ class Test_entra_users_mfa_capable:
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=True,
account_enabled=True,
)
}
@@ -117,6 +119,7 @@ class Test_entra_users_mfa_capable:
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=True,
account_enabled=True,
),
user2_id: User(
id=user2_id,
@@ -124,6 +127,7 @@ class Test_entra_users_mfa_capable:
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=False,
account_enabled=True,
),
}
@@ -143,3 +147,93 @@ class Test_entra_users_mfa_capable:
assert result[1].resource == entra_client.users[user2_id]
assert result[1].resource_name == "Test User 2"
assert result[1].resource_id == user2_id
def test_disabled_user_not_checked(self):
"""Disabled user should not be checked: expected no 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,
)
user_id = str(uuid4())
entra_client.users = {
user_id: User(
id=user_id,
name="Disabled User",
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=False,
account_enabled=False, # Disabled user
)
}
check = entra_users_mfa_capable()
result = check.execute()
# No results should be returned for disabled users
assert len(result) == 0
def test_mixed_enabled_disabled_users(self):
"""Mix of enabled and disabled users: only enabled users should be checked."""
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,
)
enabled_user_id = str(uuid4())
disabled_user_id = str(uuid4())
entra_client.users = {
enabled_user_id: User(
id=enabled_user_id,
name="Enabled User",
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=True,
account_enabled=True, # Enabled user
),
disabled_user_id: User(
id=disabled_user_id,
name="Disabled User",
on_premises_sync_enabled=False,
directory_roles_ids=[],
is_mfa_capable=False,
account_enabled=False, # Disabled user
),
}
check = entra_users_mfa_capable()
result = check.execute()
# Only the enabled user should be checked
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == "User Enabled User is MFA capable."
assert result[0].resource == entra_client.users[enabled_user_id]
assert result[0].resource_name == "Enabled User"
assert result[0].resource_id == enabled_user_id