Public/Get-EntraPrivilegedRoleReview.ps1

function Get-EntraPrivilegedRoleReview {
    <#
    .SYNOPSIS
        Reviews Entra ID privileged role assignments and PIM configuration.
    .DESCRIPTION
        Enumerates all directory role assignments, flags permanent (active) assignments to
        high-privilege roles, identifies users with multiple privileged roles, and checks
        for Global Administrator sprawl. Evaluates whether PIM (Privileged Identity Management)
        eligible assignments are in use.
    .PARAMETER MaxGlobalAdmins
        Maximum acceptable number of Global Administrators before flagging. Defaults to 4.
    .EXAMPLE
        Get-EntraPrivilegedRoleReview -MaxGlobalAdmins 2
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(1, 20)]
        [int]$MaxGlobalAdmins = 4
    )

    begin {
        Test-GraphConnection
        $results = [System.Collections.Generic.List[PSObject]]::new()

        $highPrivRoles = @(
            'Global Administrator',
            'Privileged Role Administrator',
            'Privileged Authentication Administrator',
            'Exchange Administrator',
            'SharePoint Administrator',
            'User Administrator',
            'Application Administrator',
            'Cloud Application Administrator',
            'Authentication Administrator',
            'Intune Administrator'
        )
    }

    process {
        # Get all directory roles that have been activated
        $directoryRoles = Get-MgDirectoryRole -All

        $assignmentMap = @{}  # Track per-user role accumulation

        foreach ($role in $directoryRoles) {
            $members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All
            $isHighPriv = $role.DisplayName -in $highPrivRoles

            foreach ($member in $members) {
                $memberId = $member.Id
                $memberName = $member.AdditionalProperties.displayName
                $memberUpn  = $member.AdditionalProperties.userPrincipalName
                $memberType = $member.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', ''

                # Track role accumulation
                if (-not $assignmentMap.ContainsKey($memberId)) {
                    $assignmentMap[$memberId] = @{
                        Name  = $memberName
                        UPN   = $memberUpn
                        Type  = $memberType
                        Roles = @()
                    }
                }
                $assignmentMap[$memberId].Roles += $role.DisplayName

                $findings = @()
                if ($isHighPriv) { $findings += 'HIGH-PRIVILEGE ROLE' }
                if ($memberType -eq 'servicePrincipal') { $findings += 'SERVICE PRINCIPAL IN ROLE' }

                $results.Add([PSCustomObject]@{
                    RoleName       = $role.DisplayName
                    MemberName     = $memberName
                    MemberUPN      = $memberUpn
                    MemberType     = $memberType
                    IsHighPriv     = $isHighPriv
                    AssignmentType = 'Permanent (Active)'
                    Finding        = if ($findings.Count -gt 0) { $findings -join ' | ' } else { 'OK' }
                })
            }
        }

        # Check for PIM eligible assignments if available
        try {
            $eligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ErrorAction Stop
            foreach ($assignment in $eligibleAssignments) {
                $roleDef = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $assignment.RoleDefinitionId -ErrorAction SilentlyContinue
                $principal = Get-MgDirectoryObject -DirectoryObjectId $assignment.PrincipalId -ErrorAction SilentlyContinue

                $results.Add([PSCustomObject]@{
                    RoleName       = $roleDef.DisplayName
                    MemberName     = $principal.AdditionalProperties.displayName
                    MemberUPN      = $principal.AdditionalProperties.userPrincipalName
                    MemberType     = ($principal.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', '')
                    IsHighPriv     = $roleDef.DisplayName -in $highPrivRoles
                    AssignmentType = 'PIM Eligible'
                    Finding        = 'PIM Eligible (Good)'
                })
            }
        }
        catch {
            Write-Verbose "PIM not available or insufficient permissions to read eligible assignments."
        }

        # Flag Global Admin sprawl
        $globalAdmins = $results | Where-Object { $_.RoleName -eq 'Global Administrator' -and $_.AssignmentType -eq 'Permanent (Active)' }
        if ($globalAdmins.Count -gt $MaxGlobalAdmins) {
            Write-Warning "FINDING: $($globalAdmins.Count) permanent Global Administrators (recommended max: $MaxGlobalAdmins)"
        }

        # Flag users with multiple privileged roles
        foreach ($entry in $assignmentMap.GetEnumerator()) {
            $privRoles = $entry.Value.Roles | Where-Object { $_ -in $highPrivRoles }
            if ($privRoles.Count -gt 2) {
                Write-Warning "FINDING: $($entry.Value.Name) holds $($privRoles.Count) privileged roles: $($privRoles -join ', ')"
            }
        }
    }

    end {
        $flagged = $results | Where-Object { $_.Finding -ne 'OK' -and $_.Finding -ne 'PIM Eligible (Good)' }
        Write-Host " Role assignments reviewed: $($results.Count) | Findings: $($flagged.Count)" -ForegroundColor Gray
        $results | Sort-Object RoleName, MemberName
    }
}