Src/Private/Get-AbrEntraIDSSPR.ps1

function Get-AbrEntraIDSSPR {
    [CmdletBinding()]
    param ([Parameter(Position=0,Mandatory)][string]$TenantId)

    begin {
        Write-PScriboMessage "Collecting SSPR configuration for tenant $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'SSPR'
    }

    process {
        Section -Style Heading2 'Self-Service Password Reset (SSPR)' {
            Paragraph "The following section documents the Self-Service Password Reset configuration and registration status for tenant $TenantId. SSPR reduces helpdesk burden and is required for user risk remediation workflows in Identity Protection."
            BlankLine

            try {
                Write-Host " - Retrieving SSPR configuration..."

                # SSPR policy is part of Authentication Methods Policy
                $SSPRPolicy = Invoke-MgGraphRequest -Method GET `
                    -Uri 'https://graph.microsoft.com/v1.0/policies/authenticationMethodsPolicy' `
                    -ErrorAction SilentlyContinue

                # Get SSPR registration from userRegistrationDetails (replaces deprecated credentialUserRegistrationDetails)
                # Requires Reports.Read.All -- falls back gracefully if not available
                $AllSSPRReg = @()
                try {
                    $SSPRRegDetails = Invoke-MgGraphRequest -Method GET `
                        -Uri "https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails?`$top=999" `
                        -ErrorAction Stop
                    $AllSSPRReg = if ($SSPRRegDetails -and $SSPRRegDetails.value) { $SSPRRegDetails.value } else { @() }
                    while ($SSPRRegDetails.'@odata.nextLink') {
                        $SSPRRegDetails = Invoke-MgGraphRequest -Method GET -Uri $SSPRRegDetails.'@odata.nextLink' -ErrorAction Stop
                        $AllSSPRReg += $SSPRRegDetails.value
                    }
                } catch {
                    # Reports.Read.All not granted or endpoint unavailable -- SSPR registration counts will show as 0
                    Write-PScriboMessage -IsWarning "SSPR registration data unavailable (requires Reports.Read.All): $($_.Exception.Message)"
                    $AllSSPRReg = @()
                }

                $TotalUsers      = @($AllSSPRReg).Count
                # New endpoint uses isSsprRegistered/isSsprCapable/isSsprEnabled
                # Fallback: check both old and new field names
                $SSPREnabled     = @($AllSSPRReg | Where-Object { $_.isSsprEnabled   -or $_.isSsprRegistered }).Count
                $SSPRRegistered  = @($AllSSPRReg | Where-Object { $_.isSsprRegistered }).Count
                $SSPRCapable     = @($AllSSPRReg | Where-Object { $_.isSsprCapable   -or $_.isSsprRegistered }).Count
                $SSPRPct         = if ($TotalUsers -gt 0) { [math]::Round(($SSPRRegistered / $TotalUsers) * 100, 0) } else { 0 }

                $SSPRSumObj = [System.Collections.ArrayList]::new()
                $ssprInObj = [ordered] @{
                    'Total Users Assessed'          = $TotalUsers
                    'SSPR Enabled (Licensed)'       = $SSPREnabled
                    'SSPR Registered'               = $SSPRRegistered
                    'SSPR Capable (Enabled+Reg)'    = $SSPRCapable
                    'SSPR Registration %'           = "$SSPRPct%"
                    'Not SSPR Registered'           = ($TotalUsers - $SSPRRegistered)
                }
                $SSPRSumObj.Add([pscustomobject]$ssprInObj) | Out-Null
                $null = (& {
                    if ($HealthCheck.EntraID.MFA) {
                        if ($SSPRPct -lt 50) { $null = ($SSPRSumObj | Set-Style -Style Critical | Out-Null) }
                        elseif ($SSPRPct -lt 80) { $null = ($SSPRSumObj | Set-Style -Style Warning  | Out-Null) }
                    }
                })
                $SSPRSumTableParams = @{ Name = "SSPR Registration Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                if ($Report.ShowTableCaptions) { $SSPRSumTableParams['Caption'] = "- $($SSPRSumTableParams.Name)" }
                $SSPRSumObj | Table @SSPRSumTableParams
                $null = ($script:ExcelSheets['SSPR Summary'] = $SSPRSumObj)

                # SSPR method configuration from Authentication Methods Policy
                if ($SSPRPolicy -and $SSPRPolicy.authenticationMethodConfigurations) {
                    BlankLine
                    $SSPRMethodObj = [System.Collections.ArrayList]::new()
                    $SSPRMethods = @('email','mobilePhone','officePhone','securityQuestion','microsoftAuthenticatorPush','softwareOath')
                    foreach ($Config in ($SSPRPolicy.authenticationMethodConfigurations | Where-Object { $_.id -in $SSPRMethods })) {
                        $smInObj = [ordered] @{
                            'Method'  = $Config.id
                            'State'   = $Config.state
                        }
                        $SSPRMethodObj.Add([pscustomobject]$smInObj) | Out-Null
                    }
                    if ($SSPRMethodObj.Count -gt 0) {
                        $SSPRMethTableParams = @{ Name = "SSPR Available Methods - $TenantId"; List = $false; ColumnWidths = 60, 40 }
                        if ($Report.ShowTableCaptions) { $SSPRMethTableParams['Caption'] = "- $($SSPRMethTableParams.Name)" }
                        $SSPRMethodObj | Table @SSPRMethTableParams
                    }
                }

                #region ACSC E8 + CIS SSPR Assessment
                BlankLine
                if ($script:IncludeACSCe8) {
                    Paragraph "ACSC Essential Eight Maturity Level Assessment -- SSPR:"
                    BlankLine
                    #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json)
                    $_ComplianceVars = @{
                        'SSPREnabled' = $SSPREnabled
                        'SSPRPct' = $SSPRPct
                        'SSPRRegistered' = $SSPRRegistered
                        'TotalUsers' = $TotalUsers
                    }
                    $E8SSPRChecks = Build-AbrComplianceChecks `
                        -Definitions (Get-AbrE8Checks -Section 'SSPR') `
                        -Framework E8 `
                        -CallerVariables $_ComplianceVars
                    New-AbrE8AssessmentTable -Checks $E8SSPRChecks -Name 'SSPR' -TenantId $TenantId
                    # Consolidated into ACSC E8 Assessment sheet
                    if ($E8SSPRChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8SSPRChecks | Select-Object @{N='Section';E={'SSPR'}}, ML, Control, Status, Detail ))) }
                    #endregion
                }
                if ($script:IncludeCISBaseline) {
                    BlankLine
                    Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- SSPR:"
                    BlankLine
                    #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json)
                    $CISSSPRChecks = Build-AbrComplianceChecks `
                        -Definitions (Get-AbrCISChecks -Section 'SSPR') `
                        -Framework CIS `
                        -CallerVariables $_ComplianceVars
                    New-AbrCISAssessmentTable -Checks $CISSSPRChecks -Name 'SSPR' -TenantId $TenantId
                    # Consolidated into CIS Assessment sheet
                    if ($CISSSPRChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISSSPRChecks | Select-Object @{N='Section';E={'SSPR'}}, CISControl, Level, Status, Detail ))) }
                    #endregion
                }
                #endregion

            } catch {
                Write-AbrSectionError -Section 'SSPR' -Message "$($_.Exception.Message)"
            }
        } # end Section SSPR
    }

    end { Show-AbrDebugExecutionTime -End -TitleMessage 'SSPR' }
}