feat(intune): add device compliance policy marks noncompliant check (#10599)

This commit is contained in:
Hugo Pereira Brito
2026-04-14 13:01:47 +02:00
committed by GitHub
parent 4941ed5797
commit bc3fd79457
8 changed files with 319 additions and 4 deletions
+1
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### 🚀 Added
- `intune_device_compliance_policy_unassigned_devices_not_compliant_by_default` check for M365 provider [(#10599)](https://github.com/prowler-cloud/prowler/pull/10599)
- `entra_conditional_access_policy_all_apps_all_users` check for M365 provider [(#10619)](https://github.com/prowler-cloud/prowler/pull/10619)
- `bedrock_full_access_policy_attached` check for AWS provider [(#10577)](https://github.com/prowler-cloud/prowler/pull/10577)
- `iam_role_access_not_stale_to_bedrock` and `iam_user_access_not_stale_to_bedrock` checks for AWS provider [(#10536)](https://github.com/prowler-cloud/prowler/pull/10536)
+3 -1
View File
@@ -873,7 +873,9 @@
{
"Id": "4.1",
"Description": "Compliance policies are sets of rules and conditions that are used to evaluate the configuration of managed devices. These policies can help secure organizational data and resources from devices that don't meet those configuration requirements. The recommended state is Mark devices with no compliance policy assigned as Not compliant.",
"Checks": [],
"Checks": [
"intune_device_compliance_policy_unassigned_devices_not_compliant_by_default"
],
"Attributes": [
{
"Section": "4 Microsoft Intune admin center",
@@ -0,0 +1,36 @@
{
"Provider": "m365",
"CheckID": "intune_device_compliance_policy_unassigned_devices_not_compliant_by_default",
"CheckTitle": "Built-in Device Compliance Policy marks devices without an assigned compliance policy as Not compliant by default",
"CheckType": [],
"ServiceName": "intune",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "security",
"Description": "Intune has a built-in Device Compliance Policy that governs how devices without an explicit compliance policy are treated. When the default behavior marks those devices as Compliant, unmanaged devices can be treated as compliant and gain access to corporate resources. This check verifies the default is set to Not compliant (secureByDefault = true).",
"Risk": "If the built-in policy marks devices without a compliance policy as Compliant, those devices can bypass Conditional Access policies requiring device compliance, granting unauthorized access to corporate resources from unmanaged or non-compliant endpoints.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/graph/api/resources/intune-deviceconfig-devicemanagementsettings?view=graph-rest-1.0"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Microsoft Intune admin center (intune.microsoft.com)\n2. Go to Devices > Compliance\n3. Select Compliance policy settings\n4. Set 'Mark devices with no compliance policy assigned as' to 'Not compliant'\n5. Save the settings",
"Terraform": ""
},
"Recommendation": {
"Text": "Set the built-in Device Compliance Policy default so devices with no compliance policy assigned are marked as Not compliant.",
"Url": "https://hub.prowler.com/check/intune_device_compliance_policy_unassigned_devices_not_compliant_by_default"
}
},
"Categories": [
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "The check evaluates the secureByDefault property from the deviceManagement/settings Microsoft Graph endpoint."
}
@@ -0,0 +1,52 @@
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.intune.intune_client import intune_client
class intune_device_compliance_policy_unassigned_devices_not_compliant_by_default(
Check
):
"""Ensure the built-in Device Compliance Policy marks unassigned devices as Not compliant by default."""
def execute(self) -> list[CheckReportM365]:
findings = []
report = CheckReportM365(
metadata=self.metadata(),
resource=intune_client.settings or {},
resource_name="Intune Device Compliance Settings",
resource_id="deviceManagement/settings",
)
verification_error = getattr(intune_client, "verification_error", None)
settings = getattr(intune_client, "settings", None)
secure_by_default = getattr(settings, "secure_by_default", None)
if verification_error:
report.status = "MANUAL"
report.status_extended = (
"Intune built-in Device Compliance Policy could not be verified. "
f"{verification_error}"
)
elif settings is None or secure_by_default is None:
report.status = "MANUAL"
report.status_extended = (
"Intune built-in Device Compliance Policy could not be verified "
"because Microsoft Graph did not return the secure-by-default "
"compliance setting."
)
elif secure_by_default is True:
report.status = "PASS"
report.status_extended = (
"Intune built-in Device Compliance Policy marks devices "
"with no compliance policy assigned as Not compliant."
)
else:
report.status = "FAIL"
report.status_extended = (
"Intune built-in Device Compliance Policy marks devices "
"with no compliance policy assigned as Compliant. "
"Change the default to Not compliant in Intune settings."
)
findings.append(report)
return findings
@@ -125,6 +125,15 @@ class Intune(M365Service):
request_configuration=request_configuration
)
settings = getattr(device_management, "settings", None)
secure_by_default = getattr(settings, "secure_by_default", None)
# Some tenants/API responses omit nested settings when $select is used.
# Retry without query parameters before concluding the value is unavailable.
if settings is None or secure_by_default is None:
device_management = await self.client.device_management.get()
settings = getattr(device_management, "settings", None)
secure_by_default = getattr(settings, "secure_by_default", None)
if settings is None:
return (
IntuneSettings(secure_by_default=None),
@@ -132,9 +141,7 @@ class Intune(M365Service):
)
return (
IntuneSettings(
secure_by_default=getattr(settings, "secure_by_default", None)
),
IntuneSettings(secure_by_default=secure_by_default),
None,
)
except Exception as error:
@@ -0,0 +1,163 @@
from unittest import mock
from prowler.providers.m365.services.intune.intune_service import IntuneSettings
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
CHECK_MODULE_PATH = "prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default"
class Test_intune_device_compliance_policy_unassigned_devices_not_compliant_by_default:
def test_secure_by_default_true(self):
intune_client = mock.MagicMock()
intune_client.audited_tenant = "audited_tenant"
intune_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}.intune_client", new=intune_client),
):
from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default,
)
intune_client.settings = IntuneSettings(secure_by_default=True)
intune_client.verification_error = None
result = (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute()
)
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == (
"Intune built-in Device Compliance Policy marks devices "
"with no compliance policy assigned as Not compliant."
)
def test_secure_by_default_false(self):
intune_client = mock.MagicMock()
intune_client.audited_tenant = "audited_tenant"
intune_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}.intune_client", new=intune_client),
):
from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default,
)
intune_client.settings = IntuneSettings(secure_by_default=False)
intune_client.verification_error = None
result = (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute()
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == (
"Intune built-in Device Compliance Policy marks devices "
"with no compliance policy assigned as Compliant. "
"Change the default to Not compliant in Intune settings."
)
def test_secure_by_default_none(self):
intune_client = mock.MagicMock()
intune_client.audited_tenant = "audited_tenant"
intune_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}.intune_client", new=intune_client),
):
from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default,
)
intune_client.settings = IntuneSettings(secure_by_default=None)
intune_client.verification_error = None
result = (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute()
)
assert len(result) == 1
assert result[0].status == "MANUAL"
assert result[0].status_extended == (
"Intune built-in Device Compliance Policy could not be verified "
"because Microsoft Graph did not return the secure-by-default "
"compliance setting."
)
def test_settings_is_none(self):
intune_client = mock.MagicMock()
intune_client.audited_tenant = "audited_tenant"
intune_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}.intune_client", new=intune_client),
):
from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default,
)
intune_client.settings = None
intune_client.verification_error = None
result = (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute()
)
assert len(result) == 1
assert result[0].status == "MANUAL"
assert result[0].status_extended == (
"Intune built-in Device Compliance Policy could not be verified "
"because Microsoft Graph did not return the secure-by-default "
"compliance setting."
)
def test_verification_error_returns_manual(self):
intune_client = mock.MagicMock()
intune_client.audited_tenant = "audited_tenant"
intune_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}.intune_client", new=intune_client),
):
from prowler.providers.m365.services.intune.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default.intune_device_compliance_policy_unassigned_devices_not_compliant_by_default import (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default,
)
intune_client.settings = None
intune_client.verification_error = (
"Could not read Microsoft Intune device management settings."
)
result = (
intune_device_compliance_policy_unassigned_devices_not_compliant_by_default().execute()
)
assert len(result) == 1
assert result[0].status == "MANUAL"
assert result[0].status_extended == (
"Intune built-in Device Compliance Policy could not be verified. "
"Could not read Microsoft Intune device management settings."
)
@@ -349,6 +349,60 @@ def test_intune_get_settings_null_settings():
assert settings.secure_by_default is None
def test_intune_get_settings_retries_without_select_when_settings_missing():
"""Test _get_settings retries without $select when settings are omitted."""
intune = Intune.__new__(Intune)
selected_response = SimpleNamespace(settings=None)
full_response = SimpleNamespace(settings=SimpleNamespace(secure_by_default=True))
mock_client = mock.MagicMock()
mock_client.device_management.get = AsyncMock(
side_effect=[selected_response, full_response]
)
intune.client = mock_client
loop = asyncio.new_event_loop()
try:
settings, error = loop.run_until_complete(intune._get_settings())
finally:
loop.close()
assert error is None
assert settings is not None
assert settings.secure_by_default is True
assert mock_client.device_management.get.await_count == 2
def test_intune_get_settings_retries_without_select_when_value_missing():
"""Test _get_settings retries without $select when secure_by_default is omitted."""
intune = Intune.__new__(Intune)
selected_response = SimpleNamespace(
settings=SimpleNamespace(secure_by_default=None)
)
full_response = SimpleNamespace(settings=SimpleNamespace(secure_by_default=False))
mock_client = mock.MagicMock()
mock_client.device_management.get = AsyncMock(
side_effect=[selected_response, full_response]
)
intune.client = mock_client
loop = asyncio.new_event_loop()
try:
settings, error = loop.run_until_complete(intune._get_settings())
finally:
loop.close()
assert error is None
assert settings is not None
assert settings.secure_by_default is False
assert mock_client.device_management.get.await_count == 2
def test_intune_get_settings_exception():
"""Test _get_settings handles exceptions gracefully."""
intune = Intune.__new__(Intune)