Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions prowler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `entra_conditional_access_policy_device_registration_mfa_required` check and `entra_intune_enrollment_sign_in_frequency_every_time` enhancement for M365 provider [(#10222)](https://github.com/prowler-cloud/prowler/pull/10222)
- `entra_conditional_access_policy_block_elevated_insider_risk` check for M365 provider [(#10234)](https://github.com/prowler-cloud/prowler/pull/10234)
- `Vercel` provider support with 30 checks [(#10189)](https://github.com/prowler-cloud/prowler/pull/10189)
- `intune_device_compliance_policy_unassigned_devices_not_compliant_by_default` check for M365 provider [(#10599)](https://github.com/prowler-cloud/prowler/pull/10599)

### 🔄 Changed

Expand Down
4 changes: 3 additions & 1 deletion prowler/compliance/m365/cis_6.0_m365.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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."
}
Original file line number Diff line number Diff line change
@@ -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
13 changes: 10 additions & 3 deletions prowler/providers/m365/services/intune/intune_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,23 @@ async def _get_settings(self) -> tuple[Optional["IntuneSettings"], Optional[str]
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),
None,
)

return (
IntuneSettings(
secure_by_default=getattr(settings, "secure_by_default", None)
),
IntuneSettings(secure_by_default=secure_by_default),
None,
)
except Exception as error:
Expand Down
Original file line number Diff line number Diff line change
@@ -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."
)
54 changes: 54 additions & 0 deletions tests/providers/m365/services/intune/intune_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading