Src/Private/Get-AbrEntraIDIdentityProtection.ps1

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

    begin {
        Write-PScriboMessage "Collecting Identity Protection data for tenant $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Identity Protection'
    }

    process {
        Section -Style Heading2 'Identity Protection' {
            Paragraph "The following section documents the Microsoft Entra ID Identity Protection configuration for tenant $TenantId, including risky users, risk detections, and risk-based Conditional Access policy coverage."
            BlankLine

            try {
                #region Risk Policy Summary
                Write-Host " - Retrieving Identity Protection risk policies..."
                $CAPolicies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction SilentlyContinue

                # Detect sign-in risk CA policies
                $SignInRiskPolicies = @($CAPolicies | Where-Object {
                    $_.Conditions.SignInRiskLevels.Count -gt 0 -and $_.State -eq 'enabled'
                })
                # Detect user risk CA policies
                $UserRiskPolicies = @($CAPolicies | Where-Object {
                    $_.Conditions.UserRiskLevels.Count -gt 0 -and $_.State -eq 'enabled'
                })

                # Medium+high risk coverage (CIS 5.2.2.8)
                $SignInRiskBlocksMedHigh = $false
                $SignInRiskBlocksMedHighDetail = 'No CA policy found covering both medium and high sign-in risk levels. [WARN]'
                foreach ($P in $SignInRiskPolicies) {
                    $levels = $P.Conditions.SignInRiskLevels
                    if ($levels -contains 'medium' -and $levels -contains 'high') {
                        $SignInRiskBlocksMedHigh = $true
                        $SignInRiskBlocksMedHighDetail = "Policy '$($P.DisplayName)' covers medium and high sign-in risk. [OK]"
                        break
                    }
                }

                $RiskPolicySumObj = [System.Collections.ArrayList]::new()
                $riskSumInObj = [ordered] @{
                    'Sign-In Risk CA Policies (Enforced)' = $SignInRiskPolicies.Count
                    'User Risk CA Policies (Enforced)'    = $UserRiskPolicies.Count
                    'Sign-In Risk Levels Covered'         = if ($SignInRiskPolicies.Count -gt 0) { ($SignInRiskPolicies.Conditions.SignInRiskLevels | Select-Object -Unique) -join ', ' } else { 'None' }
                    'User Risk Levels Covered'            = if ($UserRiskPolicies.Count -gt 0) { ($UserRiskPolicies.Conditions.UserRiskLevels | Select-Object -Unique) -join ', ' } else { 'None' }
                }
                $RiskPolicySumObj.Add([pscustomobject]$riskSumInObj) | Out-Null
                $null = (& {
                    if ($HealthCheck.EntraID.ConditionalAccess) {
                        $null = ($RiskPolicySumObj | Where-Object { [int]$_.'Sign-In Risk CA Policies (Enforced)' -eq 0 } | Set-Style -Style Warning | Out-Null)
                        $null = ($RiskPolicySumObj | Where-Object { [int]$_.'User Risk CA Policies (Enforced)'  -eq 0 } | Set-Style -Style Warning | Out-Null)
                    }
                })
                $RiskPolTableParams = @{ Name = "Risk-Based CA Policy Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                if ($Report.ShowTableCaptions) { $RiskPolTableParams['Caption'] = "- $($RiskPolTableParams.Name)" }
                $RiskPolicySumObj | Table @RiskPolTableParams
                #endregion

                #region Risky Users
                Write-Host " - Retrieving risky users..."
                try {
                    $RiskyUsers = Invoke-MgGraphRequest -Method GET `
                        -Uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?$top=999&$orderby=riskLastUpdatedDateTime desc' `
                        -ErrorAction Stop

                    $AllRiskyUsers = $RiskyUsers.value
                    while ($RiskyUsers.'@odata.nextLink') {
                        $RiskyUsers = Invoke-MgGraphRequest -Method GET -Uri $RiskyUsers.'@odata.nextLink' -ErrorAction Stop
                        $AllRiskyUsers += $RiskyUsers.value
                    }

                    $HighRisk   = @($AllRiskyUsers | Where-Object { $_.riskLevel -eq 'high' }).Count
                    $MedRisk    = @($AllRiskyUsers | Where-Object { $_.riskLevel -eq 'medium' }).Count
                    $LowRisk    = @($AllRiskyUsers | Where-Object { $_.riskLevel -eq 'low' }).Count
                    $AtRisk     = @($AllRiskyUsers | Where-Object { $_.riskState -in @('atRisk','confirmedCompromised') }).Count
                    $Dismissed  = @($AllRiskyUsers | Where-Object { $_.riskState -eq 'dismissed' }).Count
                    $Remediated = @($AllRiskyUsers | Where-Object { $_.riskState -eq 'remediated' }).Count

                    BlankLine
                    $RiskyUserSumObj = [System.Collections.ArrayList]::new()
                    $riskyUserInObj = [ordered] @{
                        'Total Risky Users'           = @($AllRiskyUsers).Count
                        'High Risk'                   = $HighRisk
                        'Medium Risk'                 = $MedRisk
                        'Low Risk'                    = $LowRisk
                        'Currently At Risk'           = $AtRisk
                        'Confirmed Compromised'       = @($AllRiskyUsers | Where-Object { $_.riskState -eq 'confirmedCompromised' }).Count
                        'Remediated'                  = $Remediated
                        'Dismissed'                   = $Dismissed
                    }
                    $RiskyUserSumObj.Add([pscustomobject]$riskyUserInObj) | Out-Null
                    $null = (& {
                        if ($HealthCheck.EntraID.MFA) {
                            $null = ($RiskyUserSumObj | Where-Object { [int]$_.'High Risk' -gt 0 }             | Set-Style -Style Critical | Out-Null)
                            $null = ($RiskyUserSumObj | Where-Object { [int]$_.'Confirmed Compromised' -gt 0 } | Set-Style -Style Critical | Out-Null)
                            $null = ($RiskyUserSumObj | Where-Object { [int]$_.'Medium Risk' -gt 0 }           | Set-Style -Style Warning  | Out-Null)
                        }
                    })
                    $RiskyUserSumTableParams = @{ Name = "Risky User Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                    if ($Report.ShowTableCaptions) { $RiskyUserSumTableParams['Caption'] = "- $($RiskyUserSumTableParams.Name)" }
                    $RiskyUserSumObj | Table @RiskyUserSumTableParams

                    # Per-user risky detail at InfoLevel 2
                    if ($InfoLevel.IdentityProtection -ge 2 -and $AtRisk -gt 0) {
                        $ActiveRiskyUsers = @($AllRiskyUsers | Where-Object { $_.riskState -in @('atRisk','confirmedCompromised') } | Select-Object -First 50)
                        if ($ActiveRiskyUsers.Count -gt 0) {
                            Section -Style Heading3 'Active Risky Users' {
                                Paragraph "The following $($ActiveRiskyUsers.Count) user(s) are currently at risk or confirmed compromised in tenant $TenantId. Investigate and remediate immediately."
                                BlankLine
                                $RiskyUserDetailObj = [System.Collections.ArrayList]::new()
                                foreach ($RU in ($ActiveRiskyUsers | Sort-Object riskLevel)) {
                                    $ruInObj = [ordered] @{
                                        'Display Name'   = $RU.userDisplayName
                                        'UPN'            = $RU.userPrincipalName
                                        'Risk Level'     = $RU.riskLevel
                                        'Risk State'     = $RU.riskState
                                        'Risk Detail'    = if ($RU.riskDetail) { $RU.riskDetail } else { '--' }
                                        'Last Updated'   = if ($RU.riskLastUpdatedDateTime) { ([datetime]$RU.riskLastUpdatedDateTime).ToString('yyyy-MM-dd HH:mm') } else { '--' }
                                    }
                                    $RiskyUserDetailObj.Add([pscustomobject]$ruInObj) | Out-Null
                                }
                                $null = (& {
                                    if ($HealthCheck.EntraID.MFA) {
                                        $null = ($RiskyUserDetailObj | Where-Object { $_.'Risk Level' -eq 'high' -or $_.'Risk State' -eq 'confirmedCompromised' } | Set-Style -Style Critical | Out-Null)
                                        $null = ($RiskyUserDetailObj | Where-Object { $_.'Risk Level' -eq 'medium' } | Set-Style -Style Warning | Out-Null)
                                    }
                                })
                                $RiskyDetailTableParams = @{ Name = "Active Risky Users - $TenantId"; List = $false; ColumnWidths = 16, 22, 10, 14, 20, 18 }
                                if ($Report.ShowTableCaptions) { $RiskyDetailTableParams['Caption'] = "- $($RiskyDetailTableParams.Name)" }
                                $RiskyUserDetailObj | Table @RiskyDetailTableParams
                                $null = ($script:ExcelSheets['Risky Users'] = $RiskyUserDetailObj)
                            }
                        }
                    }
                    $null = ($script:ExcelSheets['Risk Summary'] = $RiskyUserSumObj)
                } catch {
                    Write-AbrSectionError -Section 'Risky Users' -Message "Identity Protection data requires IdentityRiskyUser.Read.All scope or Entra ID P2 licence. Error: $($_.Exception.Message)"
                }
                #endregion

                #region Risk-Based CA Policy Detail
                if ($SignInRiskPolicies.Count -gt 0 -or $UserRiskPolicies.Count -gt 0) {
                    BlankLine
                    $AllRiskPolicies = @($SignInRiskPolicies) + @($UserRiskPolicies) | Select-Object -Unique Id, DisplayName, State, Conditions, GrantControls
                    $RiskCAObj = [System.Collections.ArrayList]::new()
                    foreach ($P in ($AllRiskPolicies | Sort-Object DisplayName)) {
                        $rcaInObj = [ordered] @{
                            'Policy Name'        = $P.DisplayName
                            'State'              = $P.State
                            'Sign-In Risk Levels'= if ($P.Conditions.SignInRiskLevels) { $P.Conditions.SignInRiskLevels -join ', ' } else { '--' }
                            'User Risk Levels'   = if ($P.Conditions.UserRiskLevels) { $P.Conditions.UserRiskLevels -join ', ' } else { '--' }
                            'Grant Controls'     = if ($P.GrantControls.BuiltInControls) { $P.GrantControls.BuiltInControls -join ', ' } else { '--' }
                        }
                        $RiskCAObj.Add([pscustomobject]$rcaInObj) | Out-Null
                    }
                    $RiskCATableParams = @{ Name = "Risk-Based CA Policies - $TenantId"; List = $false; ColumnWidths = 26, 12, 18, 18, 26 }
                    if ($Report.ShowTableCaptions) { $RiskCATableParams['Caption'] = "- $($RiskCATableParams.Name)" }
                    $RiskCAObj | Table @RiskCATableParams
                    $null = ($script:ExcelSheets['Risk CA Policies'] = $RiskCAObj)
                }
                #endregion

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

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

    end { Show-AbrDebugExecutionTime -End -TitleMessage 'Identity Protection' }
}