From 4941ed57975c2a6266616e8729b3e0de70e08392 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:47:57 +0100 Subject: [PATCH] feat(entra): add new check `entra_conditional_access_policy_all_apps_all_users` (#10619) Co-authored-by: Hugo P.Brito --- prowler/CHANGELOG.md | 1 + .../__init__.py | 0 ...ss_policy_all_apps_all_users.metadata.json | 37 + ...tional_access_policy_all_apps_all_users.py | 121 +++ ...l_access_policy_all_apps_all_users_test.py | 858 ++++++++++++++++++ 5 files changed, 1017 insertions(+) create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/__init__.py create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json create mode 100644 prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py create mode 100644 tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 9a7bece29d..d5e5feb8e5 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 +- `entra_conditional_access_policy_all_apps_all_users` check for M365 provider [(#10619)](https://github.com/prowler-cloud/prowler/pull/10619) - `bedrock_full_access_policy_attached` check for AWS provider [(#10577)](https://github.com/prowler-cloud/prowler/pull/10577) - `iam_role_access_not_stale_to_bedrock` and `iam_user_access_not_stale_to_bedrock` checks for AWS provider [(#10536)](https://github.com/prowler-cloud/prowler/pull/10536) - `iam_policy_no_wildcard_marketplace_subscribe` and `iam_inline_policy_no_wildcard_marketplace_subscribe` checks for AWS provider [(#10525)](https://github.com/prowler-cloud/prowler/pull/10525) diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/__init__.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json new file mode 100644 index 0000000000..3df70aea46 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_all_apps_all_users", + "CheckTitle": "Conditional Access policy covers all cloud apps and all users", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Helper check that verifies whether **Microsoft Entra Conditional Access** includes at least one **enabled** policy scoped to **all cloud applications** and **all users**. Policies that only require a **password change** and **report-only** policies are excluded from passing. The result is intended to support other, more specific Conditional Access checks rather than enforce a baseline on its own.", + "Risk": "Without a Conditional Access policy that targets **all cloud apps** and **all users**, newly added applications or user accounts may not be covered by existing access controls until policies are explicitly updated.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/entra/identity/conditional-access/plan-conditional-access", + "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 at https://entra.microsoft.com.\n2. Expand **Protection** > **Conditional Access** and select **Policies**.\n3. Click **New policy**.\n4. Under **Users**, select **Include** > **All users**. Exclude break-glass accounts as needed.\n5. Under **Target resources**, select **Include** > **All cloud apps**.\n6. Under **Grant**, select the desired access controls.\n7. Set the policy to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure there is at least one **enabled** Conditional Access policy that targets **all cloud apps** and **all users** so that other Conditional Access checks have broad coverage to evaluate. Exclude only **break-glass accounts**.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_all_apps_all_users" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Generic helper check intended to support other Conditional Access checks. Conditional Access policies require Microsoft Entra ID P1 or P2 licenses." +} diff --git a/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py new file mode 100644 index 0000000000..ecf0968066 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users.py @@ -0,0 +1,121 @@ +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_conditional_access_policy_all_apps_all_users(Check): + """Check if at least one Conditional Access policy covers all cloud apps and all users. + + This check verifies that at least one enabled Conditional Access policy + targets all cloud applications and all users, ensuring baseline protection + across the entire tenant. Policies that only require a password change are + excluded because they do not provide meaningful access control. + + - PASS: An enabled Conditional Access policy covers all apps and all users with no exclusions. + - MANUAL: A policy targets all apps and all users but includes exclusions that require review. + - FAIL: No Conditional Access policy provides coverage for all apps and all users. + """ + + def execute(self) -> list[CheckReportM365]: + """Execute the check to verify Conditional Access coverage for all apps and all users. + + Returns: + A list of reports containing the result of the check. + """ + 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 covers all cloud apps and all users." + ) + + manual_policy = None + reporting_only_policy = None + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + application_conditions = policy.conditions.application_conditions + user_conditions = policy.conditions.user_conditions + if not application_conditions or not user_conditions: + continue + + if "All" not in application_conditions.included_applications: + continue + + if "All" not in user_conditions.included_users: + continue + + # Exclude policies that only require a password change, + # as they do not provide meaningful access control. + if policy.grant_controls.built_in_controls == [ + ConditionalAccessGrantControl.PASSWORD_CHANGE + ]: + continue + + has_exclusions = bool( + application_conditions.excluded_applications + or user_conditions.excluded_users + or user_conditions.excluded_groups + or user_conditions.excluded_roles + ) + + if has_exclusions: + if manual_policy is None: + manual_policy = policy + continue + + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + if reporting_only_policy is None: + reporting_only_policy = policy + continue + + 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} covers all cloud apps and all users." + findings.append(report) + return findings + + if manual_policy is not None: + report = CheckReportM365( + metadata=self.metadata(), + resource=manual_policy, + resource_name=manual_policy.display_name, + resource_id=manual_policy.id, + ) + report.status = "MANUAL" + report.status_extended = ( + f"Conditional Access Policy {manual_policy.display_name} " + "targets all cloud apps and all users but includes exclusions. " + "Review excluded users/groups/roles/applications and verify " + "compensating policies protect excluded identities and apps." + ) + elif reporting_only_policy is not None: + report = CheckReportM365( + metadata=self.metadata(), + resource=reporting_only_policy, + resource_name=reporting_only_policy.display_name, + resource_id=reporting_only_policy.id, + ) + report.status = "FAIL" + report.status_extended = ( + f"Conditional Access Policy {reporting_only_policy.display_name} " + "covers all cloud apps and all users but is only in report-only mode." + ) + + findings.append(report) + return findings diff --git a/tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py b/tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py new file mode 100644 index 0000000000..f9991b0e49 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_conditional_access_policy_all_apps_all_users/entra_conditional_access_policy_all_apps_all_users_test.py @@ -0,0 +1,858 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.m365.services.entra.entra_service import ( + ApplicationEnforcedRestrictions, + ApplicationsConditions, + ConditionalAccessGrantControl, + ConditionalAccessPolicyState, + Conditions, + GrantControlOperator, + GrantControls, + PersistentBrowser, + SessionControls, + SignInFrequency, + SignInFrequencyInterval, + 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_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users" + + +def _make_session_controls(): + """Return default session controls for test policies.""" + return 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 + ), + ) + + +def _make_conditions( + included_applications=None, + included_users=None, + excluded_applications=None, + excluded_users=None, + excluded_groups=None, + excluded_roles=None, +): + """Return Conditions with the given application and user scopes.""" + return Conditions( + application_conditions=ApplicationsConditions( + included_applications=included_applications or ["All"], + excluded_applications=excluded_applications or [], + included_user_actions=[], + ), + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=excluded_groups or [], + included_users=included_users or ["All"], + excluded_users=excluded_users or [], + included_roles=[], + excluded_roles=excluded_roles or [], + ), + client_app_types=[], + user_risk_levels=[], + ) + + +def _make_grant_controls(built_in_controls=None): + """Return GrantControls with the given built-in controls.""" + return GrantControls( + built_in_controls=built_in_controls or [ConditionalAccessGrantControl.MFA], + operator=GrantControlOperator.AND, + authentication_strength=None, + ) + + +class Test_entra_conditional_access_policy_all_apps_all_users: + 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_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + + entra_client.conditional_access_policies = {} + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + 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_id = str(uuid4()) + display_name = "All Apps All Users 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.DISABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + 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_excluding_roles(self): + policy_id = str(uuid4()) + display_name = "All Users Except Role 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + excluded_roles=["62e90394-69f5-4237-9190-012177145e10"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} targets all cloud apps and all users but includes exclusions. Review excluded users/groups/roles/applications and verify compensating policies protect excluded identities and apps." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_not_targeting_all_apps(self): + policy_id = str(uuid4()) + display_name = "Specific App 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + included_applications=["some-app-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + 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_users(self): + policy_id = str(uuid4()) + display_name = "Specific Users 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + included_users=["some-user-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + 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_excluding_applications(self): + policy_id = str(uuid4()) + display_name = "All Apps Except One 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + excluded_applications=["excluded-app-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} targets all cloud apps and all users but includes exclusions. Review excluded users/groups/roles/applications and verify compensating policies protect excluded identities and apps." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_excluding_users(self): + policy_id = str(uuid4()) + display_name = "All Users Except One 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions( + excluded_users=["excluded-user-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} targets all cloud apps and all users but includes exclusions. Review excluded users/groups/roles/applications and verify compensating policies protect excluded identities and apps." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_only_password_change(self): + policy_id = str(uuid4()) + display_name = "Password Change 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls( + built_in_controls=[ + ConditionalAccessGrantControl.PASSWORD_CHANGE + ], + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + 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(self): + policy_id = str(uuid4()) + display_name = "All Apps All Users - Report Only" + 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_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED_FOR_REPORTING, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} covers all cloud apps and all users but is only in report-only mode." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_policy_enabled_all_apps_all_users(self): + policy_id = str(uuid4()) + display_name = "All Apps All Users 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} covers all cloud apps and all users." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[policy_id].dict() + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_multiple_policies_first_disabled_second_enabled(self): + disabled_id = str(uuid4()) + enabled_id = str(uuid4()) + enabled_name = "All Apps All Users - Enabled" + 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_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + disabled_id: ConditionalAccessPolicy( + id=disabled_id, + display_name="All Apps All Users - Disabled", + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.DISABLED, + ), + enabled_id: ConditionalAccessPolicy( + id=enabled_id, + display_name=enabled_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {enabled_name} covers all cloud apps and all users." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[enabled_id].dict() + ) + assert result[0].resource_name == enabled_name + assert result[0].resource_id == enabled_id + assert result[0].location == "global" + + def test_policy_with_block_grant_control(self): + policy_id = str(uuid4()) + display_name = "Block All Apps All Users" + 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_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls( + built_in_controls=[ConditionalAccessGrantControl.BLOCK], + ), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {display_name} covers all cloud apps and all users." + ) + assert result[0].resource_name == display_name + assert result[0].resource_id == policy_id + assert result[0].location == "global" + + def test_manual_policy_does_not_prevent_later_pass(self): + manual_policy_id = str(uuid4()) + pass_policy_id = str(uuid4()) + manual_display_name = "All Apps All Users With Exclusions" + pass_display_name = "All Apps All Users Enforced" + 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_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + manual_policy_id: ConditionalAccessPolicy( + id=manual_policy_id, + display_name=manual_display_name, + conditions=_make_conditions( + excluded_users=["excluded-user-id"], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + pass_policy_id: ConditionalAccessPolicy( + id=pass_policy_id, + display_name=pass_display_name, + conditions=_make_conditions(), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ), + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Conditional Access Policy {pass_display_name} covers all cloud apps and all users." + ) + assert ( + result[0].resource + == entra_client.conditional_access_policies[pass_policy_id].dict() + ) + assert result[0].resource_name == pass_display_name + assert result[0].resource_id == pass_policy_id + + def test_policy_no_application_conditions(self): + policy_id = str(uuid4()) + display_name = "No App Conditions 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + entra_client.conditional_access_policies = { + policy_id: ConditionalAccessPolicy( + id=policy_id, + display_name=display_name, + conditions=Conditions( + application_conditions=None, + user_conditions=UsersConditions( + included_groups=[], + excluded_groups=[], + included_users=["All"], + excluded_users=[], + included_roles=[], + excluded_roles=[], + ), + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + 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_no_user_conditions(self): + policy_id = str(uuid4()) + display_name = "No User Conditions 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( + f"{CHECK_MODULE_PATH}.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_conditional_access_policy_all_apps_all_users.entra_conditional_access_policy_all_apps_all_users import ( + entra_conditional_access_policy_all_apps_all_users, + ) + from prowler.providers.m365.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + 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=None, + client_app_types=[], + user_risk_levels=[], + ), + grant_controls=_make_grant_controls(), + session_controls=_make_session_controls(), + state=ConditionalAccessPolicyState.ENABLED, + ) + } + + check = entra_conditional_access_policy_all_apps_all_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "No Conditional Access Policy covers all cloud apps and all users." + ) + assert result[0].resource == {} + assert result[0].resource_name == "Conditional Access Policies" + assert result[0].resource_id == "conditionalAccessPolicies" + assert result[0].location == "global"