Compare commits

...

1 Commits

Author SHA1 Message Date
HugoPBrito
848d5708d4 feat(m365): add entra_policy_blocks_unknown_unsupported_device_platforms security check
Add new security check entra_policy_blocks_unknown_unsupported_device_platforms for m365 provider.
Includes check implementation, metadata, and unit tests.
2026-02-23 17:02:06 +01:00
11 changed files with 642 additions and 4 deletions

View File

@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `entra_policy_blocks_unknown_unsupported_device_platforms` check for m365 provider [(#10138)](https://github.com/prowler-cloud/prowler/pull/10138)
- `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)

View File

@@ -1325,7 +1325,8 @@
"Id": "5.2.2.10",
"Description": "Conditional Access (CA) can be configured to enforce access based on the device's compliance status or whether it is Entra hybrid joined. Collectively this allows CA to classify devices as managed or unmanaged, providing more granular control over authentication policies.When using `Require device to be marked as compliant`, the device must pass checks configured in **Compliance** policies defined within Intune (Endpoint Manager). Before these checks can be applied, the device must first be enrolled in Intune MDM.By selecting `Require Microsoft Entra hybrid joined device` this means the device must first be synchronized from an on-premises Active Directory to qualify for authentication.When configured to the recommended state below only one condition needs to be met for the user to authenticate from the device. This functions as an \"OR\" operator.The recommended state is:- `Require device to be marked as compliant`- `Require Microsoft Entra hybrid joined device`- `Require one of the selected controls`",
"Checks": [
"entra_managed_device_required_for_authentication"
"entra_managed_device_required_for_authentication",
"entra_policy_blocks_unknown_unsupported_device_platforms"
],
"Attributes": [
{

View File

@@ -1534,7 +1534,8 @@
"Id": "5.2.2.9",
"Description": "Conditional Access (CA) can be configured to enforce access based on the device's compliance status or whether it is Entra hybrid joined. Ensure a managed device is required for authentication.",
"Checks": [
"entra_managed_device_required_for_authentication"
"entra_managed_device_required_for_authentication",
"entra_policy_blocks_unknown_unsupported_device_platforms"
],
"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_policy_blocks_unknown_unsupported_device_platforms",
"entra_policy_guest_users_access_restrictions",
"entra_seamless_sso_disabled"
]
@@ -613,8 +614,9 @@
"Checks": [
"defenderxdr_endpoint_privileged_user_exposed_credentials",
"entra_managed_device_required_for_authentication",
"entra_users_mfa_enabled",
"entra_managed_device_required_for_mfa_registration",
"entra_policy_blocks_unknown_unsupported_device_platforms",
"entra_users_mfa_enabled",
"entra_admin_users_phishing_resistant_mfa_enabled",
"entra_users_mfa_capable"
]
@@ -686,6 +688,7 @@
"entra_admin_users_sign_in_frequency_enabled",
"entra_admin_users_mfa_enabled",
"entra_managed_device_required_for_authentication",
"entra_policy_blocks_unknown_unsupported_device_platforms",
"entra_seamless_sso_disabled",
"entra_users_mfa_enabled",
"entra_identity_protection_sign_in_risk_enabled"

View File

@@ -99,7 +99,8 @@
"Id": "1.1.6",
"Description": "Ensure a managed device is required for authentication",
"Checks": [
"entra_managed_device_required_for_authentication"
"entra_managed_device_required_for_authentication",
"entra_policy_blocks_unknown_unsupported_device_platforms"
],
"Attributes": [
{

View File

@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "entra_policy_blocks_unknown_unsupported_device_platforms",
"CheckTitle": "Conditional Access policy blocks access from unknown or unsupported device platforms",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Conditional Access Policy",
"ResourceGroup": "IAM",
"Description": "This check verifies whether at least one Conditional Access policy is configured to **block access** from unknown or unsupported device platforms.\n\nA qualifying policy must target all platforms and enforce a block grant control, ensuring that devices running unrecognized operating systems cannot authenticate.",
"Risk": "Without blocking unknown or unsupported device platforms, attackers may use **unmanaged or unrecognized devices** to bypass security controls.\n\nThese devices may lack required security configurations, endpoint protection, or compliance policies, increasing the risk of **unauthorized access** and **data exfiltration**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-policy-unknown-unsupported-device",
"https://maester.dev/docs/tests/MT.1015"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to **Protection** > **Conditional Access** > **Policies**\n3. Click **New policy**\n4. Under **Users**, include **All users**\n5. Under **Target resources**, include **All cloud apps**\n6. Under **Conditions** > **Device platforms**, configure **Include** to select **Any device**\n7. Under **Conditions** > **Device platforms**, configure **Exclude** to select the known platforms (Android, iOS, Windows, macOS, Linux)\n8. Under **Grant**, select **Block access**\n9. Set the policy to **On** and click **Create**",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a Conditional Access policy that blocks access from unknown or unsupported device platforms. Include all platforms and exclude only the known, supported ones (Android, iOS, Windows, macOS, Linux) to ensure unrecognized devices cannot authenticate.",
"Url": "https://hub.prowler.com/check/entra_policy_blocks_unknown_unsupported_device_platforms"
}
},
"Categories": [
"e3"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Equivalent to Maester test MT.1015 (Test-MtCaBlockUnknownOrUnsupportedDevicePlatform)."
}

View File

@@ -0,0 +1,68 @@
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,
DevicePlatform,
)
class entra_policy_blocks_unknown_unsupported_device_platforms(Check):
"""Check if at least one Conditional Access policy blocks unknown or unsupported device platforms.
This check evaluates whether the tenant has a Conditional Access policy that
blocks access for unknown or unsupported device platforms by requiring all
platforms to be included and blocking access.
- PASS: At least one enabled policy blocks access for all platforms (covering unknown/unsupported).
- FAIL: No enabled policy blocks unknown or unsupported device platforms.
"""
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 blocks unknown or unsupported device platforms."
for policy in entra_client.conditional_access_policies.values():
if policy.state == ConditionalAccessPolicyState.DISABLED:
continue
if not policy.conditions.platform_conditions:
continue
if (
DevicePlatform.ALL
not in policy.conditions.platform_conditions.included_platforms
):
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 blocking unknown or unsupported device platforms but does not enforce it."
else:
report.status = "PASS"
report.status_extended = f"Conditional Access Policy '{policy.display_name}' blocks unknown or unsupported device platforms."
break
findings.append(report)
return findings

View File

@@ -270,6 +270,30 @@ class Entra(M365Service):
[],
)
],
platform_conditions=(
PlatformConditions(
included_platforms=[
DevicePlatform(platform)
for platform in getattr(
policy.conditions.platforms,
"include_platforms",
[],
)
or []
],
excluded_platforms=[
DevicePlatform(platform)
for platform in getattr(
policy.conditions.platforms,
"exclude_platforms",
[],
)
or []
],
)
if getattr(policy.conditions, "platforms", None)
else None
),
),
grant_controls=GrantControls(
built_in_controls=(
@@ -705,10 +729,31 @@ class ClientAppType(Enum):
OTHER_CLIENTS = "other"
class DevicePlatform(Enum):
"""Device platform types for Conditional Access policies."""
ANDROID = "android"
IOS = "iOS"
WINDOWS = "windows"
WINDOWS_PHONE = "windowsPhone"
MACOS = "macOS"
LINUX = "linux"
ALL = "all"
UNKNOWN_FUTURE_VALUE = "unknownFutureValue"
class PlatformConditions(BaseModel):
"""Platform conditions for Conditional Access policies."""
included_platforms: List[DevicePlatform] = []
excluded_platforms: List[DevicePlatform] = []
class Conditions(BaseModel):
application_conditions: Optional[ApplicationsConditions]
user_conditions: Optional[UsersConditions]
client_app_types: Optional[List[ClientAppType]]
platform_conditions: Optional[PlatformConditions]
user_risk_levels: List[RiskLevel] = []
sign_in_risk_levels: List[RiskLevel] = []

View File

@@ -0,0 +1,481 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.m365.services.entra.entra_service import (
ApplicationsConditions,
ConditionalAccessGrantControl,
ConditionalAccessPolicyState,
Conditions,
DevicePlatform,
GrantControlOperator,
GrantControls,
PersistentBrowser,
PlatformConditions,
SessionControls,
SignInFrequency,
SignInFrequencyInterval,
UsersConditions,
)
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
class Test_entra_policy_blocks_unknown_unsupported_device_platforms:
def test_entra_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_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms import (
entra_policy_blocks_unknown_unsupported_device_platforms,
)
entra_client.conditional_access_policies = {}
check = entra_policy_blocks_unknown_unsupported_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks unknown or unsupported device platforms."
)
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_entra_policy_disabled(self):
id = str(uuid4())
display_name = "Block Unknown Platforms"
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_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms import (
entra_policy_blocks_unknown_unsupported_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
platform_conditions=PlatformConditions(
included_platforms=[DevicePlatform.ALL],
excluded_platforms=[],
),
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.DISABLED,
)
}
check = entra_policy_blocks_unknown_unsupported_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks unknown or unsupported device platforms."
)
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_entra_policy_no_platform_conditions(self):
id = str(uuid4())
display_name = "Block Policy Without Platforms"
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_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms import (
entra_policy_blocks_unknown_unsupported_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
platform_conditions=None,
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_policy_blocks_unknown_unsupported_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks unknown or unsupported device platforms."
)
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_entra_policy_platforms_not_all(self):
id = str(uuid4())
display_name = "Block Specific Platforms Only"
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_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms import (
entra_policy_blocks_unknown_unsupported_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
platform_conditions=PlatformConditions(
included_platforms=[
DevicePlatform.ANDROID,
DevicePlatform.IOS,
],
excluded_platforms=[],
),
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_policy_blocks_unknown_unsupported_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "No Conditional Access Policy blocks unknown or unsupported device platforms."
)
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_entra_policy_enabled_for_reporting(self):
id = str(uuid4())
display_name = "Block Unknown Platforms (Report Only)"
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_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms import (
entra_policy_blocks_unknown_unsupported_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
platform_conditions=PlatformConditions(
included_platforms=[DevicePlatform.ALL],
excluded_platforms=[],
),
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING,
)
}
check = entra_policy_blocks_unknown_unsupported_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' reports blocking unknown or unsupported device platforms but does not enforce it."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"
def test_entra_policy_enabled_blocks_unknown_platforms(self):
id = str(uuid4())
display_name = "Block Unknown Platforms"
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_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms.entra_client",
new=entra_client,
),
):
from prowler.providers.m365.services.entra.entra_policy_blocks_unknown_unsupported_device_platforms.entra_policy_blocks_unknown_unsupported_device_platforms import (
entra_policy_blocks_unknown_unsupported_device_platforms,
)
from prowler.providers.m365.services.entra.entra_service import (
ConditionalAccessPolicy,
)
entra_client.conditional_access_policies = {
id: ConditionalAccessPolicy(
id=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=[],
platform_conditions=PlatformConditions(
included_platforms=[DevicePlatform.ALL],
excluded_platforms=[
DevicePlatform.ANDROID,
DevicePlatform.IOS,
DevicePlatform.WINDOWS,
DevicePlatform.MACOS,
DevicePlatform.LINUX,
],
),
user_risk_levels=[],
),
grant_controls=GrantControls(
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
],
operator=GrantControlOperator.AND,
),
session_controls=SessionControls(
persistent_browser=PersistentBrowser(
is_enabled=False, mode="always"
),
sign_in_frequency=SignInFrequency(
is_enabled=False,
frequency=None,
type=None,
interval=SignInFrequencyInterval.EVERY_TIME,
),
),
state=ConditionalAccessPolicyState.ENABLED,
)
}
check = entra_policy_blocks_unknown_unsupported_device_platforms()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Conditional Access Policy '{display_name}' blocks unknown or unsupported device platforms."
)
assert (
result[0].resource
== entra_client.conditional_access_policies[id].dict()
)
assert result[0].resource_name == display_name
assert result[0].resource_id == id
assert result[0].location == "global"