From df09b14c75b14f840d92a0efc6a97dbcecbb34d3 Mon Sep 17 00:00:00 2001 From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:37:09 +0100 Subject: [PATCH] feat(m365): add entra_all_apps_conditional_access_coverage security check (#9902) Co-authored-by: HugoPBrito --- prowler/CHANGELOG.md | 1 + .../compliance/m365/iso27001_2022_m365.json | 9 +- .../__init__.py | 0 ..._conditional_access_coverage.metadata.json | 37 + ...ra_all_apps_conditional_access_coverage.py | 87 +++ ...l_apps_conditional_access_coverage_test.py | 715 ++++++++++++++++++ 6 files changed, 845 insertions(+), 4 deletions(-) create mode 100644 prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/__init__.py create mode 100644 prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json create mode 100644 prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.py create mode 100644 tests/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 754440d435..a8cfadb5ca 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -113,6 +113,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828) - CloudTrail Timeline abstraction for querying resource modification history [(#9101)](https://github.com/prowler-cloud/prowler/pull/9101) - Cloudflare `--account-id` filter argument [(#9894)](https://github.com/prowler-cloud/prowler/pull/9894) +- `entra_all_apps_conditional_access_coverage` check for M365 provider [(#9902)](https://github.com/prowler-cloud/prowler/pull/9902) - `rds_instance_extended_support` check for AWS provider [(#9865)](https://github.com/prowler-cloud/prowler/pull/9865) - `OpenStack` provider support with Compute service including 1 security check [(#9811)](https://github.com/prowler-cloud/prowler/pull/9811) - `OpenStack` documentation for the support in the CLI [(#9848)](https://github.com/prowler-cloud/prowler/pull/9848) diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 023dbdb230..64f3c0ff14 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -238,9 +238,9 @@ ], "Checks": [ "defenderxdr_endpoint_privileged_user_exposed_credentials", - "entra_admin_users_sign_in_frequency_enabled", "entra_admin_users_mfa_enabled", "entra_admin_users_sign_in_frequency_enabled", + "entra_all_apps_conditional_access_coverage", "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", @@ -686,12 +686,13 @@ } ], "Checks": [ - "entra_admin_users_sign_in_frequency_enabled", "entra_admin_users_mfa_enabled", + "entra_admin_users_sign_in_frequency_enabled", + "entra_all_apps_conditional_access_coverage", + "entra_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", "entra_seamless_sso_disabled", - "entra_users_mfa_enabled", - "entra_identity_protection_sign_in_risk_enabled" + "entra_users_mfa_enabled" ] }, { diff --git a/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/__init__.py b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json new file mode 100644 index 0000000000..d76c4e6991 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_all_apps_conditional_access_coverage", + "CheckTitle": "Conditional Access policy ensures comprehensive coverage for all cloud apps", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Conditional Access Policy", + "ResourceGroup": "IAM", + "Description": "At least one Conditional Access policy is configured to target **all cloud apps**. This ensures comprehensive security coverage and automatic protection for newly onboarded applications without requiring policy updates.", + "Risk": "Without a policy targeting **all cloud apps**, newly integrated applications may not be protected by **Conditional Access**. This creates security gaps where users could access sensitive resources without proper authentication controls.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + ], + "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. Create a new policy by selecting **New policy**.\n4. Under **Target resources**, select **All cloud apps**.\n5. Configure appropriate exclusions for applications that require different policies.\n6. Set the desired access controls (e.g., require MFA, compliant device).\n7. Set the policy to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create at least one **Conditional Access** policy that targets **all cloud apps** to ensure comprehensive protection. Use **exclusions** to handle applications requiring different access controls rather than creating narrow policies for each application.", + "Url": "https://hub.prowler.com/check/entra_all_apps_conditional_access_coverage" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.py b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.py new file mode 100644 index 0000000000..c11598e846 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage.py @@ -0,0 +1,87 @@ +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, +) + + +class entra_all_apps_conditional_access_coverage(Check): + """Check if at least one Conditional Access policy targets all cloud apps. + + This check iterates over all Conditional Access policies and collects those + that target all cloud applications. A single finding is produced listing + every matching policy name. + + - PASS: At least one fully enabled policy targets all cloud apps. + - FAIL: No policy targets all cloud apps, or only report-only policies do. + """ + + def execute(self) -> list[CheckReportM365]: + """Execute the check to verify all cloud apps coverage. + + Returns: + list[CheckReportM365]: A single-element list with the result. + """ + findings = [] + enabled_policies = [] + reporting_only_policies = [] + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + # Skip policies that require password change + if ( + ConditionalAccessGrantControl.PASSWORD_CHANGE + in policy.grant_controls.built_in_controls + ): + continue + + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + reporting_only_policies.append(policy) + else: + enabled_policies.append(policy) + + if enabled_policies: + policy_names = ", ".join(p.display_name for p in enabled_policies) + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "PASS" + report.status_extended = ( + f"Conditional Access Policies targeting all cloud apps: {policy_names}." + ) + elif reporting_only_policies: + policy_names = ", ".join(p.display_name for p in reporting_only_policies) + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = f"Conditional Access Policies targeting all cloud apps are only configured for reporting: {policy_names}." + else: + report = CheckReportM365( + metadata=self.metadata(), + resource={}, + resource_name="Conditional Access Policies", + resource_id="conditionalAccessPolicies", + ) + report.status = "FAIL" + report.status_extended = ( + "No Conditional Access Policy targets all cloud apps." + ) + + findings.append(report) + return findings diff --git a/tests/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage_test.py b/tests/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage_test.py new file mode 100644 index 0000000000..e4b3b209b6 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_all_apps_conditional_access_coverage/entra_all_apps_conditional_access_coverage_test.py @@ -0,0 +1,715 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicy, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + UsersConditions, +) +from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider + + +class Test_entra_all_apps_conditional_access_coverage: + def test_no_conditional_access_policies(self): + """No conditional access policies configured: expected FAIL.""" + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = {} + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all cloud apps." + ) + 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_policy_disabled(self): + """Policy in DISABLED state: expected to be ignored and return FAIL.""" + policy_id = str(uuid4()) + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="Disabled Policy", + 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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all cloud apps." + ) + 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_policy_not_targeting_all_apps(self): + """Policy does not target all apps: expected FAIL.""" + policy_id = str(uuid4()) + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="Specific Apps Policy", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["app-id-1", "app-id-2"], + 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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all cloud apps." + ) + 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_policy_with_password_change_requirement(self): + """Policy with password change requirement: expected to be skipped and return FAIL.""" + policy_id = str(uuid4()) + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name="Password Change Policy", + 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=[ + ConditionalAccessGrantControl.PASSWORD_CHANGE + ], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy targets all cloud apps." + ) + 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_policy_enabled_for_reporting_only(self): + """ + Policy targeting all apps but only enabled for reporting: + expected FAIL with specific message. + """ + policy_id = str(uuid4()) + display_name = "Reporting Only Policy" + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policies targeting all cloud apps are only configured for reporting: {display_name}." + ) + 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_policy_enabled_targeting_all_apps(self): + """ + Valid policy: + - State ENABLED + - Targets all cloud apps + - No password change requirement + + Expected PASS. + """ + policy_id = str(uuid4()) + display_name = "All Apps Policy" + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policies targeting all cloud apps: {display_name}." + ) + 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_policy_with_block_grant_control(self): + """ + Valid policy with block grant control: + - State ENABLED + - Targets all cloud apps + - Uses BLOCK grant control + + Expected PASS. + """ + policy_id = str(uuid4()) + display_name = "Block All Apps Policy" + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_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=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policies targeting all cloud apps: {display_name}." + ) + 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_multiple_policies_lists_all_enabled(self): + """ + Multiple policies: + - First policy is disabled (skipped) + - Second policy targets specific apps (skipped) + - Third and fourth policies are enabled and target all apps + + Expected: single PASS listing both enabled policy names. + """ + disabled_policy_id = str(uuid4()) + specific_apps_policy_id = str(uuid4()) + policy_a_id = str(uuid4()) + policy_a_name = "MFA All Apps" + policy_b_id = str(uuid4()) + policy_b_name = "Block All Apps" + + 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_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_all_apps_conditional_access_coverage.entra_all_apps_conditional_access_coverage import ( + entra_all_apps_conditional_access_coverage, + ) + + entra_client.conditional_access_policies = { + disabled_policy_id: ConditionalAccessPolicy( + id=disabled_policy_id, + display_name="Disabled Policy", + 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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.DISABLED, + ), + specific_apps_policy_id: ConditionalAccessPolicy( + id=specific_apps_policy_id, + display_name="Specific Apps Policy", + conditions=Conditions( + application_conditions=ApplicationsConditions( + included_applications=["app-id-1"], + 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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + policy_a_id: ConditionalAccessPolicy( + id=policy_a_id, + display_name=policy_a_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=[ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + policy_b_id: ConditionalAccessPolicy( + id=policy_b_id, + display_name=policy_b_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=[ConditionalAccessGrantControl.BLOCK], + operator=GrantControlOperator.AND, + 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=SignInFrequencyInterval.EVERY_TIME, + ), + ), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_all_apps_conditional_access_coverage() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert policy_a_name in result[0].status_extended + assert policy_b_name in result[0].status_extended + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global"