feat(m365): add entra_device_code_flow_blocked security check (#10218)

This commit is contained in:
Hugo Pereira Brito
2026-03-13 10:31:47 +00:00
committed by GitHub
parent 37d59b118f
commit 534ad3d04f
8 changed files with 1111 additions and 2 deletions

View File

@@ -4,6 +4,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [5.21.0] (Prowler UNRELEASED)
### 🚀 Added
- `entra_conditional_access_policy_device_code_flow_blocked` check for M365 provider [(#10218)](https://github.com/prowler-cloud/prowler/pull/10218)
### 🔄 Changed
- Update M365 SharePoint service metadata to new format [(#9684)](https://github.com/prowler-cloud/prowler/pull/9684)

View File

@@ -1606,7 +1606,9 @@
{
"Id": "5.2.2.12",
"Description": "The Microsoft identity platform supports the device authorization grant, which allows users to sign in to input-constrained devices such as a smart TV, IoT device, or a printer. Ensure the device code sign-in flow is blocked.",
"Checks": [],
"Checks": [
"entra_conditional_access_policy_device_code_flow_blocked"
],
"Attributes": [
{
"Section": "5 Microsoft Entra admin center",

View File

@@ -243,6 +243,7 @@
"entra_break_glass_account_fido2_security_key_registered",
"entra_default_app_management_policy_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_legacy_authentication_blocked",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",
@@ -464,6 +465,7 @@
"defender_malware_policy_common_attachments_filter_enabled",
"defender_malware_policy_comprehensive_attachments_filter_applied",
"defender_malware_policy_notifications_internal_users_malware_enabled",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_identity_protection_sign_in_risk_enabled",
"entra_identity_protection_user_risk_enabled",
"entra_legacy_authentication_blocked",
@@ -694,8 +696,9 @@
"entra_admin_users_mfa_enabled",
"entra_admin_users_sign_in_frequency_enabled",
"entra_all_apps_conditional_access_coverage",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_break_glass_account_fido2_security_key_registered",
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
"entra_conditional_access_policy_device_code_flow_blocked",
"entra_identity_protection_sign_in_risk_enabled",
"entra_managed_device_required_for_authentication",
"entra_seamless_sso_disabled",

View File

@@ -0,0 +1,41 @@
{
"Provider": "m365",
"CheckID": "entra_conditional_access_policy_device_code_flow_blocked",
"CheckTitle": "Conditional Access policy blocks device code flow to prevent phishing attacks",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Conditional Access policies restrict the **device code authentication flow**, which is commonly abused in phishing campaigns to hijack user sessions. A policy targeting `deviceCodeFlow` in authentication flow conditions with a block grant control prevents this attack vector.",
"Risk": "Device code flow is heavily exploited in phishing attacks such as **Storm-2372**, where attackers trick users into entering device codes on legitimate Microsoft login pages. Without a blocking policy, attackers can obtain tokens and gain persistent access to organizational resources.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-conditions#authentication-flows",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-authentication-flows"
],
"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**, include **All users** and exclude break-glass accounts.\n5. Under **Target resources**, include **All cloud apps**.\n6. Under **Conditions** > **Authentication flows**, select **Device code flow**.\n7. Under **Grant**, select **Block access**.\n8. Set the policy to **On** and click **Create**.",
"Terraform": ""
},
"Recommendation": {
"Text": "Block device code flow via Conditional Access to mitigate phishing attacks that abuse this authentication method. Exclude only break-glass accounts and legitimate service accounts that require device code flow. Regularly review exceptions to minimize the attack surface.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_device_code_flow_blocked"
}
},
"Categories": [
"identity-access",
"trust-boundaries",
"e3"
],
"DependsOn": [],
"RelatedTo": [
"entra_legacy_authentication_blocked"
],
"Notes": "The authenticationFlows condition in Conditional Access may require the beta Graph API endpoint and the Prefer: include-unknown-enum-members header."
}

View File

@@ -0,0 +1,78 @@
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,
ConditionalAccessPolicyState,
TransferMethod,
)
class entra_conditional_access_policy_device_code_flow_blocked(Check):
"""Check if at least one Conditional Access policy blocks device code flow.
This check ensures that at least one enabled Conditional Access policy
targets the device code authentication flow and blocks access, protecting
against phishing attacks that abuse this flow (e.g., Storm-2372).
- PASS: An enabled Conditional Access policy blocks device code flow.
- FAIL: No Conditional Access policy restricts device code flow.
"""
def execute(self) -> list[CheckReportM365]:
"""Execute the check to verify device code flow is blocked by a Conditional Access policy.
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 blocks device code flow."
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if "All" not in policy.conditions.user_conditions.included_users:
continue
if (
"All"
not in policy.conditions.application_conditions.included_applications
):
continue
if not policy.conditions.authentication_flows:
continue
if (
TransferMethod.DEVICE_CODE_FLOW
not in policy.conditions.authentication_flows.transfer_methods
):
continue
if (
ConditionalAccessGrantControl.BLOCK
in policy.grant_controls.built_in_controls
):
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 device code flow but does not block it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks device code flow."
break
findings.append(report)
return findings

View File

@@ -172,6 +172,18 @@ class Entra(M365Service):
conditional_access_policies_list = (
await self.client.identity.conditional_access.policies.get()
)
# TODO: Remove this workaround once microsoft/kiota-python#515 is
# fixed and a new version of microsoft-kiota-serialization-json is
# released (see PR microsoft/kiota-python#516). At that point, use
# the SDK's native deserialization for authentication_flows instead.
#
# The SDK deserializer uses get_collection_of_enum_values for
# transferMethods, but the Graph API returns it as a single string
# (e.g., "deviceCodeFlow"), causing the SDK to return an empty list.
# We fetch the raw JSON to correctly parse transferMethods.
raw_auth_flows_map = await self._get_raw_authentication_flows()
for policy in conditional_access_policies_list.value:
conditional_access_policies[policy.id] = ConditionalAccessPolicy(
id=policy.id,
@@ -301,6 +313,9 @@ class Entra(M365Service):
)
],
),
authentication_flows=self._parse_authentication_flows(
raw_auth_flows_map.get(policy.id)
),
),
grant_controls=GrantControls(
built_in_controls=(
@@ -446,6 +461,76 @@ class Entra(M365Service):
)
return default_app_management_policy
async def _get_raw_authentication_flows(self) -> dict:
"""Fetch authentication flows from the Graph API using a raw JSON request.
TODO: Remove this method once microsoft/kiota-python#515 is fixed and
a new version of microsoft-kiota-serialization-json is released
(see PR microsoft/kiota-python#516). At that point, revert to using
the SDK's native deserialization via policy.conditions.authentication_flows.
The SDK deserializer incorrectly handles the transferMethods field
(uses get_collection_of_enum_values for a single string value),
so we fetch the raw JSON to correctly parse it.
Returns:
A dict mapping policy ID to the raw authenticationFlows dict.
"""
auth_flows_map = {}
try:
request_info = (
self.client.identity.conditional_access.policies.to_get_request_information()
)
request_info.headers.try_add("Prefer", "include-unknown-enum-members")
response = await self.client.request_adapter.send_primitive_async(
request_info, "bytes", {}
)
if response:
data = json.loads(response)
for policy in data.get("value", []):
policy_id = policy.get("id")
auth_flows = policy.get("conditions", {}).get("authenticationFlows")
if policy_id and auth_flows:
auth_flows_map[policy_id] = auth_flows
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return auth_flows_map
@staticmethod
def _parse_authentication_flows(auth_flows) -> "AuthenticationFlows | None":
"""Parse authentication flows from a raw JSON dict.
TODO: Remove this method once microsoft/kiota-python#515 is fixed and
revert to parsing the SDK's ConditionalAccessAuthenticationFlows object
directly (see PR microsoft/kiota-python#516).
Args:
auth_flows: A dict from the raw JSON response (e.g., {"transferMethods": "deviceCodeFlow"}).
Returns:
AuthenticationFlows object or None if not present.
"""
if not auth_flows:
return None
transfer_methods = []
raw_value = auth_flows.get("transferMethods")
if raw_value:
# The API may return a single string or a comma-separated value
methods = raw_value.split(",") if isinstance(raw_value, str) else raw_value
for method_str in methods:
method_str = method_str.strip()
try:
transfer_methods.append(TransferMethod(method_str))
except ValueError:
logger.warning(
f"Unknown authentication flow transfer method: {method_str}"
)
return AuthenticationFlows(transfer_methods=transfer_methods)
@staticmethod
def _parse_app_management_restrictions(restrictions):
"""Parse credential restrictions from the Graph API response into AppManagementRestrictions."""
@@ -888,6 +973,19 @@ class PlatformConditions(BaseModel):
exclude_platforms: List[str] = []
class TransferMethod(Enum):
"""Transfer methods for authentication flows in Conditional Access policies."""
DEVICE_CODE_FLOW = "deviceCodeFlow"
AUTHENTICATION_TRANSFER = "authenticationTransfer"
class AuthenticationFlows(BaseModel):
"""Model representing authentication flows conditions in Conditional Access policies."""
transfer_methods: List[TransferMethod] = []
class Conditions(BaseModel):
application_conditions: Optional[ApplicationsConditions]
user_conditions: Optional[UsersConditions]
@@ -895,6 +993,7 @@ class Conditions(BaseModel):
user_risk_levels: List[RiskLevel] = []
sign_in_risk_levels: List[RiskLevel] = []
platform_conditions: Optional[PlatformConditions] = None
authentication_flows: Optional[AuthenticationFlows] = None
class PersistentBrowser(BaseModel):

View File

@@ -0,0 +1,882 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ApplicationEnforcedRestrictions,
ApplicationsConditions,
AuthenticationFlows,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
GrantControlOperator,
GrantControls,
PersistentBrowser,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
TransferMethod,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_entra_conditional_access_policy_device_code_flow_blocked:
def test_no_conditional_access_policies(self):
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
entra_client.conditional_access_policies = {}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_disabled(self):
policy_id = str(uuid4())
display_name = "Block Device Code Flow"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_no_authentication_flows(self):
policy_id = str(uuid4())
display_name = "Block Legacy Auth"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=[],
),
client_app_types=[],
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_enabled_for_reporting(self):
policy_id = str(uuid4())
display_name = "Block Device Code Flow"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' reports device code flow but does not block it."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[policy_id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
assert result[0].location == "global"
def test_policy_enabled_blocks_device_code_flow(self):
policy_id = str(uuid4())
display_name = "Block Device Code Flow"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' blocks device code flow."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[policy_id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == policy_id
assert result[0].location == "global"
def test_policy_different_transfer_method(self):
policy_id = str(uuid4())
display_name = "Block Auth Transfer"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.AUTHENTICATION_TRANSFER]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_multiple_policies_first_disabled_second_enabled(self):
disabled_id = str(uuid4())
enabled_id = str(uuid4())
enabled_name = "Block Device Code Flow - Enabled"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
disabled_id: ConditionalAccessPolicy(
id=disabled_id,
display_name="Block Device Code Flow - Disabled",
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.DISABLED,
),
enabled_id: ConditionalAccessPolicy(
id=enabled_id,
display_name=enabled_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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
),
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{enabled_name}' blocks device code flow."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[enabled_id].dict()
)
assert result[0].resource_name == enabled_name
assert result[0].resource_id == enabled_id
assert result[0].location == "global"
def test_policy_not_targeting_all_users(self):
policy_id = str(uuid4())
display_name = "Block Device Code Flow - Specific Group"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=["some-group-id"],
excluded_groups=[],
included_users=[],
excluded_users=[],
included_roles=[],
excluded_roles=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_not_targeting_all_cloud_apps(self):
policy_id = str(uuid4())
display_name = "Block Device Code Flow - Specific App"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
policy_id: ConditionalAccessPolicy(
id=policy_id,
display_name=display_name,
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"
def test_policy_with_device_code_flow_but_no_block(self):
policy_id = str(uuid4())
display_name = "MFA for Device Code Flow"
entra_client = mock.MagicMock
entra_client.audited_tenant = "audited_tenant"
entra_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_code_flow_blocked.entra_conditional_access_policy_device_code_flow_blocked import (
entra_conditional_access_policy_device_code_flow_blocked,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
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=[],
),
client_app_types=[],
user_risk_levels=[],
authentication_flows=AuthenticationFlows(
transfer_methods=[TransferMethod.DEVICE_CODE_FLOW]
),
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.MFA,
],
operator=GrantControlOperator.AND,
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=SignInFrequencyInterval.EVERY_TIME,
),
application_enforced_restrictions=ApplicationEnforcedRestrictions(
is_enabled=False
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_conditional_access_policy_device_code_flow_blocked()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks device code flow."
)
assert result[0].resource == {}
assert result[0].resource_name == "Conditional Access Policies"
assert result[0].resource_id == "conditionalAccessPolicies"
assert result[0].location == "global"