feat(m365): add entra_policy_admin_self_service_password_reset_disabled check (#12990)

Co-authored-by: kinomakino <3662588+kinomakino@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 15:59:20 +02:00
1 parent 80c4113879
commit dd63fa960b
7 files changed
+265

No files matched your search

@@ -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
@@ -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"
@@ -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."
}
@@ -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
@@ -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
@@ -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"