diff --git a/prowler/changelog.d/m365-cis7-entra-device-registration.added.md b/prowler/changelog.d/m365-cis7-entra-device-registration.added.md new file mode 100644 index 0000000000..cd81d1e4eb --- /dev/null +++ b/prowler/changelog.d/m365-cis7-entra-device-registration.added.md @@ -0,0 +1 @@ +`entra_device_registration_join_restricted`, `entra_device_registration_max_devices_per_user_limited`, `entra_device_registration_global_admins_not_local_admins`, `entra_device_registration_registering_user_not_local_admin`, `entra_device_registration_laps_enabled` and `entra_policy_default_user_cannot_read_bitlocker_keys` checks for M365 provider, covering CIS Microsoft 365 Foundations Benchmark v7.0.0 device registration controls (5.1.4.x) diff --git a/prowler/compliance/m365/cis_7.0_m365.json b/prowler/compliance/m365/cis_7.0_m365.json index 20594d4d06..84b5aa5c4d 100644 --- a/prowler/compliance/m365/cis_7.0_m365.json +++ b/prowler/compliance/m365/cis_7.0_m365.json @@ -1250,7 +1250,9 @@ { "Id": "5.1.4.1", "Description": "This setting enables you to select the users who can register their devices as Microsoft Entra joined devices. The recommended state is Selected or None. Note: This setting is applicable only to Microsoft Entra join on Windows 10 or newer. This setting doesn't apply to Microsoft Entra hybrid joined devices, Microsoft Entra joined VMs in Azure, or Microsoft Entra joined devices that use Windows Autopilot self- deployment mode because these methods work in a userless context.", - "Checks": [], + "Checks": [ + "entra_device_registration_join_restricted" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", @@ -1271,7 +1273,9 @@ { "Id": "5.1.4.2", "Description": "This setting defines the maximum number of Microsoft Entra joined or registered devices that a user can have in Microsoft Entra ID. Once this limit is reached, no additional devices can be added until existing ones are removed. Values above 100 are automatically capped at 100. The recommended state is 10 or less.", - "Checks": [], + "Checks": [ + "entra_device_registration_max_devices_per_user_limited" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", @@ -1292,7 +1296,9 @@ { "Id": "5.1.4.3", "Description": "This setting controls whether the Global Administrator role is automatically added to the local administrators group on a device during the Microsoft Entra join process. The recommended state is No.", - "Checks": [], + "Checks": [ + "entra_device_registration_global_admins_not_local_admins" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", @@ -1313,7 +1319,9 @@ { "Id": "5.1.4.4", "Description": "This setting determines if the Microsoft Entra user registering their device as Microsoft Entra join will be added to the local administrators group. This setting applies only once during the actual registration of the device as Microsoft Entra join. The recommended state is Selected or None.", - "Checks": [], + "Checks": [ + "entra_device_registration_registering_user_not_local_admin" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", @@ -1334,7 +1342,9 @@ { "Id": "5.1.4.5", "Description": "Local Administrator Password Solution (LAPS) is the management of local account passwords on Windows devices. LAPS provides a solution to securely manage and retrieve the built-in local admin password. With cloud version of LAPS, customers can enable storing and rotation of local admin passwords for both Microsoft Entra and Microsoft Entra hybrid join devices The recommended state is Yes.", - "Checks": [], + "Checks": [ + "entra_device_registration_laps_enabled" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", @@ -1355,7 +1365,9 @@ { "Id": "5.1.4.6", "Description": "This setting determines if users can self-service recover their BitLocker key(s). 'Yes' restricts non-admin users from being able to see the BitLocker key(s) for their owned devices if there are any. 'No' allows all users to recover their BitLocker key(s). The recommended state is Yes.", - "Checks": [], + "Checks": [ + "entra_policy_default_user_cannot_read_bitlocker_keys" + ], "Attributes": [ { "Section": "5 Microsoft Entra admin center", diff --git a/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/__init__.py b/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins.metadata.json b/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins.metadata.json new file mode 100644 index 0000000000..aa9da8ddb1 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_device_registration_global_admins_not_local_admins", + "CheckTitle": "Global Administrators are not added as local administrators during Entra join", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The tenant device registration policy should not automatically add the **Global Administrator** role to the local administrators group of a device during the Microsoft Entra join process (**azureADJoin.localAdmins.enableGlobalAdmins** should be false).", + "Risk": "Automatically granting Global Administrators local admin rights on every Entra-joined device broadens the blast radius of a device compromise and violates least privilege, since local admin rights on endpoints are rarely required for directory administration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/devices/assign-local-admin" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Global administrator role is added as local administrator on the device during Microsoft Entra join** to **No**\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable the automatic addition of Global Administrators to the local administrators group during Microsoft Entra join and grant local admin rights only where required.", + "Url": "https://hub.prowler.com/check/entra_device_registration_global_admins_not_local_admins" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins.py b/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins.py new file mode 100644 index 0000000000..b1137e0707 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins.py @@ -0,0 +1,49 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client + + +class entra_device_registration_global_admins_not_local_admins(Check): + """Check if Global Administrators are not added as local admins on Entra join. + + The device registration policy should not automatically add the Global + Administrator role to the local administrators group of a device during the + Microsoft Entra join process. + + - PASS: Global Administrators are not added as local administrators on Entra join. + - FAIL: Global Administrators are added as local administrators on Entra join. + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the Global Administrators local-admin restriction check. + + Returns: + List[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + policy = entra_client.device_registration_policy + if not policy: + return findings + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Device Registration Policy", + resource_id="deviceRegistrationPolicy", + ) + report.status = "FAIL" + report.status_extended = ( + "Global Administrators are added as local administrators on devices " + "during Microsoft Entra join." + ) + + if policy.azure_ad_join_global_admins_enabled is False: + report.status = "PASS" + report.status_extended = ( + "Global Administrators are not added as local administrators on " + "devices during Microsoft Entra join." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/__init__.py b/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted.metadata.json b/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted.metadata.json new file mode 100644 index 0000000000..b70c8d785d --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_device_registration_join_restricted", + "CheckTitle": "Users allowed to join devices to Microsoft Entra are restricted", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The tenant device registration policy should restrict who can register devices as **Microsoft Entra joined** to **Selected** users or **None**. Allowing all users to join devices increases the number of devices that establish a trust relationship with the tenant.", + "Risk": "When all users can Entra-join devices, an attacker who compromises any account can register a device, potentially satisfying device-based **Conditional Access** controls and expanding their foothold in the tenant.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/devices/device-join-plan" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Users may join devices to Microsoft Entra** to **Selected** (and choose the allowed users/groups) or **None**\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict Entra device join to a selected set of users or disable it entirely unless there is a business need for all users to join devices.", + "Url": "https://hub.prowler.com/check/entra_device_registration_join_restricted" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted.py b/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted.py new file mode 100644 index 0000000000..6b9623adef --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted.py @@ -0,0 +1,57 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationMembershipType, +) + +RESTRICTED_MEMBERSHIP_TYPES = { + DeviceRegistrationMembershipType.ENUMERATED.value, + DeviceRegistrationMembershipType.NONE.value, +} + + +class entra_device_registration_join_restricted(Check): + """Check if the users allowed to join devices to Entra are restricted. + + The device registration policy should restrict who can register devices as + Microsoft Entra joined to Selected users or None, rather than allowing all + users. + + - PASS: Only selected users or no users may join devices to Entra. + - FAIL: The users allowed to join devices are not restricted to Selected or None. + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the Entra device join restriction check. + + Returns: + List[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + policy = entra_client.device_registration_policy + if not policy: + return findings + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Device Registration Policy", + resource_id="deviceRegistrationPolicy", + ) + report.status = "FAIL" + report.status_extended = ( + "The users allowed to join devices to Microsoft Entra are not " + "restricted to selected users or none." + ) + + if policy.azure_ad_join_allowed_to_join_type in RESTRICTED_MEMBERSHIP_TYPES: + report.status = "PASS" + report.status_extended = ( + "Only selected users or no users are allowed to join devices to " + "Microsoft Entra." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/__init__.py b/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled.metadata.json b/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled.metadata.json new file mode 100644 index 0000000000..6f76a9af3f --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_device_registration_laps_enabled", + "CheckTitle": "Microsoft Entra Local Administrator Password Solution (LAPS) is enabled", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The tenant device registration policy should enable **Microsoft Entra Local Administrator Password Solution (LAPS)** (**localAdminPassword.isEnabled**). LAPS securely manages and rotates the built-in local administrator password on Windows devices and stores it for controlled retrieval.", + "Risk": "Without **LAPS**, local administrator passwords are often static and shared across devices, enabling **lateral movement**: an attacker who recovers one device's local admin password can reuse it across the fleet.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/devices/howto-manage-local-admin-passwords" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Enable Microsoft Entra Local Administrator Password Solution (LAPS)** to **Yes**\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Microsoft Entra LAPS and deploy a matching Intune policy so local administrator passwords are unique per device, rotated automatically, and retrievable only by authorized roles.", + "Url": "https://hub.prowler.com/check/entra_device_registration_laps_enabled" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled.py b/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled.py new file mode 100644 index 0000000000..bab5c86555 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled.py @@ -0,0 +1,47 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client + + +class entra_device_registration_laps_enabled(Check): + """Check if Microsoft Entra Local Administrator Password Solution (LAPS) is enabled. + + The device registration policy should enable LAPS so that the built-in local + administrator password on Windows devices is securely managed and rotated. + + - PASS: LAPS is enabled. + - FAIL: LAPS is disabled. + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the Microsoft Entra LAPS enablement check. + + Returns: + List[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + policy = entra_client.device_registration_policy + if not policy: + return findings + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Device Registration Policy", + resource_id="deviceRegistrationPolicy", + ) + report.status = "FAIL" + report.status_extended = ( + "Microsoft Entra Local Administrator Password Solution (LAPS) is disabled." + ) + + if policy.local_admin_password_enabled: + report.status = "PASS" + report.status_extended = ( + "Microsoft Entra Local Administrator Password Solution (LAPS) is " + "enabled." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/__init__.py b/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited.metadata.json b/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited.metadata.json new file mode 100644 index 0000000000..625a1539ba --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_device_registration_max_devices_per_user_limited", + "CheckTitle": "Maximum number of devices per user is limited", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The tenant device registration policy should limit the maximum number of Microsoft Entra joined or registered devices per user to **10 or less** (**userDeviceQuota**). Once the limit is reached, no additional devices can be added until existing ones are removed.", + "Risk": "An unbounded or high per-user device quota lets a compromised account register many devices, increasing the number of trusted endpoints an attacker controls and complicating device lifecycle governance.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/devices/device-join-plan" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Maximum number of devices per user** to **10** or less\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set the maximum number of devices per user to 10 or less to bound the number of trusted endpoints each identity can register.", + "Url": "https://hub.prowler.com/check/entra_device_registration_max_devices_per_user_limited" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited.py b/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited.py new file mode 100644 index 0000000000..af0c8e09d7 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited.py @@ -0,0 +1,58 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client + +# CIS recommends a maximum of 10 devices per user (or less). +MAX_DEVICES_PER_USER = 10 + + +class entra_device_registration_max_devices_per_user_limited(Check): + """Check if the maximum number of devices per user is limited. + + The device registration policy should set the maximum number of Entra joined or + registered devices per user to 10 or less. + + - PASS: The maximum number of devices per user is 10 or less. + - FAIL: The maximum number of devices per user is greater than 10 (or unlimited). + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the per-user device quota limit check. + + Returns: + List[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + policy = entra_client.device_registration_policy + if not policy: + return findings + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Device Registration Policy", + resource_id="deviceRegistrationPolicy", + ) + quota = policy.user_device_quota + report.status = "FAIL" + if quota is None: + report.status_extended = ( + "The maximum number of devices per user is not limited, exceeding " + f"the recommended limit of {MAX_DEVICES_PER_USER}." + ) + else: + report.status_extended = ( + f"The maximum number of devices per user is {quota}, which exceeds " + f"the recommended limit of {MAX_DEVICES_PER_USER}." + ) + + if quota is not None and quota <= MAX_DEVICES_PER_USER: + report.status = "PASS" + report.status_extended = ( + f"The maximum number of devices per user is {quota}, within the " + f"recommended limit of {MAX_DEVICES_PER_USER}." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/__init__.py b/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin.metadata.json b/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin.metadata.json new file mode 100644 index 0000000000..0c0e8d30af --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_device_registration_registering_user_not_local_admin", + "CheckTitle": "Registering user is not added as local administrator during Entra join", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The tenant device registration policy should restrict which registering users are added to the local administrators group during Microsoft Entra join to **Selected** users or **None** (**azureADJoin.localAdmins.registeringUsers**), rather than granting local admin to every user who registers a device.", + "Risk": "Automatically granting the registering user local administrator rights gives standard users elevated control over their devices, increasing the impact of endpoint compromise and enabling local privilege abuse.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/devices/assign-local-admin" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **Device settings**\n3. Set **Registering user is added as local administrator on the device during Microsoft Entra join** to **Selected** or **None**\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict or disable the automatic addition of the registering user to the local administrators group during Microsoft Entra join, granting local admin rights only to selected users where required.", + "Url": "https://hub.prowler.com/check/entra_device_registration_registering_user_not_local_admin" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin.py b/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin.py new file mode 100644 index 0000000000..c5a88331eb --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin.py @@ -0,0 +1,59 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationMembershipType, +) + +RESTRICTED_MEMBERSHIP_TYPES = { + DeviceRegistrationMembershipType.ENUMERATED.value, + DeviceRegistrationMembershipType.NONE.value, +} + + +class entra_device_registration_registering_user_not_local_admin(Check): + """Check if the registering user is not added as local admin on Entra join. + + The device registration policy should restrict which registering users are added + to the local administrators group during Microsoft Entra join to Selected users + or None, rather than all registering users. + + - PASS: Registering users are restricted (Selected or None) from becoming local + administrators on Entra join. + - FAIL: The registering users added as local administrators are not restricted + to Selected or None. + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the registering-user local-admin restriction check. + + Returns: + List[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + policy = entra_client.device_registration_policy + if not policy: + return findings + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name="Device Registration Policy", + resource_id="deviceRegistrationPolicy", + ) + report.status = "FAIL" + report.status_extended = ( + "Registering users are not restricted from being added as local " + "administrators on devices during Microsoft Entra join." + ) + + if policy.azure_ad_join_registering_users_type in RESTRICTED_MEMBERSHIP_TYPES: + report.status = "PASS" + report.status_extended = ( + "Registering users are restricted from being added as local " + "administrators on devices during Microsoft Entra join." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/__init__.py b/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys.metadata.json b/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys.metadata.json new file mode 100644 index 0000000000..24d8d6b954 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "m365", + "CheckID": "entra_policy_default_user_cannot_read_bitlocker_keys", + "CheckTitle": "Non-admin users cannot read BitLocker keys for their owned devices", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra tenant's authorization policy should restrict **non-admin users** from reading (self-recovering) **BitLocker recovery keys** for devices they own. Restricting self-service recovery reduces the risk of an attacker who has compromised a user account from also recovering the disk-encryption key of that user's device.", + "Risk": "If a user can retrieve the **BitLocker recovery key** for their own device, an attacker who compromises the account can decrypt the device's disk, exposing data at rest. Recovery-key access should be limited to administrators and controlled recovery workflows.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/devices/device-management-azure-portal" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center at https://entra.microsoft.com/\n2. Go to **Identity** > **Devices** > **All devices** > **Device settings**\n3. Set **Users can recover BitLocker key(s) for their owned devices** to **No**\n4. Click **Save**", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable self-service BitLocker recovery-key access for non-admin users so that recovery keys can only be retrieved by administrators through a controlled process.", + "Url": "https://hub.prowler.com/check/entra_policy_default_user_cannot_read_bitlocker_keys" + } + }, + "Categories": [ + "identity-access", + "encryption", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys.py b/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys.py new file mode 100644 index 0000000000..83639bf872 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys.py @@ -0,0 +1,48 @@ +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client + + +class entra_policy_default_user_cannot_read_bitlocker_keys(Check): + """Check if default users are restricted from reading BitLocker keys for their owned devices. + + This check verifies whether the authorization policy prevents non-admin users + from self-recovering BitLocker keys for devices they own in Microsoft Entra ID. + + - PASS: Non-admin users cannot read BitLocker keys for their owned devices. + - FAIL: Non-admin users are allowed to read BitLocker keys for their owned devices. + """ + + def execute(self) -> List[CheckReportM365]: + """Execute the check for BitLocker key self-recovery restrictions. + + Returns: + List[CheckReportM365]: A list containing the result of the check. + """ + findings = [] + auth_policy = entra_client.authorization_policy + + report = CheckReportM365( + metadata=self.metadata(), + resource=auth_policy if auth_policy else {}, + resource_name=auth_policy.name if auth_policy else "Authorization Policy", + resource_id=auth_policy.id if auth_policy else "authorizationPolicy", + ) + report.status = "FAIL" + report.status_extended = "Non-admin users are allowed to read BitLocker keys for their owned devices." + + if ( + getattr(auth_policy, "default_user_role_permissions", None) + and getattr( + auth_policy.default_user_role_permissions, + "allowed_to_read_bitlocker_keys_for_owned_device", + None, + ) + is False + ): + report.status = "PASS" + report.status_extended = "Non-admin users are not allowed to read BitLocker keys for their owned devices." + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index a083eb1c3c..9c94a4b544 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -102,6 +102,7 @@ class Entra(M365Service): self._get_service_principals(), self._get_app_registrations(), self._get_exchange_mailbox_permission_service_principals(), + self._get_device_registration_policy(), ) ) @@ -122,6 +123,9 @@ class Entra(M365Service): self.exchange_mailbox_permission_service_principals: Dict[ str, "ServicePrincipal" ] = attributes[12] + self.device_registration_policy: Optional[DeviceRegistrationPolicy] = ( + attributes[13] + ) self.user_accounts_status = {} # Resolve directory-object identifiers referenced by Conditional Access @@ -1192,6 +1196,53 @@ OAuthAppInfo ) return authentication_method_configurations + async def _get_device_registration_policy(self): + """Retrieve the tenant device registration policy from Microsoft Entra. + + Fetches the ``policies/deviceRegistrationPolicy`` singleton from the v1.0 + Graph endpoint. The response is parsed from raw JSON because the audited + settings (``azureADJoin.*`` membership objects) are polymorphic + ``@odata.type`` values that are simpler to read from the raw payload than + through the typed SDK model. + + Returns: + Optional[DeviceRegistrationPolicy]: The parsed policy, or None on error. + """ + logger.info("Entra - Getting device registration policy...") + device_registration_policy = None + try: + request_info = ( + self.client.policies.device_registration_policy.to_get_request_information() + ) + response = await self.client.request_adapter.send_primitive_async( + request_info, "bytes", {} + ) + if response: + data = json.loads(response) + azure_ad_join = data.get("azureADJoin", {}) or {} + local_admins = azure_ad_join.get("localAdmins", {}) or {} + allowed_to_join = azure_ad_join.get("allowedToJoin", {}) or {} + registering_users = local_admins.get("registeringUsers", {}) or {} + local_admin_password = data.get("localAdminPassword", {}) or {} + device_registration_policy = DeviceRegistrationPolicy( + user_device_quota=data.get("userDeviceQuota"), + azure_ad_join_allowed_to_join_type=allowed_to_join.get( + "@odata.type" + ), + azure_ad_join_global_admins_enabled=local_admins.get( + "enableGlobalAdmins" + ), + azure_ad_join_registering_users_type=registering_users.get( + "@odata.type" + ), + local_admin_password_enabled=local_admin_password.get("isEnabled"), + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return device_registration_policy + async def _get_service_principals(self): """Retrieve service principals owned by the audited tenant. @@ -1925,6 +1976,24 @@ class AuthorizationPolicy(BaseModel): guest_user_role_id: Optional[UUID] +class DeviceRegistrationMembershipType(str, Enum): + """OData types for Entra device registration membership settings.""" + + ALL = "#microsoft.graph.allDeviceRegistrationMembership" + ENUMERATED = "#microsoft.graph.enumeratedDeviceRegistrationMembership" + NONE = "#microsoft.graph.noDeviceRegistrationMembership" + + +class DeviceRegistrationPolicy(BaseModel): + """Tenant device registration policy (policies/deviceRegistrationPolicy).""" + + user_device_quota: Optional[int] = None + azure_ad_join_allowed_to_join_type: Optional[str] = None + azure_ad_join_global_admins_enabled: Optional[bool] = None + azure_ad_join_registering_users_type: Optional[str] = None + local_admin_password_enabled: Optional[bool] = None + + class Organization(BaseModel): id: str name: str diff --git a/tests/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins_test.py b/tests/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins_test.py new file mode 100644 index 0000000000..b47af9ea85 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_device_registration_global_admins_not_local_admins/entra_device_registration_global_admins_not_local_admins_test.py @@ -0,0 +1,55 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationPolicy, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_device_registration_global_admins_not_local_admins.entra_device_registration_global_admins_not_local_admins" + + +class Test_entra_device_registration_global_admins_not_local_admins: + def _run(self, policy): + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_device_registration_global_admins_not_local_admins.entra_device_registration_global_admins_not_local_admins import ( + entra_device_registration_global_admins_not_local_admins, + ) + + entra_client.device_registration_policy = policy + return entra_device_registration_global_admins_not_local_admins().execute() + + def test_no_policy(self): + assert self._run(None) == [] + + def test_global_admins_enabled(self): + result = self._run( + DeviceRegistrationPolicy(azure_ad_join_global_admins_enabled=True) + ) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Global Administrators are added as local administrators on devices during Microsoft Entra join." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_global_admins_disabled(self): + result = self._run( + DeviceRegistrationPolicy(azure_ad_join_global_admins_enabled=False) + ) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Global Administrators are not added as local administrators on devices during Microsoft Entra join." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" diff --git a/tests/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted_test.py b/tests/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted_test.py new file mode 100644 index 0000000000..5932277300 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_device_registration_join_restricted/entra_device_registration_join_restricted_test.py @@ -0,0 +1,84 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationMembershipType, + DeviceRegistrationPolicy, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_device_registration_join_restricted.entra_device_registration_join_restricted" + + +class Test_entra_device_registration_join_restricted: + def _run(self, policy): + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_device_registration_join_restricted.entra_device_registration_join_restricted import ( + entra_device_registration_join_restricted, + ) + + entra_client.device_registration_policy = policy + return entra_device_registration_join_restricted().execute() + + def test_no_policy(self): + assert self._run(None) == [] + + def test_all_users(self): + result = self._run( + DeviceRegistrationPolicy( + azure_ad_join_allowed_to_join_type=DeviceRegistrationMembershipType.ALL.value + ) + ) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "The users allowed to join devices to Microsoft Entra are not restricted to selected users or none." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_unknown_membership_type(self): + result = self._run( + DeviceRegistrationPolicy(azure_ad_join_allowed_to_join_type=None) + ) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "The users allowed to join devices to Microsoft Entra are not restricted to selected users or none." + ) + + def test_selected_users(self): + result = self._run( + DeviceRegistrationPolicy( + azure_ad_join_allowed_to_join_type=DeviceRegistrationMembershipType.ENUMERATED.value + ) + ) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Only selected users or no users are allowed to join devices to Microsoft Entra." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_none(self): + result = self._run( + DeviceRegistrationPolicy( + azure_ad_join_allowed_to_join_type=DeviceRegistrationMembershipType.NONE.value + ) + ) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Only selected users or no users are allowed to join devices to Microsoft Entra." + ) diff --git a/tests/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled_test.py b/tests/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled_test.py new file mode 100644 index 0000000000..0b63d39093 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_device_registration_laps_enabled/entra_device_registration_laps_enabled_test.py @@ -0,0 +1,51 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationPolicy, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_device_registration_laps_enabled.entra_device_registration_laps_enabled" + + +class Test_entra_device_registration_laps_enabled: + def _run(self, policy): + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_device_registration_laps_enabled.entra_device_registration_laps_enabled import ( + entra_device_registration_laps_enabled, + ) + + entra_client.device_registration_policy = policy + return entra_device_registration_laps_enabled().execute() + + def test_no_policy(self): + assert self._run(None) == [] + + def test_laps_enabled(self): + result = self._run(DeviceRegistrationPolicy(local_admin_password_enabled=True)) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Microsoft Entra Local Administrator Password Solution (LAPS) is enabled." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_laps_disabled(self): + result = self._run(DeviceRegistrationPolicy(local_admin_password_enabled=False)) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Microsoft Entra Local Administrator Password Solution (LAPS) is disabled." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" diff --git a/tests/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited_test.py b/tests/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited_test.py new file mode 100644 index 0000000000..4f0eac2952 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_device_registration_max_devices_per_user_limited/entra_device_registration_max_devices_per_user_limited_test.py @@ -0,0 +1,69 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationPolicy, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_device_registration_max_devices_per_user_limited.entra_device_registration_max_devices_per_user_limited" + + +class Test_entra_device_registration_max_devices_per_user_limited: + def _run(self, policy): + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_device_registration_max_devices_per_user_limited.entra_device_registration_max_devices_per_user_limited import ( + entra_device_registration_max_devices_per_user_limited, + ) + + entra_client.device_registration_policy = policy + return entra_device_registration_max_devices_per_user_limited().execute() + + def test_no_policy(self): + assert self._run(None) == [] + + def test_within_limit(self): + result = self._run(DeviceRegistrationPolicy(user_device_quota=10)) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "The maximum number of devices per user is 10, within the recommended limit of 10." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_zero_quota(self): + result = self._run(DeviceRegistrationPolicy(user_device_quota=0)) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "The maximum number of devices per user is 0, within the recommended limit of 10." + ) + + def test_exceeds_limit(self): + result = self._run(DeviceRegistrationPolicy(user_device_quota=50)) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "The maximum number of devices per user is 50, which exceeds the recommended limit of 10." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_none_quota(self): + result = self._run(DeviceRegistrationPolicy(user_device_quota=None)) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "The maximum number of devices per user is not limited, exceeding the recommended limit of 10." + ) diff --git a/tests/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin_test.py b/tests/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin_test.py new file mode 100644 index 0000000000..7cb800816a --- /dev/null +++ b/tests/providers/m365/services/entra/entra_device_registration_registering_user_not_local_admin/entra_device_registration_registering_user_not_local_admin_test.py @@ -0,0 +1,86 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + DeviceRegistrationMembershipType, + DeviceRegistrationPolicy, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_device_registration_registering_user_not_local_admin.entra_device_registration_registering_user_not_local_admin" + + +class Test_entra_device_registration_registering_user_not_local_admin: + def _run(self, policy): + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_device_registration_registering_user_not_local_admin.entra_device_registration_registering_user_not_local_admin import ( + entra_device_registration_registering_user_not_local_admin, + ) + + entra_client.device_registration_policy = policy + return ( + entra_device_registration_registering_user_not_local_admin().execute() + ) + + def test_no_policy(self): + assert self._run(None) == [] + + def test_all_registering_users(self): + result = self._run( + DeviceRegistrationPolicy( + azure_ad_join_registering_users_type=DeviceRegistrationMembershipType.ALL.value + ) + ) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Registering users are not restricted from being added as local administrators on devices during Microsoft Entra join." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_unknown_membership_type(self): + result = self._run( + DeviceRegistrationPolicy(azure_ad_join_registering_users_type=None) + ) + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Registering users are not restricted from being added as local administrators on devices during Microsoft Entra join." + ) + + def test_selected_registering_users(self): + result = self._run( + DeviceRegistrationPolicy( + azure_ad_join_registering_users_type=DeviceRegistrationMembershipType.ENUMERATED.value + ) + ) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Registering users are restricted from being added as local administrators on devices during Microsoft Entra join." + ) + assert result[0].resource_id == "deviceRegistrationPolicy" + assert result[0].resource_name == "Device Registration Policy" + + def test_none_registering_users(self): + result = self._run( + DeviceRegistrationPolicy( + azure_ad_join_registering_users_type=DeviceRegistrationMembershipType.NONE.value + ) + ) + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Registering users are restricted from being added as local administrators on devices during Microsoft Entra join." + ) diff --git a/tests/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys_test.py b/tests/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys_test.py new file mode 100644 index 0000000000..31134d60d6 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_policy_default_user_cannot_read_bitlocker_keys/entra_policy_default_user_cannot_read_bitlocker_keys_test.py @@ -0,0 +1,137 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + AuthorizationPolicy, + DefaultUserRolePermissions, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + + +class Test_entra_policy_default_user_cannot_read_bitlocker_keys: + def test_users_can_read_bitlocker_keys(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys import ( + entra_policy_default_user_cannot_read_bitlocker_keys, + ) + + entra_client.authorization_policy = AuthorizationPolicy( + id="authorizationPolicy", + name="Authorization Policy", + description="", + default_user_role_permissions=DefaultUserRolePermissions( + allowed_to_read_bitlocker_keys_for_owned_device=True, + ), + ) + + check = entra_policy_default_user_cannot_read_bitlocker_keys() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Non-admin users are allowed to read BitLocker keys for their owned devices." + ) + + def test_authorization_policy_none(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys import ( + entra_policy_default_user_cannot_read_bitlocker_keys, + ) + + entra_client.authorization_policy = None + + result = entra_policy_default_user_cannot_read_bitlocker_keys().execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_users_cannot_read_bitlocker_keys(self): + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys import ( + entra_policy_default_user_cannot_read_bitlocker_keys, + ) + + entra_client.authorization_policy = AuthorizationPolicy( + id="authorizationPolicy", + name="Authorization Policy", + description="", + default_user_role_permissions=DefaultUserRolePermissions( + allowed_to_read_bitlocker_keys_for_owned_device=False, + ), + ) + + check = entra_policy_default_user_cannot_read_bitlocker_keys() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Non-admin users are not allowed to read BitLocker keys for their owned devices." + ) + + def test_bitlocker_permission_unknown(self): + """A missing permission value must fail closed, not report PASS.""" + entra_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_default_user_cannot_read_bitlocker_keys.entra_policy_default_user_cannot_read_bitlocker_keys import ( + entra_policy_default_user_cannot_read_bitlocker_keys, + ) + + entra_client.authorization_policy = AuthorizationPolicy( + id="authorizationPolicy", + name="Authorization Policy", + description="", + default_user_role_permissions=DefaultUserRolePermissions( + allowed_to_read_bitlocker_keys_for_owned_device=None, + ), + ) + + result = entra_policy_default_user_cannot_read_bitlocker_keys().execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" diff --git a/tests/providers/m365/services/entra/microsoft365_entra_service_test.py b/tests/providers/m365/services/entra/microsoft365_entra_service_test.py index fb8a5e5e82..16b6b23c2d 100644 --- a/tests/providers/m365/services/entra/microsoft365_entra_service_test.py +++ b/tests/providers/m365/services/entra/microsoft365_entra_service_test.py @@ -22,6 +22,8 @@ Conditions = entra_service.Conditions CredentialRestriction = entra_service.CredentialRestriction DefaultAppManagementPolicy = entra_service.DefaultAppManagementPolicy DefaultUserRolePermissions = entra_service.DefaultUserRolePermissions +DeviceRegistrationMembershipType = entra_service.DeviceRegistrationMembershipType +DeviceRegistrationPolicy = entra_service.DeviceRegistrationPolicy Entra = entra_service.Entra GrantControlOperator = entra_service.GrantControlOperator GrantControls = entra_service.GrantControls @@ -727,6 +729,71 @@ class Test_Entra_Service: assert "AuditLog.Read.All" in error_message assert "user registration details" in error_message + def _mocked_device_registration_entra(self, send_primitive): + entra_service = Entra.__new__(Entra) + entra_service.client = SimpleNamespace( + policies=SimpleNamespace( + device_registration_policy=SimpleNamespace( + to_get_request_information=MagicMock(return_value="request-info") + ) + ), + request_adapter=SimpleNamespace(send_primitive_async=send_primitive), + ) + return entra_service + + def test__get_device_registration_policy(self): + payload = b""" + { + "id": "deviceRegistrationPolicy", + "userDeviceQuota": 50, + "azureADJoin": { + "allowedToJoin": { + "@odata.type": "#microsoft.graph.allDeviceRegistrationMembership" + }, + "localAdmins": { + "enableGlobalAdmins": true, + "registeringUsers": { + "@odata.type": "#microsoft.graph.enumeratedDeviceRegistrationMembership" + } + } + }, + "localAdminPassword": {"isEnabled": false} + } + """ + send_primitive = AsyncMock(return_value=payload) + entra_service = self._mocked_device_registration_entra(send_primitive) + + policy = asyncio.run(entra_service._get_device_registration_policy()) + + assert policy == DeviceRegistrationPolicy( + user_device_quota=50, + azure_ad_join_allowed_to_join_type=DeviceRegistrationMembershipType.ALL.value, + azure_ad_join_global_admins_enabled=True, + azure_ad_join_registering_users_type=DeviceRegistrationMembershipType.ENUMERATED.value, + local_admin_password_enabled=False, + ) + send_primitive.assert_awaited_once_with("request-info", "bytes", {}) + + def test__get_device_registration_policy_missing_fields(self): + send_primitive = AsyncMock(return_value=b'{"id": "deviceRegistrationPolicy"}') + entra_service = self._mocked_device_registration_entra(send_primitive) + + policy = asyncio.run(entra_service._get_device_registration_policy()) + + assert policy == DeviceRegistrationPolicy( + user_device_quota=None, + azure_ad_join_allowed_to_join_type=None, + azure_ad_join_global_admins_enabled=None, + azure_ad_join_registering_users_type=None, + local_admin_password_enabled=None, + ) + + def test__get_device_registration_policy_returns_none_on_error(self): + send_primitive = AsyncMock(side_effect=Exception("Graph error")) + entra_service = self._mocked_device_registration_entra(send_primitive) + + assert asyncio.run(entra_service._get_device_registration_policy()) is None + def test__get_service_principals_filters_third_party_owners(self): """Service principals owned by another tenant must not be returned.""" # Mixed-case input to verify the service normalizes both sides before