feat(azure): add 'entra_conditional_access_policy_require_mfa_for_admin_portals' check and update compliance (#10330)

This commit is contained in:
Daniel Barranquero
2026-03-16 12:14:58 +01:00
committed by GitHub
parent 2b7b2623c5
commit 361f8548bf
11 changed files with 379 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `entra_conditional_access_policy_device_code_flow_blocked` check for M365 provider [(#10218)](https://github.com/prowler-cloud/prowler/pull/10218)
- CheckMetadata Pydantic validators [(#8584)](https://github.com/prowler-cloud/prowler/pull/8583)
- `entra_conditional_access_policy_require_mfa_for_admin_portals` check for Azure provider and update CIS compliance [(#10330)](https://github.com/prowler-cloud/prowler/pull/10330)
### 🔄 Changed

View File

@@ -752,7 +752,7 @@
"Id": "2.2.8",
"Description": "Ensure Multi-factor Authentication is Required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{

View File

@@ -1036,7 +1036,7 @@
"Id": "6.2.7",
"Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{

View File

@@ -472,7 +472,7 @@
"Id": "5.2.7",
"Description": "Ensure that multifactor authentication is required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{

View File

@@ -63,7 +63,7 @@
"Id": "1.1.4",
"Description": "Ensure Multi-factor Authentication is Required to access Microsoft Admin Portals",
"Checks": [
"defender_ensure_defender_for_server_is_on"
"entra_conditional_access_policy_require_mfa_for_admin_portals"
],
"Attributes": [
{

View File

@@ -1,7 +1,10 @@
from uuid import UUID
# Service management API
# Conditional Access target resource identifiers
# (Graph API includeApplications values)
WINDOWS_AZURE_SERVICE_MANAGEMENT_API = "797f4846-ba00-4fd7-ba43-dac1f8f63013"
# Named app group — Graph API returns this literal string, not a GUID
MICROSOFT_ADMIN_PORTALS = "MicrosoftAdminPortals"
# Authorization policy roles
GUEST_USER_ACCESS_NO_RESTRICTICTED = UUID("a0b1b346-4d3e-4e8b-98f8-753987be4970")

View File

@@ -0,0 +1,38 @@
{
"Provider": "azure",
"CheckID": "entra_conditional_access_policy_require_mfa_for_admin_portals",
"CheckTitle": "Conditional Access policy requires MFA for Microsoft Admin Portals",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "**Microsoft Entra Conditional Access** is evaluated for a policy that requires **multifactor authentication** when accessing **Microsoft Admin Portals** (Microsoft 365 Admin Center, Microsoft Entra Admin Center, Microsoft Exchange Admin Center, etc.). The check confirms an enabled policy targets **All users**, includes the Microsoft Admin Portals app, and enforces an **MFA** grant control.",
"Risk": "Without **MFA** on admin portals, attackers with stolen credentials can access **Microsoft 365 Admin Center**, **Entra Admin Center**, or **Exchange Admin Center** to modify tenant settings, escalate privileges, and exfiltrate data. This directly impacts **confidentiality**, **integrity**, and **availability** of all services managed through those portals.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/how-to-policy-mfa-admin-portals",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Protection > Conditional Access > Policies\n3. Click + New policy and enter a name\n4. Under Users > Include, select All users\n5. Under Exclude, check Users and groups and select break-glass / non-interactive service accounts\n6. Under Target resources > Include, click Select apps, then select Microsoft Admin Portals\n7. Under Grant, select Grant access and check Require multifactor authentication\n8. Set Enable policy to Report-only, click Create\n9. After testing, change Enable policy from Report-only to On",
"Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"<example_resource_name>\" {\n display_name = \"<example_resource_name>\"\n state = \"enabled\"\n\n conditions {\n users {\n included_users = [\"All\"]\n excluded_users = [\"<break_glass_account_id>\"]\n }\n applications {\n included_applications = [\"MicrosoftAdminPortals\"] # Critical: Microsoft Admin Portals\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: requires MFA\n }\n}\n```"
},
"Recommendation": {
"Text": "Create a **Conditional Access policy** that requires **MFA** for the **Microsoft Admin Portals** app targeting **All users**. Exclude only **break-glass** emergency accounts and non-interactive service principals. Test in **Report-only** mode before enforcing. Prefer **phishing-resistant** MFA methods (FIDO2, passkeys) and apply **least privilege** principles.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_mfa_for_admin_portals"
}
},
"Categories": [
"identity-access",
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be regularly reviewed or investigated."
}

View File

@@ -0,0 +1,44 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.config import MICROSOFT_ADMIN_PORTALS
from prowler.providers.azure.services.entra.entra_client import entra_client
class entra_conditional_access_policy_require_mfa_for_admin_portals(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for (
tenant_name,
conditional_access_policies,
) in entra_client.conditional_access_policy.items():
for policy in conditional_access_policies.values():
if (
policy.state == "enabled"
and "All" in policy.users["include"]
and MICROSOFT_ADMIN_PORTALS in policy.target_resources["include"]
and any(
"mfa" in access_control.lower()
for access_control in policy.access_controls["grant"]
)
):
report = Check_Report_Azure(
metadata=self.metadata(), resource=policy
)
report.subscription = f"Tenant: {tenant_name}"
report.status = "PASS"
report.status_extended = "Conditional Access Policy requires MFA for Microsoft Admin Portals."
break
else:
report = Check_Report_Azure(
metadata=self.metadata(),
resource=conditional_access_policies,
)
report.subscription = f"Tenant: {tenant_name}"
report.resource_name = "Conditional Access Policy"
report.resource_id = "Conditional Access Policy"
report.status = "FAIL"
report.status_extended = "Conditional Access Policy does not require MFA for Microsoft Admin Portals."
findings.append(report)
return findings

View File

@@ -9,15 +9,19 @@
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "This recommendation ensures that users accessing the Windows Azure Service Management API (i.e. Azure Powershell, Azure CLI, Azure Resource Manager API, etc.) are required to use multifactor authentication (MFA) credentials when accessing resources through the Windows Azure Service Management API.",
"Risk": "Administrative access to the Windows Azure Service Management API should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multifactor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings.",
"Description": "**Microsoft Entra Conditional Access** is evaluated for a policy that requires **multifactor authentication** when accessing the **Windows Azure Service Management API** (Azure PowerShell, Azure CLI, Azure Resource Manager API, etc.). The check confirms an enabled policy targets **All users**, includes the Service Management API app, and enforces an **MFA** grant control.",
"Risk": "Without **MFA** on the Service Management API, attackers with stolen credentials can use **Azure CLI**, **PowerShell**, or the **Resource Manager API** to modify infrastructure, escalate privileges, and exfiltrate data. This directly impacts **confidentiality**, **integrity**, and **availability** of all Azure resources managed through the API.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Protection > Conditional Access > Policies\n3. Click + New policy and enter a name\n4. Under Users > Include, select All users\n5. Under Exclude, check Users and groups and select break-glass / non-interactive service accounts\n6. Under Target resources > Include, click Select apps, then select Windows Azure Service Management API\n7. Under Grant, select Grant access and check Require multifactor authentication\n8. Set Enable policy to Report-only, click Create\n9. After testing, change Enable policy from Report-only to On",
"Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"<example_resource_name>\" {\n display_name = \"<example_resource_name>\"\n state = \"enabled\"\n\n conditions {\n users {\n included_users = [\"All\"]\n excluded_users = [\"<break_glass_account_id>\"]\n }\n applications {\n included_applications = [\"797f4846-ba00-4fd7-ba43-dac1f8f63013\"] # Critical: Windows Azure Service Management API\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: requires MFA\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From the Azure Admin Portal dashboard, open Microsoft Entra ID. 2. Click Security in the Entra ID blade. 3. Click Conditional Access in the Security blade. 4. Click Policies in the Conditional Access blade. 5. Click + New policy. 6. Enter a name for the policy. 7. Click the blue text under Users. 8. Under Include, select All users. 9. Under Exclude, check Users and groups. 10. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the Select button. 11. Click the blue text under Target Resources. 12. Under Include, click the Select apps radio button. 13. Click the blue text under Select. 14. Check the box next to Windows Azure Service Management APIs then click the Select button. 15. Click the blue text under Grant. 16. Under Grant access check the box for Require multifactor authentication then click the Select button. 17. Before creating, set Enable policy to Report-only. 18. Click Create. After testing the policy in report-only mode, update the Enable policy setting from Report-only to On.",

View File

@@ -0,0 +1,280 @@
from unittest import mock
from uuid import uuid4
from tests.providers.azure.azure_fixtures import DOMAIN, set_mocked_azure_provider
class Test_entra_conditional_access_policy_require_mfa_for_admin_portals:
def test_entra_no_subscriptions(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
entra_client.conditional_access_policy = {}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 0
def test_entra_tenant_no_policies(self):
entra_client = mock.MagicMock
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
entra_client.conditional_access_policy = {DOMAIN: {}}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Conditional Access Policy"
assert result[0].resource_id == "Conditional Access Policy"
assert (
result[0].status_extended
== "Conditional Access Policy does not require MFA for Microsoft Admin Portals."
)
def test_entra_tenant_policy_no_mfa(self):
entra_client = mock.MagicMock
policy_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
from prowler.providers.azure.services.entra.entra_service import (
ConditionalAccessPolicy,
)
policy = ConditionalAccessPolicy(
id=policy_id,
name="Test Policy",
state="enabled",
users={"include": ["All"]},
target_resources={"include": ["MicrosoftAdminPortals"]},
access_controls={"grant": ["grant"]},
)
entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Conditional Access Policy"
assert result[0].resource_id == "Conditional Access Policy"
assert (
result[0].status_extended
== "Conditional Access Policy does not require MFA for Microsoft Admin Portals."
)
def test_entra_tenant_policy_mfa(self):
entra_client = mock.MagicMock
policy_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
from prowler.providers.azure.services.entra.entra_service import (
ConditionalAccessPolicy,
)
policy = ConditionalAccessPolicy(
id=policy_id,
name="Test Policy",
state="enabled",
users={"include": ["All"]},
target_resources={"include": ["MicrosoftAdminPortals"]},
access_controls={"grant": ["grant", "MFA"]},
)
entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Test Policy"
assert result[0].resource_id == policy_id
assert (
result[0].status_extended
== "Conditional Access Policy requires MFA for Microsoft Admin Portals."
)
def test_entra_tenant_policy_mfa_disabled(self):
entra_client = mock.MagicMock
policy_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
from prowler.providers.azure.services.entra.entra_service import (
ConditionalAccessPolicy,
)
policy = ConditionalAccessPolicy(
id=policy_id,
name="Test Policy",
state="disabled",
users={"include": ["All"]},
target_resources={"include": ["MicrosoftAdminPortals"]},
access_controls={"grant": ["grant", "MFA"]},
)
entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Conditional Access Policy"
assert result[0].resource_id == "Conditional Access Policy"
assert (
result[0].status_extended
== "Conditional Access Policy does not require MFA for Microsoft Admin Portals."
)
def test_entra_tenant_policy_mfa_no_target(self):
entra_client = mock.MagicMock
policy_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
from prowler.providers.azure.services.entra.entra_service import (
ConditionalAccessPolicy,
)
policy = ConditionalAccessPolicy(
id=policy_id,
name="Test Policy",
state="enabled",
users={"include": ["All"]},
target_resources={"include": []},
access_controls={"grant": ["grant", "MFA"]},
)
entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Conditional Access Policy"
assert result[0].resource_id == "Conditional Access Policy"
assert (
result[0].status_extended
== "Conditional Access Policy does not require MFA for Microsoft Admin Portals."
)
def test_entra_tenant_policy_mfa_no_users(self):
entra_client = mock.MagicMock
policy_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_admin_portals.entra_conditional_access_policy_require_mfa_for_admin_portals import (
entra_conditional_access_policy_require_mfa_for_admin_portals,
)
from prowler.providers.azure.services.entra.entra_service import (
ConditionalAccessPolicy,
)
policy = ConditionalAccessPolicy(
id=policy_id,
name="Test Policy",
state="enabled",
users={"include": []},
target_resources={"include": ["MicrosoftAdminPortals"]},
access_controls={"grant": ["grant", "MFA"]},
)
entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}}
check = entra_conditional_access_policy_require_mfa_for_admin_portals()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Conditional Access Policy"
assert result[0].resource_id == "Conditional Access Policy"
assert (
result[0].status_extended
== "Conditional Access Policy does not require MFA for Microsoft Admin Portals."
)