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..cd7fb9b828 --- /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,41 @@ +{ + "Provider": "m365", + "CheckID": "entra_conditional_access_policy_all_apps_all_users", + "CheckTitle": "Conditional Access policy covers all cloud apps and all users for baseline protection", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "Microsoft Entra **Conditional Access** is verified to have at least one **enabled** policy that targets **all cloud applications** and **all users**, providing a baseline security posture across the entire tenant.\n\nPolicies that only require a password change are excluded as they do not provide meaningful access controls.", + "Risk": "Without a Conditional Access policy covering all apps and all users, **newly added applications** and **user accounts** may bypass security controls entirely. Attackers could exploit unprotected apps or temporary accounts to gain unauthorized access to organizational resources.", + "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", + "https://maester.dev/docs/tests/MT.1004" + ], + "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 (e.g., **Require multifactor authentication**).\n7. Set the policy to **On** and click **Create**.", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create a Conditional Access policy that targets **all cloud apps** and **all users** to ensure baseline protection. Exclude only break-glass accounts. This prevents security gaps when new apps or users are added to the tenant.", + "Url": "https://hub.prowler.com/check/entra_conditional_access_policy_all_apps_all_users" + } + }, + "Categories": [ + "identity-access", + "e3" + ], + "DependsOn": [], + "RelatedTo": [ + "entra_conditional_access_policy_require_mfa_for_management_api" + ], + "Notes": "This check is equivalent to Maester test MT.1004 (Test-MtCaAllAppsExists). 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..e214f9c7a6 --- /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,76 @@ +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. + - 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." + + for policy in entra_client.conditional_access_policies.values(): + if policy.state == ConditionalAccessPolicyState.DISABLED: + continue + + if not policy.conditions.application_conditions: + continue + + if ( + "All" + not in policy.conditions.application_conditions.included_applications + ): + continue + + if "All" not in policy.conditions.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 + + report = CheckReportM365( + metadata=self.metadata(), + resource=policy, + resource_name=policy.display_name, + resource_id=policy.id, + ) + + if policy.state == ConditionalAccessPolicyState.ENABLED_FOR_REPORTING: + report.status = "FAIL" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' covers all cloud apps and all users but is only in report-only mode." + else: + report.status = "PASS" + report.status_extended = f"Conditional Access Policy '{policy.display_name}' covers all cloud apps and all users." + break + + 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..bf19780fc5 --- /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,575 @@ +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, +): + """Return Conditions with the given application and user scopes.""" + return Conditions( + application_conditions=ApplicationsConditions( + included_applications=included_applications or ["All"], + excluded_applications=[], + included_user_actions=[], + ), + 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=[], + ) + + +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_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_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_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"