feat(entra): add new check entra_admin_mfa_enabled_for_administrative_roles (#7181)

Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Daniel Barranquero
2025-03-12 14:47:29 +01:00
committed by GitHub
parent 6564ec1ff5
commit bdb877009f
4 changed files with 649 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
{
"Provider": "microsoft365",
"CheckID": "entra_admin_mfa_enabled_for_administrative_roles",
"CheckTitle": "Ensure multifactor authentication is enabled for all users in administrative roles.",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Conditional Access Policy",
"Description": "Ensure that multifactor authentication (MFA) is enabled for all users in administrative roles to enhance security and reduce the risk of unauthorized access.",
"Risk": "Without MFA enabled for administrative roles, attackers could compromise privileged accounts with only a single authentication factor, increasing the risk of data breaches and unauthorized access to sensitive resources.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to Microsoft Entra admin center https://entra.microsoft.com. 2. Expand Protection > Conditional Access and select Policies. 3. Click 'New policy' and configure: Users: Select users and groups > Directory roles (include admin roles). Target resources: Include 'All cloud apps' with no exclusions. Grant: Select 'Grant Access' and check 'Require multifactor authentication'. 4. Set policy to 'Report Only' for testing before full enforcement. 5. Click 'Create'.",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable MFA for all users in administrative roles using a Conditional Access policy in Microsoft Entra.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-admin-mfa"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}

View File

@@ -0,0 +1,80 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportMicrosoft365
from prowler.providers.microsoft365.services.entra.entra_client import entra_client
from prowler.providers.microsoft365.services.entra.entra_service import (
AdminRoles,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
)
class entra_admin_mfa_enabled_for_administrative_roles(Check):
"""
Ensure multifactor authentication is enabled for all users in administrative roles.
This check verifies that at least one Conditional Access Policy in Microsoft Entra, which is in an enabled state,
applies to administrative roles and enforces multifactor authentication (MFA). Enforcing MFA for privileged accounts
is critical to reduce the risk of unauthorized access through compromised credentials.
The check fails if no enabled policy is found that requires MFA for any administrative role.
"""
def execute(self) -> List[CheckReportMicrosoft365]:
"""
Execute the admin MFA requirement check for administrative roles.
Iterates over the Conditional Access Policies retrieved from the Entra client and generates a report
indicating whether MFA is enforced for users in administrative roles.
Returns:
List[CheckReportMicrosoft365]: A list containing a single report with the result of the check.
"""
findings = []
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource={},
resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = "No Conditional Access Policy requiring MFA for administrative roles was found."
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if not ({admin_role.value for admin_role in AdminRoles}).issubset(
set(policy.conditions.user_conditions.included_roles)
):
if "All" not in policy.conditions.user_conditions.included_users:
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue
if (
ConditionalAccessGrantControl.MFA
in policy.grant_controls.built_in_controls
):
report = CheckReportMicrosoft365(
metadata=self.metadata(),
resource=entra_client.conditional_access_policies,
resource_name=policy.display_name,
resource_id=policy.id,
)
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces MFA for administrative roles."
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' only reports MFA for administrative roles but does not enforce it."
break
findings.append(report)
return findings

View File

@@ -0,0 +1,539 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.microsoft365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicy,
ConditionalAccessPolicyState,
Conditions,
GrantControls,
GrantControlOperator,
PersistentBrowser,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.microsoft365.microsoft365_fixtures import (
DOMAIN,
set_mocked_microsoft365_provider,
)
class Test_entra_admin_mfa_enabled_for_administrative_roles:
def test_no_conditional_access_policies(self):
"""No conditional access policies configured: 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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requiring MFA for administrative roles was found."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_disabled(self):
"""Policy in DISABLED state: expected to be ignored and return FAIL."""
policy_id = str(uuid4())
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Disabled Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"], excluded_applications=[]
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requiring MFA for administrative roles was found."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_missing_admin_roles(self):
"""
Enabled policy that does not apply to administrative roles:
Does not include 'All' in included_users nor administrative roles in included_roles.
Expected FAIL.
"""
policy_id = str(uuid4())
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="No Admin Roles Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"], excluded_applications=[]
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requiring MFA for administrative roles was found."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_missing_application_all(self):
"""
Enabled policy that includes administrative users (via 'All')
but does not have "All" in included_applications.
Expected FAIL.
"""
policy_id = str(uuid4())
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Missing Application All Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["MicrosoftAdminPortals"],
excluded_applications=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requiring MFA for administrative roles was found."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_valid(self):
"""
Valid policy:
- State enabled for reporting only
- Applies to administrative roles via 'All' in included_users
- Application conditions include "All"
- MFA is configured in grant_controls
Expected FAIL due to is only for reporting.
"""
policy_id = str(uuid4())
display_name = "Valid MFA Policy"
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"], excluded_applications=[]
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
expected_status_extended = f"Conditional Access Policy '{display_name}' only reports MFA for administrative roles but does not enforce it."
assert result[0].status_extended == expected_status_extended
assert result[0].resource == entra_client.conditional_access_policies
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_valid_through_roles(self):
"""
Valid policy:
- State enabled (ENABLED)
- Applies to administrative roles
- Application conditions include "All"
- MFA is configured in grant_controls
Expected PASS.
"""
policy_id = str(uuid4())
display_name = "Valid MFA Policy"
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"], excluded_applications=[]
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
"158c047a-c907-4556-b7ef-446551a6b5f7",
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
"29232cdf-9323-42fd-ade2-1d097af3e4de",
"62e90394-69f5-4237-9190-012177145e10",
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
"966707d0-3269-4727-9be2-8c3a10f19b9d",
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
"e8611ab8-c189-46e8-94e1-60213ab1f814",
"194ae4cb-b126-40b2-bd5b-6091b380977d",
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
"fe930be7-5e62-47db-91af-98c3a49a38b1",
],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
expected_status_extended = f"Conditional Access Policy '{display_name}' enforces MFA for administrative roles."
assert result[0].status_extended == expected_status_extended
assert result[0].resource == entra_client.conditional_access_policies
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_valid_one_missing_role(self):
"""
Valid policy:
- State enabled (ENABLED or ENABLED_FOR_REPORTING)
- Applies to administrative roles except one
- Application conditions include "All"
- MFA is configured in grant_controls
Expected FAIL.
"""
policy_id = str(uuid4())
display_name = "Valid MFA Policy"
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_microsoft365_provider(),
),
mock.patch(
"prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles.entra_client",
new=entra_client,
),
):
from prowler.providers.microsoft365.services.entra.entra_admin_mfa_enabled_for_administrative_roles.entra_admin_mfa_enabled_for_administrative_roles import (
entra_admin_mfa_enabled_for_administrative_roles,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"], excluded_applications=[]
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[
"9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3",
"c4e39bd9-1100-46d3-8c65-fb160da0071f",
"b0f54661-2d74-4c50-afa3-1ec803f12efe",
"158c047a-c907-4556-b7ef-446551a6b5f7",
"b1be1c3e-b65d-4f19-8427-f6fa0d97feb9",
"29232cdf-9323-42fd-ade2-1d097af3e4de",
"62e90394-69f5-4237-9190-012177145e10",
"f2ef992c-3afb-46b9-b7cf-a126ee74c451",
"729827e3-9c14-49f7-bb1b-9608f156bbb8",
"966707d0-3269-4727-9be2-8c3a10f19b9d",
"7be44c8a-adaf-4e2a-84d6-ab2649e08a13",
"e8611ab8-c189-46e8-94e1-60213ab1f814",
"194ae4cb-b126-40b2-bd5b-6091b380977d",
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_admin_mfa_enabled_for_administrative_roles()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
expected_status_extended = "No Conditional Access Policy requiring MFA for administrative roles was found."
assert result[0].status_extended == expected_status_extended
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"