mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-24 12:24:17 +00:00
feat(m365): add CIS M365 v7.0.0 intune enrollment check
This commit is contained in:
8 files changed
+386
-2
No files matched your search
@@ -0,0 +1 @@
|
||||
1 M365 intune check covering CIS Microsoft 365 Foundations Benchmark v7.0.0 control 4.2
|
||||
@@ -1005,7 +1005,9 @@
|
||||
{
|
||||
"Id": "4.2",
|
||||
"Description": "Device enrollment restrictions let you restrict devices from enrolling in Intune based on certain device attributes such as device limit, device platform, OS Version, manufacturer or device ownership (Personally owned devices). The recommended state is to Block personally owned devices from enrollment.",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
"intune_device_enrollment_personal_device_restricted"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "4 Microsoft Intune admin center",
|
||||
|
||||
Whitespace-only changes.
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "intune_device_enrollment_personal_device_restricted",
|
||||
"CheckTitle": "Default device enrollment restriction blocks personally owned devices",
|
||||
"CheckType": [],
|
||||
"ServiceName": "intune",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "The default (priority 0) Intune device platform restriction configuration should block **personally owned** devices for all platforms (**personalDeviceEnrollmentBlocked** set to true). This ensures only corporate-owned or otherwise managed devices can enroll into Intune.",
|
||||
"Risk": "Allowing personally owned devices to enroll gives unmanaged, potentially non-compliant endpoints access to corporate resources, increasing the risk of data leakage and compromise from devices outside the organization's control.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/mem/intune/enrollment/enrollment-restrictions-set"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to the Microsoft Intune admin center at https://intune.microsoft.com/\n2. Go to **Devices** > **Device onboarding** > **Enrollment** > **Device platform restriction**\n3. Open the **Default** priority policy > **All Users** > **Properties**\n4. Set every platform to **Block** in the **Personally owned** column\n5. Save the changes",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Block personally owned device enrollment for all platforms in the default enrollment platform restriction so only corporate-owned devices can enroll into Intune.",
|
||||
"Url": "https://hub.prowler.com/check/intune_device_enrollment_personal_device_restricted"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access",
|
||||
"e3"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.intune.intune_client import intune_client
|
||||
|
||||
|
||||
class intune_device_enrollment_personal_device_restricted(Check):
|
||||
"""Check if the default device enrollment restriction blocks personal devices.
|
||||
|
||||
The default (priority 0) device platform restriction configuration should block
|
||||
personally owned devices for all platforms
|
||||
(``personalDeviceEnrollmentBlocked`` set to true).
|
||||
|
||||
- PASS: The default platform restriction blocks personal devices for all
|
||||
platforms.
|
||||
- FAIL: The default platform restriction allows personal devices for one or more
|
||||
platforms.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[CheckReportM365]:
|
||||
"""Evaluate whether the default enrollment restriction blocks personal devices.
|
||||
|
||||
Reads the default (priority 0) device platform restriction configuration and
|
||||
reports PASS when personally owned devices are blocked for all platforms, and
|
||||
FAIL when they are allowed for one or more platforms.
|
||||
|
||||
Returns:
|
||||
List[CheckReportM365]: The findings for the check.
|
||||
"""
|
||||
findings = []
|
||||
configurations = intune_client.device_enrollment_configurations
|
||||
|
||||
default_config = next(
|
||||
(config for config in configurations if config.priority == 0),
|
||||
None,
|
||||
)
|
||||
if not default_config:
|
||||
return findings
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=default_config,
|
||||
resource_name="Default Device Enrollment Platform Restrictions",
|
||||
resource_id=default_config.id,
|
||||
)
|
||||
|
||||
restrictions = default_config.platform_restrictions
|
||||
if restrictions and all(restrictions.values()):
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"The default device enrollment restriction blocks personally owned "
|
||||
"devices for all platforms."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"The default device enrollment restriction allows personally owned "
|
||||
"devices for one or more platforms."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
|
||||
from kiota_abstractions.base_request_configuration import RequestConfiguration
|
||||
from pydantic.v1 import BaseModel
|
||||
@@ -29,6 +30,9 @@ class Intune(M365Service):
|
||||
self.settings: Optional[IntuneSettings] = None
|
||||
self.compliance_policies: Optional[list[IntuneCompliancePolicy]] = []
|
||||
self.managed_devices: Optional[list[IntuneManagedDevice]] = []
|
||||
self.device_enrollment_configurations: list["DeviceEnrollmentConfiguration"] = (
|
||||
[]
|
||||
)
|
||||
self.verification_error: Optional[str] = None
|
||||
|
||||
loop = self._get_event_loop()
|
||||
@@ -44,6 +48,9 @@ class Intune(M365Service):
|
||||
self.settings = settings
|
||||
self.compliance_policies = policies
|
||||
self.managed_devices = managed_devices
|
||||
self.device_enrollment_configurations = loop.run_until_complete(
|
||||
self._get_device_enrollment_configurations()
|
||||
)
|
||||
self.verification_error = (
|
||||
" ".join(
|
||||
error
|
||||
@@ -82,6 +89,72 @@ class Intune(M365Service):
|
||||
except Exception as error:
|
||||
logger.debug(f"Intune - Failed to clean up event loop: {error}")
|
||||
|
||||
async def _get_device_enrollment_configurations(
|
||||
self,
|
||||
) -> "list[DeviceEnrollmentConfiguration]":
|
||||
"""Retrieve device enrollment configurations (platform restrictions).
|
||||
|
||||
Fetches ``deviceManagement/deviceEnrollmentConfigurations`` as raw JSON and
|
||||
parses the platform-restriction configurations, capturing each platform's
|
||||
``personalDeviceEnrollmentBlocked`` flag.
|
||||
|
||||
Returns:
|
||||
list[DeviceEnrollmentConfiguration]: The parsed enrollment configurations.
|
||||
"""
|
||||
logger.info("M365 - Getting Intune device enrollment configurations...")
|
||||
configurations: list[DeviceEnrollmentConfiguration] = []
|
||||
try:
|
||||
# The beta endpoint exposes the full set of per-platform restrictions
|
||||
# (including macOS/Android for Work) that v1.0 may omit.
|
||||
next_link = (
|
||||
"https://graph.microsoft.com/beta/deviceManagement/"
|
||||
"deviceEnrollmentConfigurations"
|
||||
)
|
||||
while next_link:
|
||||
builder = self.client.device_management.device_enrollment_configurations.with_url(
|
||||
next_link
|
||||
)
|
||||
request_info = builder.to_get_request_information()
|
||||
response = await self.client.request_adapter.send_primitive_async(
|
||||
request_info, "bytes", {}
|
||||
)
|
||||
if not response:
|
||||
break
|
||||
data = json.loads(response)
|
||||
for config in data.get("value", []) or []:
|
||||
odata_type = config.get("@odata.type", "")
|
||||
if "PlatformRestrictionsConfiguration" not in odata_type:
|
||||
continue
|
||||
platform_restrictions = {}
|
||||
for key, value in config.items():
|
||||
if (
|
||||
key.endswith("Restriction")
|
||||
and isinstance(value, dict)
|
||||
and "personalDeviceEnrollmentBlocked" in value
|
||||
):
|
||||
# A platform is compliant when personal enrollment is
|
||||
# blocked OR the platform itself is fully blocked (CIS:
|
||||
# a blocked platform is also a passing state).
|
||||
platform_restrictions[key] = bool(
|
||||
value.get("personalDeviceEnrollmentBlocked", False)
|
||||
) or bool(value.get("platformBlocked", False))
|
||||
configurations.append(
|
||||
DeviceEnrollmentConfiguration(
|
||||
id=config.get("id", ""),
|
||||
priority=config.get("priority", 0),
|
||||
odata_type=odata_type,
|
||||
platform_restrictions=platform_restrictions,
|
||||
)
|
||||
)
|
||||
|
||||
# Follow pagination using the opaque nextLink until exhausted.
|
||||
next_link = data.get("@odata.nextLink") or data.get("nextLink")
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return configurations
|
||||
|
||||
async def _load_intune_configuration(
|
||||
self,
|
||||
) -> tuple[
|
||||
@@ -320,3 +393,22 @@ class IntuneManagedDevice(BaseModel):
|
||||
device_name: str = ""
|
||||
compliance_state: str = ""
|
||||
management_agent: str = ""
|
||||
|
||||
|
||||
class DeviceEnrollmentConfiguration(BaseModel):
|
||||
"""Represents an Intune device enrollment (platform restrictions) configuration.
|
||||
|
||||
Attributes:
|
||||
id: The device enrollment configuration identifier.
|
||||
priority: The configuration priority (0 is the default configuration).
|
||||
odata_type: The Graph ``@odata.type`` of the configuration.
|
||||
platform_restrictions: Mapping of platform restriction name to a boolean
|
||||
indicating whether personal-device enrollment is blocked (or the
|
||||
platform is fully blocked) for that platform.
|
||||
"""
|
||||
|
||||
id: str
|
||||
priority: int = 0
|
||||
odata_type: Optional[str] = None
|
||||
# Mapping of platform restriction name -> personalDeviceEnrollmentBlocked flag.
|
||||
platform_restrictions: Dict[str, bool] = {}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.m365.services.intune.intune_service import (
|
||||
DeviceEnrollmentConfiguration,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
|
||||
|
||||
CHECK_MODULE_PATH = "prowler.providers.m365.services.intune.intune_device_enrollment_personal_device_restricted.intune_device_enrollment_personal_device_restricted"
|
||||
|
||||
|
||||
class Test_intune_device_enrollment_personal_device_restricted:
|
||||
def _run(self, configurations):
|
||||
intune_client = mock.MagicMock()
|
||||
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_enrollment_personal_device_restricted.intune_device_enrollment_personal_device_restricted import (
|
||||
intune_device_enrollment_personal_device_restricted,
|
||||
)
|
||||
|
||||
intune_client.device_enrollment_configurations = configurations
|
||||
return intune_device_enrollment_personal_device_restricted().execute()
|
||||
|
||||
def test_no_default_config(self):
|
||||
assert self._run([]) == []
|
||||
|
||||
def test_all_blocked(self):
|
||||
result = self._run(
|
||||
[
|
||||
DeviceEnrollmentConfiguration(
|
||||
id="cfg",
|
||||
priority=0,
|
||||
platform_restrictions={
|
||||
"iosRestriction": True,
|
||||
"androidRestriction": True,
|
||||
"windowsRestriction": True,
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_one_allowed(self):
|
||||
result = self._run(
|
||||
[
|
||||
DeviceEnrollmentConfiguration(
|
||||
id="cfg",
|
||||
priority=0,
|
||||
platform_restrictions={
|
||||
"iosRestriction": True,
|
||||
"androidRestriction": False,
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
@@ -96,6 +96,134 @@ def _build_intune_service(
|
||||
return Intune(set_mocked_m365_provider())
|
||||
|
||||
|
||||
class Test_Intune_DeviceEnrollmentConfigurations:
|
||||
def _run_getter(self, payload):
|
||||
import json
|
||||
|
||||
service = Intune.__new__(Intune)
|
||||
client = mock.MagicMock()
|
||||
builder = (
|
||||
client.device_management.device_enrollment_configurations.with_url.return_value
|
||||
)
|
||||
builder.to_get_request_information.return_value = mock.MagicMock()
|
||||
client.request_adapter.send_primitive_async = AsyncMock(
|
||||
return_value=json.dumps(payload).encode()
|
||||
)
|
||||
service.client = client
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
service._get_device_enrollment_configurations()
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
def test_parses_platform_restrictions_and_platform_blocked(self):
|
||||
payload = {
|
||||
"value": [
|
||||
{
|
||||
"@odata.type": "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration",
|
||||
"id": "default",
|
||||
"priority": 0,
|
||||
"iosRestriction": {
|
||||
"personalDeviceEnrollmentBlocked": True,
|
||||
"platformBlocked": False,
|
||||
},
|
||||
"androidRestriction": {
|
||||
"personalDeviceEnrollmentBlocked": False,
|
||||
"platformBlocked": True,
|
||||
},
|
||||
"windowsRestriction": {
|
||||
"personalDeviceEnrollmentBlocked": False,
|
||||
"platformBlocked": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"@odata.type": "#microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration",
|
||||
"id": "whfb",
|
||||
"priority": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
result = self._run_getter(payload)
|
||||
# Only the platform-restrictions config is captured (WHfB ignored).
|
||||
assert len(result) == 1
|
||||
config = result[0]
|
||||
assert config.priority == 0
|
||||
# ios: personal blocked -> compliant; android: platformBlocked -> compliant;
|
||||
# windows: neither -> not compliant.
|
||||
assert config.platform_restrictions == {
|
||||
"iosRestriction": True,
|
||||
"androidRestriction": True,
|
||||
"windowsRestriction": False,
|
||||
}
|
||||
|
||||
def test_empty_response(self):
|
||||
assert self._run_getter({"value": []}) == []
|
||||
|
||||
def test_follows_pagination_across_pages(self):
|
||||
import json
|
||||
|
||||
first_page = {
|
||||
"value": [
|
||||
{
|
||||
"@odata.type": "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration",
|
||||
"id": "page-one",
|
||||
"priority": 0,
|
||||
"iosRestriction": {
|
||||
"personalDeviceEnrollmentBlocked": True,
|
||||
"platformBlocked": False,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@odata.nextLink": "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?$skiptoken=abc",
|
||||
}
|
||||
second_page = {
|
||||
"value": [
|
||||
{
|
||||
"@odata.type": "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration",
|
||||
"id": "page-two",
|
||||
"priority": 1,
|
||||
"androidRestriction": {
|
||||
"personalDeviceEnrollmentBlocked": False,
|
||||
"platformBlocked": True,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
service = Intune.__new__(Intune)
|
||||
client = mock.MagicMock()
|
||||
builder = (
|
||||
client.device_management.device_enrollment_configurations.with_url.return_value
|
||||
)
|
||||
builder.to_get_request_information.return_value = mock.MagicMock()
|
||||
client.request_adapter.send_primitive_async = AsyncMock(
|
||||
side_effect=[
|
||||
json.dumps(first_page).encode(),
|
||||
json.dumps(second_page).encode(),
|
||||
]
|
||||
)
|
||||
service.client = client
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
result = loop.run_until_complete(
|
||||
service._get_device_enrollment_configurations()
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
# Configurations from BOTH pages must be collected.
|
||||
assert len(result) == 2
|
||||
assert {config.id for config in result} == {"page-one", "page-two"}
|
||||
assert client.request_adapter.send_primitive_async.await_count == 2
|
||||
# The second request must follow the opaque nextLink, not repeat the first.
|
||||
client.device_management.device_enrollment_configurations.with_url.assert_any_call(
|
||||
first_page["@odata.nextLink"]
|
||||
)
|
||||
|
||||
|
||||
class Test_Intune_Service:
|
||||
def test_get_settings_secure_by_default_true(self):
|
||||
intune = _build_intune_service()
|
||||
|
||||
Reference in new issue
Block a user