mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
feat(intune): add device compliance policy marks noncompliant check (#10599)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
+36
@@ -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."
|
||||
}
|
||||
+52
@@ -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:
|
||||
|
||||
+163
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user