Compare commits

...

1 Commits

Author SHA1 Message Date
Hugo P.Brito
b0660ee288 feat(m365): add entra_conditional_access_policy_mfa_enforced_for_guest_users security check
Add new security check entra_conditional_access_policy_mfa_enforced_for_guest_users for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-04-08 16:14:24 +01:00
11 changed files with 895 additions and 0 deletions

View File

@@ -21,6 +21,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222)
- `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)
- `entra_conditional_access_policy_mfa_enforced_for_guest_users` check for M365 provider [(#10616)](https://github.com/prowler-cloud/prowler/pull/10616)
### 🔄 Changed

View File

@@ -1146,6 +1146,7 @@
"Id": "5.2.2.2",
"Description": "Enable multifactor authentication for all users in the Microsoft 365 tenant. Users will be prompted to authenticate with a second factor upon logging in to Microsoft 365 services. The second factor is most commonly a text message to a registered mobile phone number where they type in an authorization code, or with a mobile application like Microsoft Authenticator.",
"Checks": [
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_users_mfa_enabled"
],
"Attributes": [

View File

@@ -1380,6 +1380,7 @@
"Id": "5.2.2.2",
"Description": "Enable multifactor authentication for all users in the Microsoft 365 tenant. Users will be prompted to authenticate with a second factor upon logging in to Microsoft 365 services.",
"Checks": [
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_users_mfa_enabled"
],
"Attributes": [

View File

@@ -206,6 +206,7 @@
"admincenter_users_admins_reduced_license_footprint",
"entra_admin_portals_access_restriction",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_policy_guest_users_access_restrictions",
"entra_seamless_sso_disabled"
@@ -245,6 +246,7 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_break_glass_account_fido2_security_key_registered",
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_default_app_management_policy_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_device_registration_mfa_required",
@@ -686,6 +688,7 @@
"entra_conditional_access_policy_app_enforced_restrictions",
"entra_conditional_access_policy_block_elevated_insider_risk",
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_policy_guest_users_access_restrictions",
"sharepoint_external_sharing_restricted"
]
@@ -707,6 +710,7 @@
"entra_admin_users_sign_in_frequency_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_device_registration_mfa_required",
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_intune_enrollment_sign_in_frequency_every_time",
"entra_break_glass_account_fido2_security_key_registered",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",

View File

@@ -46,6 +46,7 @@
"Id": "1.1.3",
"Description": "Ensure multifactor authentication is enabled for all users",
"Checks": [
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
"entra_users_mfa_enabled"
],
"Attributes": [

View File

@@ -0,0 +1,43 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_mfa_enforced_for_guest_users",
"CheckTitle": "Conditional Access Policy enforces MFA for guest and external users",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Microsoft Entra **Conditional Access** is verified to have at least one **enabled** policy that requires **multifactor authentication** for all **guest and external user types** across all cloud applications. This includes internal guests, B2B collaboration guests and members, B2B direct connect users, other external users, and service providers.",
"Risk": "Without MFA for guest users, compromised external accounts can access tenant resources using only a password. Attackers may exploit **B2B collaboration**, **direct connect**, or **service provider** accounts to exfiltrate data, escalate privileges, or move laterally across the organization.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-alt-require-mfa-guest-access",
"https://maester.dev/docs/tests/MT.1016"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com).\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **Include** > **Select users and groups** > check **Guest or external users** > select all guest user types.\n5. Under **Target resources**, select **Include** > **All cloud apps**.\n6. Under **Grant**, select **Grant access** > check **Require multifactor authentication** > click **Select**.\n7. Set the policy to **Report-only** until validated, then enable it.",
"Terraform": ""
},
"Recommendation": {
"Text": "Enforce **MFA** via **Conditional Access** for all **guest and external user types** across all cloud applications. Prefer **phishing-resistant** methods, monitor guest sign-ins, and regularly review external collaboration settings.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_mfa_enforced_for_guest_users"
}
},
"Categories": [
"identity-access",
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [
"entra_policy_guest_users_access_restrictions",
"entra_policy_guest_invite_only_for_admin_roles",
"entra_dynamic_group_for_guests_created"
],
"Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. This check corresponds to Maester test MT.1016 (Test-MtCaMfaForGuest)."
}

View File

@@ -0,0 +1,137 @@
"""Check if at least one Conditional Access policy requires MFA for guest users."""
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.entra.entra_client import entra_client
from prowler.providers.m365.services.entra.entra_service import (
ALL_GUEST_USER_TYPES,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
)
class entra_conditional_access_policy_mfa_enforced_for_guest_users(Check):
"""Check if at least one enabled Conditional Access policy requires MFA for guest users.
This check verifies that the Microsoft Entra tenant has at least one
enabled Conditional Access policy that requires multifactor authentication
(MFA) for all guest and external user types across all cloud applications.
- PASS: At least one enabled CA policy requires MFA for all guest user types.
- FAIL: No enabled CA policy enforces MFA for guest users.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check logic.
Returns:
A list of reports containing the result of the check.
"""
findings = []
report = CheckReportM365(
metadata=self.metadata(),
resource={},
resource_name="Conditional Access Policies",
resource_id="conditionalAccessPolicies",
)
report.status = "FAIL"
report.status_extended = (
"No Conditional Access Policy requires MFA for guest users."
)
reporting_policy = None
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
# Policy must require MFA (built-in control or authentication strength)
# and must not only require password change.
has_mfa = (
ConditionalAccessGrantControl.MFA
in policy.grant_controls.built_in_controls
)
has_auth_strength = (
policy.grant_controls.authentication_strength is not None
)
only_password_change = (
policy.grant_controls.built_in_controls
== [ConditionalAccessGrantControl.PASSWORD_CHANGE]
)
if not (has_mfa or has_auth_strength) or only_password_change:
continue
# Policy must target all cloud applications.
if not policy.conditions.application_conditions:
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue
# Policy must target guest users: either include all users, or
# specifically include all guest/external user types.
targets_all_users = (
"All" in policy.conditions.user_conditions.included_users
)
targets_guests_via_include = (
"GuestsOrExternalUsers"
in policy.conditions.user_conditions.included_users
)
included_guests = (
policy.conditions.user_conditions.included_guests_or_external_users
)
targets_all_guest_types = included_guests is not None and (
ALL_GUEST_USER_TYPES
<= set(included_guests.guest_or_external_user_types)
)
if not (
targets_all_users
or targets_guests_via_include
or targets_all_guest_types
):
continue
# Policy must not exclude guest/external user types.
excluded_guests = (
policy.conditions.user_conditions.excluded_guests_or_external_users
)
if (
excluded_guests is not None
and excluded_guests.guest_or_external_user_types
):
continue
if policy.state == ConditionalAccessPolicyState.ENABLED:
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name,
resource_id=policy.id,
)
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' requires MFA for guest users."
break
if (
policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING
and reporting_policy is None
):
reporting_policy = policy
if report.status == "FAIL" and reporting_policy:
report = CheckReportM365(
metadata=self.metadata(),
resource=reporting_policy,
resource_name=reporting_policy.display_name,
resource_id=reporting_policy.id,
)
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{reporting_policy.display_name}' targets guest users with MFA but is only in report-only mode."
findings.append(report)
return findings

View File

@@ -264,6 +264,20 @@ class Entra(M365Service):
[],
)
],
included_guests_or_external_users=self._parse_guests_or_external_users(
getattr(
policy.conditions.users,
"include_guests_or_external_users",
None,
)
),
excluded_guests_or_external_users=self._parse_guests_or_external_users(
getattr(
policy.conditions.users,
"exclude_guests_or_external_users",
None,
)
),
),
client_app_types=[
ClientAppType(client_app_type)
@@ -546,6 +560,36 @@ class Entra(M365Service):
return AuthenticationFlows(transfer_methods=transfer_methods)
@staticmethod
def _parse_guests_or_external_users(
sdk_obj,
) -> "GuestsOrExternalUsers | None":
"""Parse guest or external user conditions from the MS Graph SDK object.
The SDK deserializes ``guestOrExternalUserTypes`` via
``get_collection_of_enum_values``, returning a list of SDK enum members.
Args:
sdk_obj: A ``ConditionalAccessGuestsOrExternalUsers`` SDK object, or ``None``.
Returns:
A ``GuestsOrExternalUsers`` model instance, or ``None`` if the input is absent.
"""
if sdk_obj is None:
return None
raw_types = getattr(sdk_obj, "guest_or_external_user_types", None) or []
guest_types: list[GuestOrExternalUserType] = []
for raw_type in raw_types:
try:
guest_types.append(GuestOrExternalUserType(raw_type.value))
except (ValueError, AttributeError):
logger.warning(
f"Unknown guest or external user type: {raw_type}"
)
return GuestsOrExternalUsers(guest_or_external_user_types=guest_types)
@staticmethod
def _parse_app_management_restrictions(restrictions):
"""Parse credential restrictions from the Graph API response into AppManagementRestrictions."""
@@ -957,13 +1001,49 @@ class ApplicationsConditions(BaseModel):
included_user_actions: List[UserAction]
class GuestOrExternalUserType(Enum):
"""Guest or external user types for Conditional Access policies.
Reference: https://learn.microsoft.com/en-us/graph/api/resources/conditionalaccessguestsorexternalusers
"""
NONE = "none"
INTERNAL_GUEST = "internalGuest"
B2B_COLLABORATION_GUEST = "b2bCollaborationGuest"
B2B_COLLABORATION_MEMBER = "b2bCollaborationMember"
B2B_DIRECT_CONNECT_USER = "b2bDirectConnectUser"
OTHER_EXTERNAL_USER = "otherExternalUser"
SERVICE_PROVIDER = "serviceProvider"
# All guest/external user types that represent actual guest or external users.
ALL_GUEST_USER_TYPES = {
GuestOrExternalUserType.INTERNAL_GUEST,
GuestOrExternalUserType.B2B_COLLABORATION_GUEST,
GuestOrExternalUserType.B2B_COLLABORATION_MEMBER,
GuestOrExternalUserType.B2B_DIRECT_CONNECT_USER,
GuestOrExternalUserType.OTHER_EXTERNAL_USER,
GuestOrExternalUserType.SERVICE_PROVIDER,
}
class GuestsOrExternalUsers(BaseModel):
"""Model representing guest or external user conditions in Conditional Access policies."""
guest_or_external_user_types: List[GuestOrExternalUserType] = []
class UsersConditions(BaseModel):
"""Model representing user conditions for Conditional Access policies."""
included_groups: List[str]
excluded_groups: List[str]
included_users: List[str]
excluded_users: List[str]
included_roles: List[str]
excluded_roles: List[str]
included_guests_or_external_users: Optional[GuestsOrExternalUsers] = None
excluded_guests_or_external_users: Optional[GuestsOrExternalUsers] = None
class RiskLevel(Enum):

View File

@@ -0,0 +1,627 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ALL_GUEST_USER_TYPES,
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GuestOrExternalUserType,
GuestsOrExternalUsers,
GrantControlOperator,
GrantControls,
PersistentBrowser,
SessionControls,
SignInFrequency,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users"
def build_policy(
*,
display_name: str,
state: ConditionalAccessPolicyState,
included_applications: list[str] | None = None,
included_users: list[str] | None = None,
built_in_controls: list[ConditionalAccessGrantControl] | None = None,
authentication_strength: str | None = None,
included_guests_or_external_users: GuestsOrExternalUsers | None = None,
excluded_guests_or_external_users: GuestsOrExternalUsers | None = None,
):
"""Build a ConditionalAccessPolicy for testing."""
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
return ConditionalAccessPolicy(
id=str(uuid4()),
display_name=display_name,
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=included_applications or ["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=included_users or [],
excluded_users=[],
included_roles=[],
excluded_roles=[],
included_guests_or_external_users=included_guests_or_external_users,
excluded_guests_or_external_users=excluded_guests_or_external_users,
),
client_app_types=[],
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=built_in_controls or [],
operator=GrantControlOperator.OR,
authentication_strength=authentication_strength,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(is_enabled=False, mode="always"),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=None,
),
),
state=state,
)
class Test_entra_conditional_access_policy_mfa_enforced_for_guest_users:
"""Tests for the entra_conditional_access_policy_mfa_enforced_for_guest_users check."""
def test_no_conditional_access_policies(self):
"""Test FAIL when there are no Conditional Access policies."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
entra_client.conditional_access_policies = {}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_disabled_policy_is_skipped(self):
"""Test FAIL when the only matching policy is disabled."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Guests",
state=ConditionalAccessPolicyState.DISABLED,
included_users=["GuestsOrExternalUsers"],
built_in_controls=[ConditionalAccessGrantControl.MFA],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_policy_enabled_targeting_all_users_with_mfa(self):
"""Test PASS when an enabled policy targets all users with MFA for all apps."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for All Users",
state=ConditionalAccessPolicyState.ENABLED,
included_users=["All"],
built_in_controls=[ConditionalAccessGrantControl.MFA],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Conditional Access Policy 'MFA for All Users' requires MFA for guest users."
)
assert result[0].resource_id == policy.id
assert result[0].resource_name == "MFA for All Users"
def test_policy_enabled_targeting_guests_or_external_users(self):
"""Test PASS when an enabled policy specifically targets GuestsOrExternalUsers."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Guest Users",
state=ConditionalAccessPolicyState.ENABLED,
included_users=["GuestsOrExternalUsers"],
built_in_controls=[ConditionalAccessGrantControl.MFA],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Conditional Access Policy 'MFA for Guest Users' requires MFA for guest users."
)
assert result[0].resource_id == policy.id
def test_policy_enabled_targeting_all_guest_types_via_included_guests(self):
"""Test PASS when policy targets all six guest types via included_guests_or_external_users."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for All Guest Types",
state=ConditionalAccessPolicyState.ENABLED,
built_in_controls=[ConditionalAccessGrantControl.MFA],
included_guests_or_external_users=GuestsOrExternalUsers(
guest_or_external_user_types=list(ALL_GUEST_USER_TYPES),
),
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_id == policy.id
def test_policy_with_authentication_strength_passes(self):
"""Test PASS when policy uses authentication strength instead of MFA built-in control."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="Auth Strength for Guests",
state=ConditionalAccessPolicyState.ENABLED,
included_users=["GuestsOrExternalUsers"],
authentication_strength="Phishing-resistant MFA",
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_id == policy.id
def test_policy_only_password_change_fails(self):
"""Test FAIL when the policy only requires password change."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="Password Change for Guests",
state=ConditionalAccessPolicyState.ENABLED,
included_users=["GuestsOrExternalUsers"],
built_in_controls=[ConditionalAccessGrantControl.PASSWORD_CHANGE],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_policy_not_targeting_all_apps_fails(self):
"""Test FAIL when the policy does not target all cloud applications."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Guests Specific App",
state=ConditionalAccessPolicyState.ENABLED,
included_applications=["some-specific-app-id"],
included_users=["GuestsOrExternalUsers"],
built_in_controls=[ConditionalAccessGrantControl.MFA],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_policy_not_targeting_guests_fails(self):
"""Test FAIL when the policy does not target guest users."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Specific Users",
state=ConditionalAccessPolicyState.ENABLED,
included_users=[str(uuid4())],
built_in_controls=[ConditionalAccessGrantControl.MFA],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_policy_with_partial_guest_types_fails(self):
"""Test FAIL when policy only targets some guest types but not all six."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Some Guests",
state=ConditionalAccessPolicyState.ENABLED,
built_in_controls=[ConditionalAccessGrantControl.MFA],
included_guests_or_external_users=GuestsOrExternalUsers(
guest_or_external_user_types=[
GuestOrExternalUserType.B2B_COLLABORATION_GUEST,
GuestOrExternalUserType.INTERNAL_GUEST,
],
),
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_policy_with_excluded_guest_types_fails(self):
"""Test FAIL when the policy excludes guest/external user types."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Guests with Exclusions",
state=ConditionalAccessPolicyState.ENABLED,
included_users=["All"],
built_in_controls=[ConditionalAccessGrantControl.MFA],
excluded_guests_or_external_users=GuestsOrExternalUsers(
guest_or_external_user_types=[
GuestOrExternalUserType.SERVICE_PROVIDER,
],
),
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_reporting_only_policy_fails_with_detail(self):
"""Test FAIL with detail when the matching policy is in report-only mode."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="MFA for Guests Report Only",
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
included_users=["GuestsOrExternalUsers"],
built_in_controls=[ConditionalAccessGrantControl.MFA],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Conditional Access Policy 'MFA for Guests Report Only' targets guest users with MFA but is only in report-only mode."
)
assert result[0].resource_id == policy.id
assert result[0].resource_name == "MFA for Guests Report Only"
def test_no_application_conditions_fails(self):
"""Test FAIL when the policy has no application conditions."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
policy_id = str(uuid4())
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="No App Conditions",
conditions=Conditions(
application_conditions=None,
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["GuestsOrExternalUsers"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.OR,
authentication_strength=None,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=None,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)
def test_no_mfa_grant_control_fails(self):
"""Test FAIL when the policy does not require MFA as a grant control."""
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(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_mfa_enforced_for_guest_users.entra_conditional_access_policy_mfa_enforced_for_guest_users import (
entra_conditional_access_policy_mfa_enforced_for_guest_users,
)
policy = build_policy(
display_name="Compliant Device for Guests",
state=ConditionalAccessPolicyState.ENABLED,
included_users=["GuestsOrExternalUsers"],
built_in_controls=[ConditionalAccessGrantControl.COMPLIANT_DEVICE],
)
entra_client.conditional_access_policies = {policy.id: policy}
result = (
entra_conditional_access_policy_mfa_enforced_for_guest_users().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy requires MFA for guest users."
)