Compare commits

...

1 Commits

Author SHA1 Message Date
HugoPBrito
3c472d0a24 feat(m365): add entra_guest_users_mfa_enabled security check
Add new security check entra_guest_users_mfa_enabled for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-02-24 11:16:57 +01:00
11 changed files with 873 additions and 0 deletions

View File

@@ -28,6 +28,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Registry scan mode for `image` provider: enumerate and scan all images from OCI standard, Docker Hub, and ECR [(#9985)](https://github.com/prowler-cloud/prowler/pull/9985)
- Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)
- CIS 6.0 for the AWS provider [(#10127)](https://github.com/prowler-cloud/prowler/pull/10127)
- `entra_guest_users_mfa_enabled` check for m365 provider [(#10146)](https://github.com/prowler-cloud/prowler/pull/10146)
### 🐞 Fixed

View File

@@ -1143,6 +1143,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_guest_users_mfa_enabled",
"entra_users_mfa_enabled"
],
"Attributes": [

View File

@@ -1377,6 +1377,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_guest_users_mfa_enabled",
"entra_users_mfa_enabled"
],
"Attributes": [

View File

@@ -202,6 +202,7 @@
"admincenter_users_admins_reduced_license_footprint",
"entra_admin_portals_access_restriction",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_guest_users_mfa_enabled",
"entra_policy_guest_users_access_restrictions",
"entra_seamless_sso_disabled"
]
@@ -240,6 +241,7 @@
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_guest_users_mfa_enabled",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -264,6 +266,7 @@
"Checks": [
"entra_admin_portals_access_restriction",
"entra_app_registration_no_unused_privileged_permissions",
"entra_guest_users_mfa_enabled",
"entra_policy_guest_users_access_restrictions",
"sharepoint_external_sharing_managed",
"sharepoint_external_sharing_restricted",
@@ -667,6 +670,7 @@
"Checks": [
"sharepoint_external_sharing_restricted",
"entra_admin_portals_access_restriction",
"entra_guest_users_mfa_enabled",
"entra_policy_guest_users_access_restrictions"
]
},
@@ -685,6 +689,7 @@
"Checks": [
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_guest_users_mfa_enabled",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
"entra_users_mfa_enabled",

View File

@@ -45,6 +45,7 @@
"Id": "1.1.3",
"Description": "Ensure multifactor authentication is enabled for all users",
"Checks": [
"entra_guest_users_mfa_enabled",
"entra_users_mfa_enabled"
],
"Attributes": [
@@ -189,6 +190,7 @@
"Id": "1.1.11",
"Description": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Non-Privileged Users",
"Checks": [
"entra_guest_users_mfa_enabled",
"entra_users_mfa_enabled"
],
"Attributes": [
@@ -769,6 +771,7 @@
"Id": "1.3.6",
"Description": "Ensure that guest user access is restricted",
"Checks": [
"entra_guest_users_mfa_enabled",
"entra_policy_guest_users_access_restrictions"
],
"Attributes": [

View File

@@ -0,0 +1,40 @@
{
"Provider": "m365",
"CheckID": "entra_guest_users_mfa_enabled",
"CheckTitle": "Conditional Access policy enforces MFA for guest users",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Conditional Access Policy",
"ResourceGroup": "IAM",
"Description": "At least one Conditional Access policy requires **multifactor authentication** for all guest and external user types across all cloud applications.\n\nThis covers internal guests, B2B collaboration guests and members, B2B direct connect users, other external users, and service providers.",
"Risk": "Guest and external users authenticate from environments outside the organization's control. Without MFA, a compromised guest credential can grant an attacker access to tenant resources, leading to **data exfiltration** or **lateral movement** across collaboration boundaries.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-mfa-guest",
"https://maester.dev/docs/tests/MT.1016"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center at 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** and select all guest types.\n5. Under **Target resources**, include **All cloud apps**.\n6. Under **Grant**, select **Grant Access** and check **Require multifactor authentication**.\n7. Set the policy to **Report Only** for initial testing, then switch to **On**.\n8. Click **Create**.",
"Terraform": ""
},
"Recommendation": {
"Text": "Enforce MFA for all guest and external user types through a Conditional Access policy. Apply the **zero trust** principle to external identities by treating them as untrusted by default and requiring strong authentication before granting access to any cloud application.",
"Url": "https://hub.prowler.com/check/entra_guest_users_mfa_enabled"
}
},
"Categories": [
"e3",
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"entra_users_mfa_enabled"
],
"Notes": "Equivalent to Maester test MT.1016 (Test-MtCaMfaForGuest)."
}

View File

@@ -0,0 +1,122 @@
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 (
ConditionalAccessGrantControl,
ConditionalAccessPolicy,
ConditionalAccessPolicyState,
GuestOrExternalUserType,
)
ALL_GUEST_TYPES = {guest_type for guest_type in GuestOrExternalUserType}
class entra_guest_users_mfa_enabled(Check):
"""Conditional Access policy enforces MFA for guest users.
This check verifies that at least one enabled Conditional Access policy
requires multifactor authentication for all guest and external user types
across all cloud applications.
- PASS: An enabled policy requires MFA for all guest user types.
- FAIL: No enabled policy requires MFA for guest users, or matching
policies are only in report-only mode.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the guest users MFA check.
Returns:
A list containing a single report with 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 enforces MFA for guest users."
)
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if not self._policy_targets_guests(policy):
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue
if not self._policy_requires_mfa(policy):
continue
if self._policy_excludes_guests(policy):
continue
report = CheckReportM365(
metadata=self.metadata(),
resource=policy,
resource_name=policy.display_name,
resource_id=policy.id,
)
if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING:
report.status = "FAIL"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' reports MFA requirement for guest users but does not enforce it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' enforces MFA for guest users."
break
findings.append(report)
return findings
@staticmethod
def _policy_targets_guests(policy: ConditionalAccessPolicy) -> bool:
"""Check if a policy targets guest users.
A policy targets guests if it either applies to all users or explicitly
includes all guest and external user types.
"""
if "All" in policy.conditions.user_conditions.included_users:
return True
guests_config = (
policy.conditions.user_conditions.included_guests_or_external_users
)
if guests_config and ALL_GUEST_TYPES.issubset(
set(guests_config.guest_or_external_user_types)
):
return True
return False
@staticmethod
def _policy_requires_mfa(policy: ConditionalAccessPolicy) -> bool:
"""Check if a policy requires MFA via built-in controls or authentication strength."""
if (
ConditionalAccessGrantControl.MFA
in policy.grant_controls.built_in_controls
):
return True
if policy.grant_controls.authentication_strength is not None:
return True
return False
@staticmethod
def _policy_excludes_guests(policy: ConditionalAccessPolicy) -> bool:
"""Check if a policy excludes guest or external user types."""
excluded = (
policy.conditions.user_conditions.excluded_guests_or_external_users
)
if excluded and excluded.guest_or_external_user_types:
return True
return False

View File

@@ -245,6 +245,20 @@ class Entra(M365Service):
[],
)
],
included_guests_or_external_users=Entra._parse_guests_or_external_users(
getattr(
policy.conditions.users,
"include_guests_or_external_users",
None,
)
),
excluded_guests_or_external_users=Entra._parse_guests_or_external_users(
getattr(
policy.conditions.users,
"exclude_guests_or_external_users",
None,
)
),
),
client_app_types=[
ClientAppType(client_app_type)
@@ -352,6 +366,30 @@ class Entra(M365Service):
)
return conditional_access_policies
@staticmethod
def _parse_guests_or_external_users(sdk_value):
"""Parse a ConditionalAccessGuestsOrExternalUsers SDK object into a GuestsOrExternalUsers model.
Args:
sdk_value: The SDK object from the Microsoft Graph API, or None.
Returns:
A GuestsOrExternalUsers instance, or None if the SDK value is absent.
"""
if sdk_value is None:
return None
raw_types = getattr(sdk_value, "guest_or_external_user_types", None)
if not raw_types:
return GuestsOrExternalUsers(guest_or_external_user_types=[])
guest_types = []
for type_str in str(raw_types).split(","):
type_str = type_str.strip()
try:
guest_types.append(GuestOrExternalUserType(type_str))
except ValueError:
pass
return GuestsOrExternalUsers(guest_or_external_user_types=guest_types)
async def _get_admin_consent_policy(self):
logger.info("Entra - Getting group settings...")
admin_consent_policy = None
@@ -681,6 +719,26 @@ 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
"""
INTERNAL_GUEST = "internalGuest"
B2B_COLLABORATION_GUEST = "b2bCollaborationGuest"
B2B_COLLABORATION_MEMBER = "b2bCollaborationMember"
B2B_DIRECT_CONNECT_USER = "b2bDirectConnectUser"
OTHER_EXTERNAL_USER = "otherExternalUser"
SERVICE_PROVIDER = "serviceProvider"
class GuestsOrExternalUsers(BaseModel):
"""Guests or external users targeted by a Conditional Access policy."""
guest_or_external_user_types: List[GuestOrExternalUserType] = []
class UsersConditions(BaseModel):
included_groups: List[str]
excluded_groups: List[str]
@@ -688,6 +746,8 @@ class UsersConditions(BaseModel):
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,640 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicy,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
GuestOrExternalUserType,
GuestsOrExternalUsers,
PersistentBrowser,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE = "prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled"
DEFAULT_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,
),
)
ALL_GUEST_TYPES_LIST = [guest_type for guest_type in GuestOrExternalUserType]
class Test_entra_guest_users_mfa_enabled:
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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
entra_client.conditional_access_policies = {}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for guest users."
)
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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
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=[],
included_user_actions=[],
),
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,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for guest users."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
def test_policy_all_users_mfa_enabled(self):
"""Enabled policy targeting All users with MFA: expected PASS."""
policy_id = str(uuid4())
display_name = "MFA for All 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}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
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=[],
included_user_actions=[],
),
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,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' enforces MFA for guest users."
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_report_only(self):
"""Policy in report-only state: expected FAIL with specific message."""
policy_id = str(uuid4())
display_name = "Report Only 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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
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=[],
included_user_actions=[],
),
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,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' reports MFA requirement for guest users but does not enforce it."
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_guest_types_mfa_enabled(self):
"""Enabled policy targeting all guest types with MFA: expected PASS."""
policy_id = str(uuid4())
display_name = "MFA for Guests"
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}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
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=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[],
excluded_roles=[],
included_guests_or_external_users=GuestsOrExternalUsers(
guest_or_external_user_types=ALL_GUEST_TYPES_LIST,
),
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' enforces MFA for guest users."
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_incomplete_guest_types(self):
"""Policy targeting only some guest types: 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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Partial Guest Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[],
excluded_roles=[],
included_guests_or_external_users=GuestsOrExternalUsers(
guest_or_external_user_types=[
GuestOrExternalUserType.B2B_COLLABORATION_GUEST,
GuestOrExternalUserType.INTERNAL_GUEST,
],
),
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for guest users."
)
def test_policy_excludes_guest_types(self):
"""Policy targeting All users but excluding guest types: 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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Excludes Guests Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
excluded_guests_or_external_users=GuestsOrExternalUsers(
guest_or_external_user_types=[
GuestOrExternalUserType.B2B_COLLABORATION_GUEST,
],
),
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.MFA],
operator=GrantControlOperator.AND,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for guest users."
)
def test_policy_authentication_strength_mfa(self):
"""Policy with authentication strength instead of built-in MFA control: expected PASS."""
policy_id = str(uuid4())
display_name = "Auth Strength 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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
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=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[],
operator=GrantControlOperator.AND,
authentication_strength="Multifactor authentication",
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' enforces MFA for guest users."
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
def test_policy_no_mfa_grant_control(self):
"""Policy without MFA grant control: 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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Block Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["All"],
excluded_applications=[],
included_user_actions=[],
),
user_conditions=UsersConditions(
included_groups=[],
excluded_groups=[],
included_users=["All"],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
operator=GrantControlOperator.AND,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for guest users."
)
def test_policy_not_all_applications(self):
"""Policy targeting specific apps instead of All: 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_m365_provider(),
),
mock.patch(
f"{CHECK_MODULE}.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_guest_users_mfa_enabled.entra_guest_users_mfa_enabled import (
entra_guest_users_mfa_enabled,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name="Specific App MFA Policy",
conditions=Conditions(
application_conditions=ApplicationsConditions(
included_applications=["some-app-id"],
excluded_applications=[],
included_user_actions=[],
),
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,
authentication_strength=None,
),
session_controls=DEFAULT_SESSION_CONTROLS,
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_guest_users_mfa_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy enforces MFA for guest users."
)