diff --git a/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md b/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md index 2c6f0ce64..805f47d16 100644 --- a/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md +++ b/docs/engine/policies/cis/microsoft-365-foundations/v6.0.0/controls.md @@ -126,9 +126,9 @@ This document provides a comprehensive overview of all 140 controls in the CIS M | 5.1.3.1 | L1 | Ensure a dynamic group for guest users is created | Automated | Not Started | `entra.groups.groups` | Not Started | Collector exists but control logic not defined | | 5.1.3.2 | L1 | Ensure users cannot create security groups | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowedToCreateSecurityGroups | | 5.1.4.1 | L2 | Ensure the ability to join devices to Entra is restricted | Automated | Automated | `entra.devices.device_registration_policy` | Implemented | | -| 5.1.4.2 | L1 | Ensure the maximum number of devices per user is limited | Automated | Automated | `entra.devices.device_management_settings` | Implemented | | -| 5.1.4.3 | L1 | Ensure the GA role is not added as a local administrator during Entra join | Automated | Automated | `entra.devices.device_management_settings` | Implemented | | -| 5.1.4.4 | L1 | Ensure local administrator assignment is limited during Entra join | Automated | Automated | `entra.devices.device_management_settings` | Implemented | | +| 5.1.4.2 | L1 | Ensure the maximum number of devices per user is limited | Automated | Automated | `entra.devices.device_registration_policy` | Implemented | `userDeviceQuota > 0`; 0 treated as unlimited | +| 5.1.4.3 | L1 | Ensure the GA role is not added as a local administrator during Entra join | Automated | Automated | `entra.devices.device_registration_policy` | Implemented | `azureADJoin.localAdministratorsConfiguration.enableGlobalAdmins == false`; field may be absent on older tenants | +| 5.1.4.4 | L1 | Ensure local administrator assignment is limited during Entra join | Automated | Automated | `entra.devices.device_registration_policy` | Implemented | `azureADJoin.localAdministratorsConfiguration.registeringUsers == notAllowed` | | 5.1.4.5 | L1 | Ensure Local Administrator Password Solution is enabled | Automated | Not Started | | Not Started | Need LAPS configuration collector | | 5.1.4.6 | L2 | Ensure users are restricted from recovering BitLocker keys | Automated | Automated | `entra.policies.authorization_policy` | Implemented | Check allowedToReadBitlockerKeysForOwnedDevice | | 5.1.5.1 | L2 | Ensure user consent to apps accessing company data on their behalf is not allowed | Automated | Automated | `entra.policies.authorization_policy` | Implemented | | diff --git a/engine/collectors/entra/devices/device_registration_policy.py b/engine/collectors/entra/devices/device_registration_policy.py index e4f8f6526..8c3bce662 100644 --- a/engine/collectors/entra/devices/device_registration_policy.py +++ b/engine/collectors/entra/devices/device_registration_policy.py @@ -15,29 +15,32 @@ class DeviceRegistrationPolicyDataCollector(BaseDataCollector): - """Collects device registration policy for CIS compliance evaluation. - - This collector retrieves device join settings, LAPS configuration, - and local admin assignment settings for compliance evaluation. - """ + """Collects device registration policy for CIS compliance evaluation.""" async def collect(self, client: GraphClient) -> dict[str, Any]: """Collect device registration policy data. Returns: - Dict containing: - - device_registration_policy: The device registration policy - - azure_ad_join_settings: Azure AD join configuration - - local_admin_settings: Local admin assignment settings - - laps_settings: LAPS configuration + Dict containing device join settings, quota, local admin config, and LAPS state. """ - # Get device registration policy policy = await client.get("/policies/deviceRegistrationPolicy", beta=True) - # Extract key settings - azure_ad_join = policy.get("azureADJoin", {}) - azure_ad_registration = policy.get("azureADRegistration", {}) - local_admin_password = policy.get("localAdminPassword", {}) + azure_ad_join = policy.get("azureADJoin", {}) or {} + azure_ad_registration = policy.get("azureADRegistration", {}) or {} + local_admin_password = policy.get("localAdminPassword", {}) or {} + local_admins_config = azure_ad_join.get("localAdmins", {}) or {} + + reg_users_raw = local_admins_config.get("registeringUsers") + if isinstance(reg_users_raw, dict): + odata_type = reg_users_raw.get("@odata.type", "") + if "noDeviceRegistrationMembership" in odata_type: + registering_users_local_admin: str | None = "notAllowed" + elif "allDeviceRegistrationMembership" in odata_type: + registering_users_local_admin = "allowed" + else: + registering_users_local_admin = odata_type or None + else: + registering_users_local_admin = reg_users_raw return { "device_registration_policy": policy, @@ -51,4 +54,7 @@ async def collect(self, client: GraphClient) -> dict[str, Any]: "laps_enabled": local_admin_password.get("isEnabled"), "user_device_quota": policy.get("userDeviceQuota"), "multi_factor_auth_configuration": policy.get("multiFactorAuthConfiguration"), + "local_admins_config": local_admins_config, + "global_admins_enabled_on_join": local_admins_config.get("enableGlobalAdmins"), + "registering_users_local_admin": registering_users_local_admin, } diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.2_limit_device_registration_quota.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.2_limit_device_registration_quota.rego new file mode 100644 index 000000000..bc9225d24 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.2_limit_device_registration_quota.rego @@ -0,0 +1,52 @@ +# METADATA +# title: Ensure the maximum number of devices per user is limited +# description: | +# Setting an upper bound on device registrations per user reduces the attack +# surface from compromised accounts and limits lateral movement via registered +# devices. CIS recommends setting this to 5. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.4.2 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.DeviceConfiguration + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_4_2 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve device registration policy", + "details": {}, +} + +compliant := true if { + input.user_device_quota != null + input.user_device_quota > 0 + input.user_device_quota <= 20 +} else := false + +result := output if { + quota := input.user_device_quota + output := { + "compliant": compliant, + "message": build_message(quota), + "affected_resources": [], + "details": {"user_device_quota": quota}, + } +} + +build_message(q) := "Device registration quota is not configured" if { + q == null +} else := "Device registration quota is set to unlimited (non-compliant; CIS requires 20 or less)" if { + q == 0 +} else := sprintf("Device registration quota is set to %d", [q]) if { + q <= 20 +} else := sprintf("Device registration quota is %d; CIS requires 20 or less", [q]) diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.3_ga_not_local_admin_on_join.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.3_ga_not_local_admin_on_join.rego new file mode 100644 index 000000000..b69a42785 --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.3_ga_not_local_admin_on_join.rego @@ -0,0 +1,50 @@ +# METADATA +# title: Ensure the GA role is not added as a local administrator during Entra join +# description: | +# When a device joins Entra ID, automatically granting the Global Administrator +# role local admin rights on that machine creates unnecessary privilege exposure. +# The GA role should not be part of the local administrators configuration for +# joined devices. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.4.3 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.DeviceConfiguration + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_4_3 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve device registration policy", + "details": {}, +} + +compliant := true if { + input.global_admins_enabled_on_join == false +} else := false + +result := output if { + ga_enabled := input.global_admins_enabled_on_join + output := { + "compliant": compliant, + "message": build_message(ga_enabled), + "affected_resources": [], + "details": { + "global_admins_enabled_on_join": ga_enabled, + "local_admins_config": object.get(input, "local_admins_config", null), + }, + } +} + +build_message(false) := "Global Administrator role is not assigned local admin rights on Entra-joined devices" +build_message(null) := "Unable to determine whether GA role is granted local admin rights on join; azureADJoin.localAdmins not returned by API" +build_message(true) := "Global Administrator role is configured as a local administrator on Entra-joined devices" diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.4_limit_local_admin_on_join.rego b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.4_limit_local_admin_on_join.rego new file mode 100644 index 000000000..44abed19b --- /dev/null +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/5.1.4.4_limit_local_admin_on_join.rego @@ -0,0 +1,52 @@ +# METADATA +# title: Ensure local administrator assignment is limited during Entra join +# description: | +# By default, the user registering a device may be granted local administrator +# rights on that device. Restricting this prevents standard users from gaining +# local admin access simply by joining a machine to Entra ID. +# related_resources: +# - ref: https://www.cisecurity.org/benchmark/microsoft_365 +# description: CIS Microsoft 365 Foundations Benchmark +# custom: +# control_id: CIS-5.1.4.4 +# framework: cis +# benchmark: microsoft-365-foundations +# version: v6.0.0 +# severity: medium +# service: EntraID +# requires_permissions: +# - Policy.Read.DeviceConfiguration + +package cis.microsoft_365_foundations.v6_0_0.control_5_1_4_4 + +import rego.v1 + +default result := { + "compliant": false, + "message": "Evaluation failed: unable to retrieve device registration policy", + "details": {}, +} + +compliant := true if { + input.registering_users_local_admin != null + lower(input.registering_users_local_admin) == "notallowed" +} else := false + +result := output if { + reg_users := input.registering_users_local_admin + output := { + "compliant": compliant, + "message": build_message(reg_users), + "affected_resources": [], + "details": { + "registering_users_local_admin": reg_users, + "local_admins_config": object.get(input, "local_admins_config", null), + }, + } +} + +build_message(val) := "Unable to determine local admin assignment for registering users; azureADJoin.localAdmins not returned by API" if { + val == null +} else := "Registering users are not granted local administrator rights on Entra-joined devices" if { + lower(val) == "notallowed" +} else := sprintf("Registering users are granted local admin rights on join (registeringUsers=%s)", [val]) diff --git a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json index c6c02e25a..11a614dce 100644 --- a/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json +++ b/engine/policies/cis/microsoft-365-foundations/v6.0.0/metadata.json @@ -766,11 +766,11 @@ "level": "L1", "is_manual": false, "benchmark_audit_type": "Automated", - "automation_status": "not_started", - "data_collector_id": "entra.devices.device_management_settings", - "policy_file": null, - "requires_permissions": ["Policy.Read.All"], - "notes": null + "automation_status": "ready", + "data_collector_id": "entra.devices.device_registration_policy", + "policy_file": "5.1.4.2_limit_device_registration_quota.rego", + "requires_permissions": ["Policy.Read.DeviceConfiguration"], + "notes": "CIS v6.0.0 requires userDeviceQuota to be set to 20 or less. A value of 0 is treated as unlimited and is non-compliant." }, { "control_id": "5.1.4.3", @@ -781,11 +781,11 @@ "level": "L1", "is_manual": false, "benchmark_audit_type": "Automated", - "automation_status": "not_started", - "data_collector_id": "entra.devices.device_management_settings", - "policy_file": null, - "requires_permissions": ["Policy.Read.All"], - "notes": null + "automation_status": "ready", + "data_collector_id": "entra.devices.device_registration_policy", + "policy_file": "5.1.4.3_ga_not_local_admin_on_join.rego", + "requires_permissions": ["Policy.Read.DeviceConfiguration"], + "notes": "Uses azureADJoin.localAdministratorsConfiguration.enableGlobalAdmins from /policies/deviceRegistrationPolicy (beta). Field may be absent on older tenants; policy returns non-compliant with an explanatory message in that case." }, { "control_id": "5.1.4.4", @@ -796,11 +796,11 @@ "level": "L1", "is_manual": false, "benchmark_audit_type": "Automated", - "automation_status": "not_started", - "data_collector_id": "entra.devices.device_management_settings", - "policy_file": null, - "requires_permissions": ["Policy.Read.All"], - "notes": null + "automation_status": "ready", + "data_collector_id": "entra.devices.device_registration_policy", + "policy_file": "5.1.4.4_limit_local_admin_on_join.rego", + "requires_permissions": ["Policy.Read.DeviceConfiguration"], + "notes": "Uses azureADJoin.localAdministratorsConfiguration.registeringUsers from /policies/deviceRegistrationPolicy (beta). Compliant when value is notAllowed." }, { "control_id": "5.1.4.5",