From 3b875484b076275ed0be3b24b15c99df969d873e Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:36:05 +0100 Subject: [PATCH] feat(m365): add device registration MFA and harden Intune enrollment CA check (#10222) Co-authored-by: Hugo Brito Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 4 +- prowler/compliance/m365/cis_6.0_m365.json | 2 +- .../compliance/m365/iso27001_2022_m365.json | 6 + .../__init__.py | 0 ...ce_registration_mfa_required.metadata.json | 41 ++ ...policy_device_registration_mfa_required.py | 75 +++ ...sign_in_frequency_every_time.metadata.json | 14 +- ...enrollment_sign_in_frequency_every_time.py | 38 +- ...y_device_registration_mfa_required_test.py | 276 +++++++++++ ...lment_sign_in_frequency_every_time_test.py | 460 +++++++++--------- 10 files changed, 663 insertions(+), 253 deletions(-) create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/__init__.py create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.metadata.json create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.py create mode 100644 tests/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index c661129bef..114f63de0a 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🚀 Added +- `apikeys_api_restricted_with_gemini_api` and `gemini_api_disabled` checks for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) - `cloudfront_distributions_logging_enabled` detects Standard Logging v2 via CloudWatch Log Delivery [(#10090)](https://github.com/prowler-cloud/prowler/pull/10090) - `glue_etl_jobs_no_secrets_in_arguments` check for plaintext secrets in AWS Glue ETL job arguments [(#10368)](https://github.com/prowler-cloud/prowler/pull/10368) - `awslambda_function_no_dead_letter_queue`, `awslambda_function_using_cross_account_layers`, and `awslambda_function_env_vars_not_encrypted_with_cmk` checks for AWS Lambda [(#10381)](https://github.com/prowler-cloud/prowler/pull/10381) @@ -14,8 +15,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_conditional_access_policy_block_o365_elevated_insider_risk` check for M365 provider [(#10232)](https://github.com/prowler-cloud/prowler/pull/10232) - `--resource-group` and `--list-resource-groups` CLI flags to filter checks by resource group across all providers [(#10479)](https://github.com/prowler-cloud/prowler/pull/10479) - CIS Google Workspace Foundations Benchmark v1.3.0 compliance [(#10462)](https://github.com/prowler-cloud/prowler/pull/10462) -- `apikeys_api_restricted_with_gemini_api` check for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) -- `gemini_api_disabled` check for GCP provider [(#10280)](https://github.com/prowler-cloud/prowler/pull/10280) +- `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) ### 🔄 Changed diff --git a/prowler/compliance/m365/cis_6.0_m365.json b/prowler/compliance/m365/cis_6.0_m365.json index 641159443a..f13c323df4 100644 --- a/prowler/compliance/m365/cis_6.0_m365.json +++ b/prowler/compliance/m365/cis_6.0_m365.json @@ -1584,7 +1584,7 @@ "Id": "5.2.2.11", "Description": "Sign-in frequency defines the time period before a user is asked to sign in again when attempting to access a resource. Ensure sign-in frequency for Intune Enrollment is set to 'Every time'.", "Checks": [ - "entra_admin_users_sign_in_frequency_enabled" + "entra_intune_enrollment_sign_in_frequency_every_time" ], "Attributes": [ { diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 88c571ddd7..827ceca474 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -246,6 +246,8 @@ "entra_break_glass_account_fido2_security_key_registered", "entra_default_app_management_policy_enabled", "entra_all_apps_conditional_access_coverage", + "entra_conditional_access_policy_device_registration_mfa_required", + "entra_intune_enrollment_sign_in_frequency_every_time", "entra_conditional_access_policy_device_code_flow_blocked", "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", @@ -623,6 +625,8 @@ "entra_admin_users_phishing_resistant_mfa_enabled", "entra_conditional_access_policy_approved_client_app_required_for_mobile", "entra_conditional_access_policy_app_enforced_restrictions", + "entra_conditional_access_policy_device_registration_mfa_required", + "entra_intune_enrollment_sign_in_frequency_every_time", "entra_managed_device_required_for_authentication", "entra_managed_device_required_for_mfa_registration", "entra_users_mfa_capable", @@ -700,6 +704,8 @@ "entra_admin_users_mfa_enabled", "entra_admin_users_sign_in_frequency_enabled", "entra_all_apps_conditional_access_coverage", + "entra_conditional_access_policy_device_registration_mfa_required", + "entra_intune_enrollment_sign_in_frequency_every_time", "entra_break_glass_account_fido2_security_key_registered", "entra_conditional_access_policy_approved_client_app_required_for_mobile", "entra_conditional_access_policy_device_code_flow_blocked", diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.metadata.json new file mode 100644 index 0000000000..fe2985fa09 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_device_registration_mfa_required", + "CheckTitle": "Conditional Access policies enforce MFA for device registration", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **Conditional Access** policies can require **multifactor authentication (MFA)** for **device registration** operations.\n\nThis control ensures users must complete MFA before **registering or joining devices** to the directory, reducing the likelihood that compromised credentials can be used to register rogue devices.", + "Risk": "Without MFA for device registration, attackers with stolen credentials could register unauthorized devices into the directory, gain persistence, and bypass compliance-based Conditional Access protections that rely on trusted device state.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-device-registration", + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#user-actions" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "1. Navigate to the Microsoft Entra admin center (https://entra.microsoft.com).\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, include **All users**.\n5. Under **Target resources**, select **User actions** and check **Register or join devices**.\n6. Under **Grant**, select **Grant access** and require **multifactor authentication**.\n7. Set the policy to **Report-only** until validated, then enable it.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enforce **MFA** through **Conditional Access** for **device registration** so users must verify identity before registering or joining devices to the directory.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_device_registration_mfa_required" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_managed_device_required_for_mfa_registration", + "entra_intune_enrollment_sign_in_frequency_every_time" + ], + "Notes": "Requires Entra ID P1 or later license." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.py new file mode 100644 index 0000000000..d137717584 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required.py @@ -0,0 +1,75 @@ +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 ( + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + UserAction, +) + + +class entra_conditional_access_policy_device_registration_mfa_required(Check): + """Ensure MFA is required for device registration.""" + + def execute(self) -> list[CheckReportM365]: + findings = [] + + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = ( + "No Conditional Access Policy requires MFA for device registration." + ) + + reporting_policy = None + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if "All" not in policy.conditions.user_conditions.included_users: + continue + + if ( + UserAction.REGISTER_DEVICE + not in policy.conditions.application_conditions.included_user_actions + ): + continue + + if ( + ConditionalAccessGrantControl.MFA + not in policy.grant_controls.built_in_controls + ): + continue + + if policy.state == ConditionalAccessPolicyState.ENABLED: + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + report.status = "PASS" + report.status_extended = ( + f"Conditional Access Policy '{policy.display_name}' enforces MFA " + "for device registration." + ) + break + + if ( + policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING + and reporting_policy is None + ): + reporting_policy = policy + + if report.status == "FAIL" and reporting_policy: + report.status_extended = ( + f"Conditional Access Policy '{reporting_policy.display_name}' reports " + "MFA for device registration but does not enforce it." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json b/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json index b54a720615..936b973c11 100644 --- a/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json +++ b/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.metadata.json @@ -1,7 +1,7 @@ { "Provider": "m365", "CheckID": "entra_intune_enrollment_sign_in_frequency_every_time", - "CheckTitle": "Conditional Access enforces Every Time sign-in frequency for Intune Enrollment", + "CheckTitle": "Conditional Access requires strong authentication and Every Time sign-in frequency for Intune Enrollment", "CheckType": [], "ServiceName": "entra", "SubServiceName": "", @@ -9,8 +9,8 @@ "Severity": "high", "ResourceType": "NotDefined", "ResourceGroup": "IAM", - "Description": "Microsoft Entra **Conditional Access** for **Microsoft Intune Enrollment** enforces the session control **sign-in frequency** set to `Every time` for all users.\n\nThis evaluates whether an active policy targets the Intune Enrollment app and requires reauthentication on each enrollment attempt.", - "Risk": "Absent `Every time` reauth at enrollment, attackers with stolen or replayed credentials can enroll rogue devices and obtain compliant access.\n\nImpacts:\n- Confidentiality: data exposure from unauthorized devices\n- Integrity: untrusted endpoints modifying resources\n- Availability: persistence via device-based access paths", + "Description": "Microsoft Entra **Conditional Access** for **Microsoft Intune Enrollment** must require **strong authentication** and set **sign-in frequency** to `Every time` for all users.\n\nThis check evaluates whether an active policy targets the Intune Enrollment app, requires MFA or authentication strength, and forces reauthentication on each enrollment attempt.", + "Risk": "Absent strong authentication and `Every time` reauthentication at enrollment, attackers with stolen or replayed credentials can enroll rogue devices and obtain compliant access.\n\nImpacts:\n- Confidentiality: data exposure from unauthorized devices\n- Integrity: untrusted endpoints modifying resources\n- Availability: persistence via device-based access paths", "RelatedUrl": "", "AdditionalURLs": [ "https://learn.microsoft.com/en-us/intune/intune-service/fundamentals/deployment-guide-enrollment", @@ -18,13 +18,13 @@ ], "Remediation": { "Code": { - "CLI": "az rest --method POST --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies --headers 'Content-Type=application/json' --body '{\"displayName\":\"Intune Enrollment - Every time\",\"state\":\"enabled\",\"conditions\":{\"users\":{\"includeUsers\":[\"All\"]},\"applications\":{\"includeApplications\":[\"d4ebce55-015a-49b5-a083-c84d1797ae8c\"]}},\"sessionControls\":{\"signInFrequency\":{\"isEnabled\":true,\"type\":\"everyTime\"}}}'", + "CLI": "az rest --method POST --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies --headers 'Content-Type=application/json' --body '{\"displayName\":\"Intune Enrollment - MFA and Every time\",\"state\":\"enabled\",\"conditions\":{\"users\":{\"includeUsers\":[\"All\"]},\"applications\":{\"includeApplications\":[\"d4ebce55-015a-49b5-a083-c84d1797ae8c\"]}},\"grantControls\":{\"operator\":\"OR\",\"builtInControls\":[\"mfa\"]},\"sessionControls\":{\"signInFrequency\":{\"isEnabled\":true,\"type\":\"everyTime\"}}}'", "NativeIaC": "", - "Other": "1. Sign in to Microsoft Entra admin center (entra.microsoft.com)\n2. Go to Protection > Conditional Access > Policies > New policy\n3. Users > Include: select All users\n4. Target resources (Resources/Cloud apps) > Select resources: choose Microsoft Intune Enrollment\n5. Session > Sign-in frequency: select Every time\n6. Enable policy: On\n7. Create the policy", + "Other": "1. Sign in to Microsoft Entra admin center (entra.microsoft.com)\n2. Go to Protection > Conditional Access > Policies > New policy\n3. Users > Include: select All users\n4. Target resources (Resources/Cloud apps) > Select resources: choose Microsoft Intune Enrollment (App ID: `d4ebce55-015a-49b5-a083-c84d1797ae8c`)\n5. Grant > Grant access: select either Require multifactor authentication or Require authentication strength\n6. Session > Sign-in frequency: select Every time\n7. Enable policy: On\n8. Create the policy", "Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"\" {\n display_name = \"\"\n state = \"enabled\"\n\n conditions {\n users {\n include_users = [\"All\"] # critical: include all users\n }\n applications {\n include_applications = [\"d4ebce55-015a-49b5-a083-c84d1797ae8c\"] # critical: target Microsoft Intune Enrollment app\n }\n }\n\n session_controls {\n sign_in_frequency {\n is_enabled = true # critical: enable sign-in frequency control\n type = \"everyTime\" # critical: require reauthentication every time\n }\n }\n}\n```" }, "Recommendation": { - "Text": "Implement a **Conditional Access** policy on the **Intune Enrollment** app that sets sign-in frequency to `Every time` and applies broadly.\n\nCombine with **MFA** and device **compliance** requirements, use **least privilege** exclusions sparingly, and monitor sign-in/audit logs to strengthen **defense in depth**.", + "Text": "Implement a **Conditional Access** policy on the **Intune Enrollment** app that requires **MFA** or **authentication strength** and sets sign-in frequency to `Every time`.\n\nMicrosoft Entra requires this grant control when `Every time` is configured for Intune Enrollment, so Prowler validates both conditions together in a single check.", "Url": "https://hub.prowler.com/check/entra_intune_enrollment_sign_in_frequency_every_time" } }, @@ -34,5 +34,5 @@ ], "DependsOn": [], "RelatedTo": [], - "Notes": "" + "Notes": "This check intentionally validates both the grant control and session control together. Microsoft Entra requires `Require multifactor authentication` or `Require authentication strength` when `Sign-in frequency = Every time` is configured for Microsoft Intune Enrollment, so these conditions cannot be meaningfully separated into independent policies for this scenario." } diff --git a/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.py b/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.py index 8fc1ddbbc3..54e0a26fb4 100644 --- a/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.py +++ b/prowler/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time.py @@ -1,13 +1,15 @@ 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 ( + ConditionalAccessGrantControl, ConditionalAccessPolicyState, + GrantControlOperator, SignInFrequencyInterval, ) class entra_intune_enrollment_sign_in_frequency_every_time(Check): - """Ensure sign-in frequency for Intune Enrollment is set to 'Every time'.""" + """Ensure Intune enrollment enforces strong auth and Every Time sign-in.""" def execute(self) -> list[CheckReportM365]: """Execute the check to ensure that sign-in frequency for Intune Enrollment is set to 'Every time'. @@ -24,7 +26,10 @@ class entra_intune_enrollment_sign_in_frequency_every_time(Check): resource_id="conditionalAccessPolicies", ) report.status = "FAIL" - report.status_extended = "No Conditional Access Policy enforces Every Time sign-in frequency for Intune Enrollment." + report.status_extended = ( + "No Conditional Access Policy requires strong authentication and " + "enforces Every Time sign-in frequency for Intune Enrollment." + ) for policy in entra_client.conditional_access_policies.values(): if policy.state == ConditionalAccessPolicyState.DISABLED: @@ -45,6 +50,23 @@ class entra_intune_enrollment_sign_in_frequency_every_time(Check): if "All" not in policy.conditions.user_conditions.included_users: continue + requires_mfa = ( + ConditionalAccessGrantControl.MFA + in policy.grant_controls.built_in_controls + ) + requires_authentication_strength = ( + policy.grant_controls.authentication_strength is not None + ) + + if not (requires_mfa or requires_authentication_strength): + continue + + if ( + policy.grant_controls.operator == GrantControlOperator.OR + and len(policy.grant_controls.built_in_controls) > 1 + ): + continue + if not policy.session_controls.sign_in_frequency.is_enabled: continue @@ -60,10 +82,18 @@ class entra_intune_enrollment_sign_in_frequency_every_time(Check): ) if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: report.status = "FAIL" - report.status_extended = f"Conditional Access Policy {policy.display_name} reports Every Time sign-in frequency for Intune Enrollment but does not enforce it." + report.status_extended = ( + f"Conditional Access Policy '{policy.display_name}' reports " + "strong authentication and Every Time sign-in frequency for " + "Intune Enrollment but does not enforce them." + ) else: report.status = "PASS" - report.status_extended = f"Conditional Access Policy {policy.display_name} enforces Every Time sign-in frequency for Intune Enrollment." + report.status_extended = ( + f"Conditional Access Policy '{policy.display_name}' requires " + "strong authentication and enforces Every Time sign-in " + "frequency for Intune Enrollment." + ) break findings.append(report) diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required_test.py new file mode 100644 index 0000000000..e8060b5ee2 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_device_registration_mfa_required/entra_conditional_access_policy_device_registration_mfa_required_test.py @@ -0,0 +1,276 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + UserAction, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required" + + +def build_policy( + *, + display_name: str, + state: ConditionalAccessPolicyState, + included_users: list[str] | None = None, + included_user_actions: list[UserAction] | None = None, + built_in_controls: list[ConditionalAccessGrantControl] | None = None, +): + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + return ConditionalAccessPolicy( + id=str(uuid4()), + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=[], + excluded_applications=[], + included_user_actions=included_user_actions or [], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=included_users or ["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=GrantControls( + built_in_controls=built_in_controls or [], + operator=GrantControlOperator.OR, + authentication_strength=None, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=False, + frequency=None, + type=None, + interval=None, + ), + ), + state=state, + ) + + +class Test_entra_conditional_access_policy_device_registration_mfa_required: + def test_no_conditional_access_policies(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required import ( + entra_conditional_access_policy_device_registration_mfa_required, + ) + + entra_client.conditional_access_policies = {} + + result = ( + entra_conditional_access_policy_device_registration_mfa_required().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for device registration." + ) + + def test_enabled_policy_requires_mfa_for_device_registration(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required import ( + entra_conditional_access_policy_device_registration_mfa_required, + ) + + policy = build_policy( + display_name="Device registration MFA", + state=ConditionalAccessPolicyState.ENABLED, + included_user_actions=[UserAction.REGISTER_DEVICE], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_device_registration_mfa_required().execute() + ) + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Conditional Access Policy 'Device registration MFA' enforces MFA for device registration." + ) + assert result[0].resource_id == policy.id + + def test_reporting_only_policy_fails(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required import ( + entra_conditional_access_policy_device_registration_mfa_required, + ) + + policy = build_policy( + display_name="Device registration MFA", + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + included_user_actions=[UserAction.REGISTER_DEVICE], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_device_registration_mfa_required().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Conditional Access Policy 'Device registration MFA' reports MFA for device registration but does not enforce it." + ) + + def test_policy_not_targeting_all_users_fails(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required import ( + entra_conditional_access_policy_device_registration_mfa_required, + ) + + policy = build_policy( + display_name="Scoped device registration MFA", + state=ConditionalAccessPolicyState.ENABLED, + included_users=[str(uuid4())], + included_user_actions=[UserAction.REGISTER_DEVICE], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_device_registration_mfa_required().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for device registration." + ) + + def test_disabled_policy_is_skipped(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required import ( + entra_conditional_access_policy_device_registration_mfa_required, + ) + + policy = build_policy( + display_name="Disabled device registration MFA", + state=ConditionalAccessPolicyState.DISABLED, + included_user_actions=[UserAction.REGISTER_DEVICE], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_device_registration_mfa_required().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for device registration." + ) + + def test_policy_without_mfa_grant_control_fails(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_device_registration_mfa_required.entra_conditional_access_policy_device_registration_mfa_required import ( + entra_conditional_access_policy_device_registration_mfa_required, + ) + + policy = build_policy( + display_name="Device registration without MFA", + state=ConditionalAccessPolicyState.ENABLED, + included_user_actions=[UserAction.REGISTER_DEVICE], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = ( + entra_conditional_access_policy_device_registration_mfa_required().execute() + ) + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires MFA for device registration." + ) diff --git a/tests/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time_test.py b/tests/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time_test.py index 32fca532d2..3cd9adaaf7 100644 --- a/tests/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time_test.py +++ b/tests/providers/m365/services/entra/entra_intune_enrollment_sign_in_frequency_every_time/entra_intune_enrollment_sign_in_frequency_every_time_test.py @@ -4,6 +4,7 @@ from uuid import uuid4 from prowler.providers.m365.services.entra.entra_service import ( ApplicationEnforcedRestrictions, ApplicationsConditions, + ConditionalAccessGrantControl, ConditionalAccessPolicyState, Conditions, GrantControlOperator, @@ -17,21 +18,85 @@ from prowler.providers.m365.services.entra.entra_service import ( ) from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider +CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time" +INTUNE_ENROLLMENT_APP_ID = "d4ebce55-015a-49b5-a083-c84d1797ae8c" +MICROSOFT_INTUNE_APP_ID = "0000000a-0000-0000-c000-000000000000" + + +def build_policy( + *, + display_name: str, + state: ConditionalAccessPolicyState, + included_users: list[str] | None = None, + included_applications: list[str] | None = None, + excluded_applications: list[str] | None = None, + built_in_controls: list[ConditionalAccessGrantControl] | None = None, + operator: GrantControlOperator = GrantControlOperator.OR, + authentication_strength: str | None = None, + sign_in_frequency_enabled: bool = True, + sign_in_frequency_interval: ( + SignInFrequencyInterval | None + ) = SignInFrequencyInterval.EVERY_TIME, +): + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + return ConditionalAccessPolicy( + id=str(uuid4()), + display_name=display_name, + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=included_applications or [], + excluded_applications=excluded_applications or [], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=included_users or ["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + ), + grant_controls=GrantControls( + built_in_controls=built_in_controls or [], + operator=operator, + authentication_strength=authentication_strength, + ), + session_controls=SessionControls( + persistent_browser=PersistentBrowser(is_enabled=False, mode="always"), + sign_in_frequency=SignInFrequency( + is_enabled=sign_in_frequency_enabled, + frequency=None, + type=( + None + if sign_in_frequency_interval == SignInFrequencyInterval.EVERY_TIME + else SignInFrequencyType.HOURS + ), + interval=sign_in_frequency_interval, + ), + application_enforced_restrictions=ApplicationEnforcedRestrictions( + is_enabled=False + ), + ), + state=state, + ) + class Test_entra_intune_enrollment_sign_in_frequency_every_time: - def test_entra_no_conditional_access_policies(self): + def test_no_conditional_access_policies(self): entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN + 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_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time.entra_client", - new=entra_client, - ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), ): from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( entra_intune_enrollment_sign_in_frequency_every_time, @@ -39,21 +104,16 @@ class Test_entra_intune_enrollment_sign_in_frequency_every_time: entra_client.conditional_access_policies = {} - check = entra_intune_enrollment_sign_in_frequency_every_time() - result = check.execute() + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() + assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == "No Conditional Access Policy enforces Every Time sign-in frequency for Intune Enrollment." + == "No Conditional Access Policy requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." ) - assert result[0].resource == {} - assert result[0].resource_name == "Conditional Access Policies" - assert result[0].resource_id == "conditionalAccessPolicies" - assert result[0].location == "global" - def test_entra_intune_enrollment_sign_in_frequency_every_time_disabled(self): - id = str(uuid4()) + def test_enabled_policy_requires_mfa_and_every_time(self): entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN @@ -63,76 +123,30 @@ class Test_entra_intune_enrollment_sign_in_frequency_every_time: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), - mock.patch( - "prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time.entra_client", - new=entra_client, - ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), ): from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( entra_intune_enrollment_sign_in_frequency_every_time, ) - from prowler.providers.m365.services.entra.entra_service import ( - ConditionalAccessPolicy, + + policy = build_policy( + display_name="Intune Enrollment Every Time", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=[INTUNE_ENROLLMENT_APP_ID], + built_in_controls=[ConditionalAccessGrantControl.MFA], ) + entra_client.conditional_access_policies = {policy.id: policy} - entra_client.conditional_access_policies = { - id: ConditionalAccessPolicy( - id=id, - display_name="Test", - conditions=Conditions( - application_conditions=ApplicationsConditions( - included_applications=[ - "d4ebce55-015a-49b5-a083-c84d1797ae8c" - ], # Intune Enrollment - excluded_applications=[], - included_user_actions=[], - ), - user_conditions=UsersConditions( - included_groups=[], - excluded_groups=[], - included_users=["All"], - excluded_users=[], - included_roles=[], - excluded_roles=[], - ), - ), - grant_controls=GrantControls( - built_in_controls=[], operator=GrantControlOperator.AND - ), - session_controls=SessionControls( - persistent_browser=PersistentBrowser( - is_enabled=False, mode="always" - ), - sign_in_frequency=SignInFrequency( - is_enabled=False, - frequency=None, - type=None, - interval=SignInFrequencyInterval.EVERY_TIME, - ), - application_enforced_restrictions=ApplicationEnforcedRestrictions( - is_enabled=False - ), - ), - state=ConditionalAccessPolicyState.ENABLED, - ) - } + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() - check = entra_intune_enrollment_sign_in_frequency_every_time() - result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert ( result[0].status_extended - == "No Conditional Access Policy enforces Every Time sign-in frequency for Intune Enrollment." + == "Conditional Access Policy 'Intune Enrollment Every Time' requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." ) - assert result[0].resource == {} - assert result[0].resource_name == "Conditional Access Policies" - assert result[0].resource_id == "conditionalAccessPolicies" - assert result[0].location == "global" - def test_entra_intune_sign_in_frequency_every_time_enabled(self): - id = str(uuid4()) - display_name = "Test Intune Enrollment Policy" + def test_enabled_policy_with_authentication_strength_passes(self): entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN @@ -142,76 +156,30 @@ class Test_entra_intune_enrollment_sign_in_frequency_every_time: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), - mock.patch( - "prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time.entra_client", - new=entra_client, - ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), ): from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( entra_intune_enrollment_sign_in_frequency_every_time, ) - from prowler.providers.m365.services.entra.entra_service import ( - ConditionalAccessPolicy, + + policy = build_policy( + display_name="Intune Enrollment Auth Strength", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=[INTUNE_ENROLLMENT_APP_ID], + authentication_strength="Multifactor authentication", ) + entra_client.conditional_access_policies = {policy.id: policy} - entra_client.conditional_access_policies = { - id: ConditionalAccessPolicy( - id=id, - display_name=display_name, - conditions=Conditions( - application_conditions=ApplicationsConditions( - included_applications=[ - "0000000a-0000-0000-c000-000000000000" - ], # Intune Enrollment - excluded_applications=[], - included_user_actions=[], - ), - user_conditions=UsersConditions( - included_groups=[], - excluded_groups=[], - included_users=["All"], - excluded_users=[], - included_roles=[], - excluded_roles=[], - ), - ), - grant_controls=GrantControls( - built_in_controls=[], operator=GrantControlOperator.AND - ), - session_controls=SessionControls( - persistent_browser=PersistentBrowser( - is_enabled=True, mode="never" - ), - sign_in_frequency=SignInFrequency( - is_enabled=True, - frequency=None, - type=None, - interval=SignInFrequencyInterval.EVERY_TIME, - ), - application_enforced_restrictions=ApplicationEnforcedRestrictions( - is_enabled=False - ), - ), - state=ConditionalAccessPolicyState.ENABLED, - ) - } + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() - check = entra_intune_enrollment_sign_in_frequency_every_time() - result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert ( result[0].status_extended - == "No Conditional Access Policy enforces Every Time sign-in frequency for Intune Enrollment." + == "Conditional Access Policy 'Intune Enrollment Auth Strength' requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." ) - assert result[0].resource == {} - assert result[0].resource_name == "Conditional Access Policies" - assert result[0].resource_id == "conditionalAccessPolicies" - assert result[0].location == "global" - def test_entra_all_users_intune_enrollment_4hours(self): - id = str(uuid4()) - display_name = "Test All Users Policy" + def test_policy_without_strong_auth_fails(self): entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN @@ -221,74 +189,29 @@ class Test_entra_intune_enrollment_sign_in_frequency_every_time: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), - mock.patch( - "prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time.entra_client", - new=entra_client, - ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), ): from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( entra_intune_enrollment_sign_in_frequency_every_time, ) - from prowler.providers.m365.services.entra.entra_service import ( - ConditionalAccessPolicy, + + policy = build_policy( + display_name="Every Time Only", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=[INTUNE_ENROLLMENT_APP_ID], ) + entra_client.conditional_access_policies = {policy.id: policy} - entra_client.conditional_access_policies = { - id: ConditionalAccessPolicy( - id=id, - display_name=display_name, - conditions=Conditions( - application_conditions=ApplicationsConditions( - included_applications=["All"], - excluded_applications=[], - included_user_actions=[], - ), - user_conditions=UsersConditions( - included_groups=[], - excluded_groups=[], - included_users=["All"], - excluded_users=[], - included_roles=[], - excluded_roles=[], - ), - ), - grant_controls=GrantControls( - built_in_controls=[], operator=GrantControlOperator.AND - ), - session_controls=SessionControls( - persistent_browser=PersistentBrowser( - is_enabled=True, mode="never" - ), - sign_in_frequency=SignInFrequency( - is_enabled=True, - frequency=4, - type=SignInFrequencyType.HOURS, - interval=SignInFrequencyInterval.TIME_BASED, - ), - application_enforced_restrictions=ApplicationEnforcedRestrictions( - is_enabled=False - ), - ), - state=ConditionalAccessPolicyState.ENABLED, - ) - } + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() - check = entra_intune_enrollment_sign_in_frequency_every_time() - result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == "No Conditional Access Policy enforces Every Time sign-in frequency for Intune Enrollment." + == "No Conditional Access Policy requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." ) - assert result[0].resource == {} - assert result[0].resource_name == "Conditional Access Policies" - assert result[0].resource_id == "conditionalAccessPolicies" - assert result[0].location == "global" - def test_entra_intune_enrollment_enabled_for_reporting(self): - id = str(uuid4()) - display_name = "Test Report-Only Policy" + def test_policy_without_every_time_fails(self): entra_client = mock.MagicMock entra_client.audited_tenant = "audited_tenant" entra_client.audited_domain = DOMAIN @@ -298,69 +221,128 @@ class Test_entra_intune_enrollment_sign_in_frequency_every_time: "prowler.providers.common.provider.Provider.get_global_provider", return_value=set_mocked_m365_provider(), ), - mock.patch( - "prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time.entra_client", - new=entra_client, - ), + mock.patch(f"{CHECK_MODULE_PATH}.entra_client", new=entra_client), ): from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( entra_intune_enrollment_sign_in_frequency_every_time, ) - from prowler.providers.m365.services.entra.entra_service import ( - ConditionalAccessPolicy, + + policy = build_policy( + display_name="MFA Without Every Time", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=[INTUNE_ENROLLMENT_APP_ID], + built_in_controls=[ConditionalAccessGrantControl.MFA], + sign_in_frequency_interval=SignInFrequencyInterval.TIME_BASED, ) + entra_client.conditional_access_policies = {policy.id: policy} - entra_client.conditional_access_policies = { - id: ConditionalAccessPolicy( - id=id, - display_name=display_name, - conditions=Conditions( - application_conditions=ApplicationsConditions( - included_applications=[ - "d4ebce55-015a-49b5-a083-c84d1797ae8c" - ], # Intune Enrollment - excluded_applications=[], - included_user_actions=[], - ), - user_conditions=UsersConditions( - included_groups=[], - excluded_groups=[], - included_users=["All"], - excluded_users=[], - included_roles=[], - excluded_roles=[], - ), - ), - grant_controls=GrantControls( - built_in_controls=[], operator=GrantControlOperator.AND - ), - session_controls=SessionControls( - persistent_browser=PersistentBrowser( - is_enabled=True, mode="never" - ), - sign_in_frequency=SignInFrequency( - is_enabled=True, - frequency=None, - type=None, - interval=SignInFrequencyInterval.EVERY_TIME, - ), - application_enforced_restrictions=ApplicationEnforcedRestrictions( - is_enabled=False - ), - ), - state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, - ) - } + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() - check = entra_intune_enrollment_sign_in_frequency_every_time() - result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Conditional Access Policy {display_name} reports Every Time sign-in frequency for Intune Enrollment but does not enforce it." + == "No Conditional Access Policy requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." + ) + + def test_policy_with_microsoft_intune_app_id_fails(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( + entra_intune_enrollment_sign_in_frequency_every_time, + ) + + policy = build_policy( + display_name="Microsoft Intune Policy", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=[MICROSOFT_INTUNE_APP_ID], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." + ) + + def test_policy_with_or_controls_does_not_require_mfa(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( + entra_intune_enrollment_sign_in_frequency_every_time, + ) + + policy = build_policy( + display_name="Intune MFA or managed device", + state=ConditionalAccessPolicyState.ENABLED, + included_applications=[INTUNE_ENROLLMENT_APP_ID], + built_in_controls=[ + ConditionalAccessGrantControl.MFA, + ConditionalAccessGrantControl.DOMAIN_JOINED_DEVICE, + ], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy requires strong authentication and enforces Every Time sign-in frequency for Intune Enrollment." + ) + + def test_reporting_only_policy_fails(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_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}.entra_client", new=entra_client), + ): + from prowler.providers.m365.services.entra.entra_intune_enrollment_sign_in_frequency_every_time.entra_intune_enrollment_sign_in_frequency_every_time import ( + entra_intune_enrollment_sign_in_frequency_every_time, + ) + + policy = build_policy( + display_name="Intune Enrollment Report Only", + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + included_applications=[INTUNE_ENROLLMENT_APP_ID], + built_in_controls=[ConditionalAccessGrantControl.MFA], + ) + entra_client.conditional_access_policies = {policy.id: policy} + + result = entra_intune_enrollment_sign_in_frequency_every_time().execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Conditional Access Policy 'Intune Enrollment Report Only' reports strong authentication and Every Time sign-in frequency for Intune Enrollment but does not enforce them." ) - assert result[0].resource == entra_client.conditional_access_policies[id] - assert result[0].resource_name == display_name - assert result[0].resource_id == id - assert result[0].location == "global"