diff --git a/prowler/changelog.d/entra-admin-sspr-disabled.added.md b/prowler/changelog.d/entra-admin-sspr-disabled.added.md new file mode 100644 index 0000000000..9f46f08bde --- /dev/null +++ b/prowler/changelog.d/entra-admin-sspr-disabled.added.md @@ -0,0 +1 @@ +`entra_policy_admin_self_service_password_reset_disabled` check for M365 provider, verifying that Self-Service Password Reset is disabled for administrators diff --git a/prowler/compliance/m365/iso27001_2022_m365.json b/prowler/compliance/m365/iso27001_2022_m365.json index 784099be32..17b17c4704 100644 --- a/prowler/compliance/m365/iso27001_2022_m365.json +++ b/prowler/compliance/m365/iso27001_2022_m365.json @@ -258,6 +258,7 @@ "entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced", "entra_legacy_authentication_blocked", "entra_managed_device_required_for_authentication", + "entra_policy_admin_self_service_password_reset_disabled", "entra_seamless_sso_disabled", "entra_service_principal_no_secrets_for_permanent_tier0_roles", "entra_users_mfa_enabled", @@ -675,6 +676,7 @@ "entra_break_glass_account_fido2_security_key_registered", "entra_emergency_access_exclusion", "entra_app_registration_no_unused_privileged_permissions", + "entra_policy_admin_self_service_password_reset_disabled", "entra_policy_ensure_default_user_cannot_create_tenants", "entra_policy_guest_invite_only_for_admin_roles", "entra_policy_msol_powershell_blocked", @@ -735,6 +737,7 @@ "entra_emergency_access_exclusion", "entra_identity_protection_sign_in_risk_enabled", "entra_managed_device_required_for_authentication", + "entra_policy_admin_self_service_password_reset_disabled", "entra_seamless_sso_disabled", "entra_service_principal_no_secrets_for_permanent_tier0_roles", "entra_users_mfa_enabled" diff --git a/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/__init__.py b/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled.metadata.json b/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled.metadata.json new file mode 100644 index 0000000000..5e79cf9fe6 --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "m365", + "CheckID": "entra_policy_admin_self_service_password_reset_disabled", + "CheckTitle": "Disabled Self-Service Password Reset protects administrator accounts from unauthorized recovery", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "IAM", + "Description": "The tenant authorization policy property `allowedToUseSSPR` controls whether administrators can reset their own passwords through **Self-Service Password Reset (SSPR)**.\n\nWhen disabled, password recovery for privileged accounts must go through other administrators or break-glass procedures, reducing the risk of account takeover via compromised recovery methods.", + "Risk": "If SSPR is enabled for administrators, anyone who controls an admin's registered recovery methods (e.g., **SIM swap**, compromised alternate mailbox, social engineering) can reset the admin password and take over a **privileged account** without going through a controlled recovery process.\n\nThis directly threatens the **confidentiality** and **integrity** of the entire tenant.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/entra/identity/authentication/concept-sspr-policy#administrator-reset-policy-differences", + "https://learn.microsoft.com/graph/api/resources/authorizationpolicy", + "https://learn.microsoft.com/graph/api/authorizationpolicy-get" + ], + "Remediation": { + "Code": { + "CLI": "Update-MgPolicyAuthorizationPolicy -AllowedToUseSspr:$false", + "NativeIaC": "", + "Other": "Administrator SSPR cannot be disabled from the Microsoft Entra admin center: the **Password reset** settings there only apply to non-administrator users. Use Microsoft Graph PowerShell instead:\n1. Connect-MgGraph -Scopes Policy.ReadWrite.Authorization\n2. Update-MgPolicyAuthorizationPolicy -AllowedToUseSspr:$false\n3. Verify with (Get-MgPolicyAuthorizationPolicy).AllowedToUseSspr", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **Self-Service Password Reset** for administrator accounts. Privileged account password recovery should be handled by other administrators or **break-glass procedures** to prevent account takeover through compromised recovery methods.\n\nEnsure a documented recovery path exists before disabling SSPR for admins.", + "Url": "https://hub.prowler.com/check/entra_policy_admin_self_service_password_reset_disabled" + } + }, + "Categories": [ + "identity-access" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The service default for allowedToUseSSPR is true, so most tenants that never changed it will FAIL. Before disabling, ensure there is a documented recovery path for admin passwords (other admins, break-glass accounts). Requires Policy.ReadWrite.Authorization to remediate." +} diff --git a/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled.py b/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled.py new file mode 100644 index 0000000000..f1d3a5957f --- /dev/null +++ b/prowler/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled.py @@ -0,0 +1,64 @@ +"""Check that Self-Service Password Reset is disabled for administrators.""" + +from typing import List + +from prowler.lib.check.models import Check, CheckReportM365 +from prowler.providers.m365.services.entra.entra_client import entra_client + + +class entra_policy_admin_self_service_password_reset_disabled(Check): + """Ensure that Self-Service Password Reset (SSPR) is disabled for administrators. + + This check evaluates the tenant authorization policy to verify that the + ``allowedToUseSSPR`` property is set to ``false``, preventing administrators + from resetting their own passwords through SSPR. + + - PASS: ``allowedToUseSSPR`` is ``false`` (administrators cannot use SSPR). + - FAIL: ``allowedToUseSSPR`` is ``true`` (SSPR is enabled for administrators). + - MANUAL: The authorization policy or the property could not be read. + """ + + def execute(self) -> List[CheckReportM365]: + """Evaluate allowedToUseSSPR on the tenant authorization policy.""" + findings = [] + auth_policy = entra_client.authorization_policy + + if auth_policy is None or auth_policy.allowed_to_use_sspr is None: + report = CheckReportM365( + metadata=self.metadata(), + resource=auth_policy if auth_policy else {}, + resource_name="Authorization Policy", + resource_id="authorizationPolicy", + ) + report.status = "MANUAL" + report.status_extended = ( + "Cannot evaluate whether SSPR is disabled for administrators: the " + "authorization policy or its allowedToUseSSPR setting could not be " + "retrieved. Check the scanning application's permissions and any " + "Microsoft Graph API errors in the scan log." + ) + findings.append(report) + return findings + + report = CheckReportM365( + metadata=self.metadata(), + resource=auth_policy, + resource_name=auth_policy.name, + resource_id=auth_policy.id, + ) + + if not auth_policy.allowed_to_use_sspr: + report.status = "PASS" + report.status_extended = ( + "Self-Service Password Reset (SSPR) is disabled for administrators " + "(allowedToUseSSPR is false in the authorization policy)." + ) + else: + report.status = "FAIL" + report.status_extended = ( + "Self-Service Password Reset (SSPR) is enabled for administrators " + "(allowedToUseSSPR is true in the authorization policy)." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/m365/services/entra/entra_service.py b/prowler/providers/m365/services/entra/entra_service.py index e8e43562be..6ab6dbb0bf 100644 --- a/prowler/providers/m365/services/entra/entra_service.py +++ b/prowler/providers/m365/services/entra/entra_service.py @@ -231,6 +231,9 @@ class Entra(M365Service): ), guest_invite_settings=auth_policy.allow_invites_from, guest_user_role_id=auth_policy.guest_user_role_id, + allowed_to_use_sspr=getattr( + auth_policy, "allowed_to_use_s_s_p_r", None + ), block_msol_powershell=getattr( auth_policy, "block_msol_power_shell", None ), @@ -2574,6 +2577,7 @@ class AuthorizationPolicy(BaseModel): default_user_role_permissions: Optional[DefaultUserRolePermissions] guest_invite_settings: Optional[str] guest_user_role_id: Optional[UUID] + allowed_to_use_sspr: Optional[bool] = None block_msol_powershell: Optional[bool] = None diff --git a/tests/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled_test.py b/tests/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled_test.py new file mode 100644 index 0000000000..9b007de452 --- /dev/null +++ b/tests/providers/m365/services/entra/entra_policy_admin_self_service_password_reset_disabled/entra_policy_admin_self_service_password_reset_disabled_test.py @@ -0,0 +1,155 @@ +from unittest import mock + +from prowler.providers.m365.services.entra.entra_service import ( + AuthorizationPolicy, +) +from tests.providers.m365.m365_fixtures import set_mocked_m365_provider + + +class Test_entra_policy_admin_self_service_password_reset_disabled: + def test_no_auth_policy(self): + """When authorization_policy is None, the check returns a MANUAL finding.""" + entra_client = mock.MagicMock() + entra_client.authorization_policy = None + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled import ( + entra_policy_admin_self_service_password_reset_disabled, + ) + + check = entra_policy_admin_self_service_password_reset_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert "Cannot evaluate" in result[0].status_extended + assert "could not be retrieved" in result[0].status_extended + assert result[0].resource_name == "Authorization Policy" + assert result[0].resource_id == "authorizationPolicy" + + def test_auth_policy_sspr_none(self): + """When allowed_to_use_sspr is None, the check returns a MANUAL finding.""" + entra_client = mock.MagicMock() + entra_client.authorization_policy = AuthorizationPolicy( + id="authorizationPolicy", + name="Authorization Policy", + description="", + default_user_role_permissions=None, + guest_invite_settings=None, + guest_user_role_id=None, + allowed_to_use_sspr=None, + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled import ( + entra_policy_admin_self_service_password_reset_disabled, + ) + + check = entra_policy_admin_self_service_password_reset_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert "Cannot evaluate" in result[0].status_extended + assert result[0].resource_name == "Authorization Policy" + assert result[0].resource_id == "authorizationPolicy" + + def test_sspr_enabled_fail(self): + """When allowed_to_use_sspr is True, the check returns FAIL.""" + entra_client = mock.MagicMock() + entra_client.authorization_policy = AuthorizationPolicy( + id="authorizationPolicy", + name="Authorization Policy", + description="", + default_user_role_permissions=None, + guest_invite_settings=None, + guest_user_role_id=None, + allowed_to_use_sspr=True, + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled import ( + entra_policy_admin_self_service_password_reset_disabled, + ) + + check = entra_policy_admin_self_service_password_reset_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Self-Service Password Reset (SSPR) is enabled for administrators " + "(allowedToUseSSPR is true in the authorization policy)." + ) + assert result[0].resource_id == "authorizationPolicy" + assert result[0].resource_name == "Authorization Policy" + assert result[0].location == "global" + + def test_sspr_disabled_pass(self): + """When allowed_to_use_sspr is False, the check returns PASS.""" + entra_client = mock.MagicMock() + entra_client.authorization_policy = AuthorizationPolicy( + id="authorizationPolicy", + name="Authorization Policy", + description="", + default_user_role_permissions=None, + guest_invite_settings=None, + guest_user_role_id=None, + allowed_to_use_sspr=False, + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_m365_provider(), + ), + mock.patch( + "prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled.entra_client", + new=entra_client, + ), + ): + from prowler.providers.m365.services.entra.entra_policy_admin_self_service_password_reset_disabled.entra_policy_admin_self_service_password_reset_disabled import ( + entra_policy_admin_self_service_password_reset_disabled, + ) + + check = entra_policy_admin_self_service_password_reset_disabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Self-Service Password Reset (SSPR) is disabled for administrators " + "(allowedToUseSSPR is false in the authorization policy)." + ) + assert result[0].resource_id == "authorizationPolicy" + assert result[0].resource_name == "Authorization Policy" + assert result[0].location == "global"