Src/Private/Get-AbrSPSitePermissions.ps1

function Get-AbrSPSitePermissions {
    <#
    .SYNOPSIS
    Collects detailed site-level and library-level permissions for all site collections
    and exports them to Excel. Includes users, groups, permission levels, and
    unique permissions on document libraries.

    .DESCRIPTION
        For each site collection:
          - Site-level role assignments (users and groups with their permission levels)
          - SharePoint groups and their membership
          - Document libraries with unique (broken) permissions
          - Library-level role assignments where unique permissions exist

        This data is Excel-only (too large for a Word report).
        Adds these sheets to $script:ExcelSheets:
          - 'Site Role Assignments' -- who has what on each site
          - 'SP Groups & Members' -- SharePoint group membership
          - 'Library Permissions' -- libraries with unique perms

    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing

    PERFORMANCE NOTE:
        Retrieving permissions for large tenants (100+ sites) can be slow.
        This function is called only when InfoLevel.SiteCollections >= 2.
        A cap of 30 sites is applied by default to avoid very long run times.
        Adjust $MaxSites to increase coverage.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$TenantId,

        [string]$TenantRootUrl,

        # Sites to process (filtered list, already retrieved by SiteCollections section)
        [object[]]$Sites,

        [int]$MaxSites = 30
    )

    if (-not $script:PnPAvailable) { return }
    if (-not $Sites -or $Sites.Count -eq 0) { return }

    $SitesToProcess = $Sites | Select-Object -First $MaxSites
    $TotalSites     = $Sites.Count
    $Capped         = ($TotalSites -gt $MaxSites)

    Write-Host " - Collecting site permissions ($($SitesToProcess.Count) of $TotalSites sites)$(if ($Capped) { " [capped at $MaxSites]" })..." -ForegroundColor Cyan

    $RoleAssignments  = [System.Collections.ArrayList]::new()   # site-level perms
    $GroupMembers     = [System.Collections.ArrayList]::new()   # SP group membership
    $LibraryPerms     = [System.Collections.ArrayList]::new()   # library unique perms

    $SiteIndex = 0
    foreach ($Site in $SitesToProcess) {
        $SiteIndex++
        $SiteUrl   = $Site.Url
        $SiteTitle = if ($Site.Title) { $Site.Title } else { $SiteUrl }
        Write-Host " [$SiteIndex/$($SitesToProcess.Count)] $SiteTitle" -ForegroundColor DarkGray

        try {
            # Connect to this specific site
            $SiteConn = Connect-PnPOnline -Url $SiteUrl -ClientId $script:Options.PnP.ClientId `
                            -Interactive -ReturnConnection -ErrorAction Stop

            #region Site-level role assignments
            try {
                $Web         = Get-PnPWeb -Connection $SiteConn -ErrorAction Stop
                $Assignments = Get-PnPRoleAssignment -Connection $SiteConn -ErrorAction Stop

                foreach ($Assignment in $Assignments) {
                    $Principal   = $Assignment.Member
                    $PrincipalName  = $Principal.Title
                    $PrincipalLogin = $Principal.LoginName
                    $PrincipalType  = $Principal.PrincipalType   # User, SharePointGroup, SecurityGroup

                    $PermLevels  = ($Assignment.RoleDefinitionBindings |
                        ForEach-Object { $_.Name }) -join ', '
                    $CleanLogin1 = $PrincipalLogin -replace 'i:0#\.f\|membership\|', ''

                    $null = $RoleAssignments.Add([pscustomobject][ordered]@{
                        'Site'             = $SiteTitle
                        'Site URL'         = $SiteUrl
                        'Principal Name'   = $PrincipalName
                        'Login / Email'    = $CleanLogin1
                        'Type'             = "$PrincipalType"
                        'Permission Level' = $PermLevels
                        'Inherited'        = ($Web.HasUniqueRoleAssignments -eq $false)
                    })
                }
            } catch {
                Write-AbrDebugLog "Site role assignments failed for $SiteUrl : $($_.Exception.Message)" 'WARN' 'PERMS'
            }
            #endregion

            #region SharePoint group membership
            try {
                $SPGroups = Get-PnPGroup -Connection $SiteConn -ErrorAction Stop
                foreach ($Group in $SPGroups) {
                    try {
                        $Members = Get-PnPGroupMember -Identity $Group.Title -Connection $SiteConn -ErrorAction Stop
                        if ($Members) {
                            foreach ($Member in $Members) {
                                $CleanMemberLogin = if ($Member.Email) { $Member.Email } else { $Member.LoginName -replace 'i:0#\.f\|membership\|', '' }
                        $null = $GroupMembers.Add([pscustomobject][ordered]@{
                                    'Site'         = $SiteTitle
                                    'Site URL'     = $SiteUrl
                                    'Group Name'   = $Group.Title
                                    'Member Name'  = $Member.Title
                                    'Member Email' = $CleanMemberLogin
                                    'Member Type'  = "$($Member.PrincipalType)"
                                })
                            }
                        } else {
                            # Empty group -- still record it
                            $null = $GroupMembers.Add([pscustomobject][ordered]@{
                                'Site'         = $SiteTitle
                                'Site URL'     = $SiteUrl
                                'Group Name'   = $Group.Title
                                'Member Name'  = '(empty group)'
                                'Member Email' = '--'
                                'Member Type'  = '--'
                            })
                        }
                    } catch {
                        Write-AbrDebugLog "Group member fetch failed: $($Group.Title): $($_.Exception.Message)" 'WARN' 'PERMS'
                    }
                }
            } catch {
                Write-AbrDebugLog "SP group fetch failed for $SiteUrl : $($_.Exception.Message)" 'WARN' 'PERMS'
            }
            #endregion

            #region Document library permissions (unique only)
            try {
                $Lists = Get-PnPList -Connection $SiteConn -ErrorAction Stop |
                    Where-Object {
                        $_.BaseType -eq 'DocumentLibrary' -and
                        $_.Hidden -eq $false -and
                        $_.Title -notin @('Style Library','Form Templates','Site Assets','Site Pages','_catalogs')
                    }

                foreach ($List in $Lists) {
                    try {
                        $ListDetail = Get-PnPList -Identity $List.Id -Connection $SiteConn `
                            -Includes 'HasUniqueRoleAssignments','RoleAssignments.Member','RoleAssignments.RoleDefinitionBindings' `
                            -ErrorAction Stop

                        if ($ListDetail.HasUniqueRoleAssignments) {
                            foreach ($LA in $ListDetail.RoleAssignments) {
                                $PrincipalName  = $LA.Member.Title
                                $PrincipalLogin = $LA.Member.LoginName -replace 'i:0#\.f\|membership\|', ''
                                $PrincipalType  = "$($LA.Member.PrincipalType)"
                                $PermLevels     = ($LA.RoleDefinitionBindings | ForEach-Object { $_.Name }) -join ', '

                                $null = $LibraryPerms.Add([pscustomobject][ordered]@{
                                    'Site'             = $SiteTitle
                                    'Site URL'         = $SiteUrl
                                    'Library'          = $List.Title
                                    'Principal Name'   = $PrincipalName
                                    'Login / Email'    = $PrincipalLogin
                                    'Type'             = $PrincipalType
                                    'Permission Level' = $PermLevels
                                    'Unique Perms'     = 'Yes'
                                })
                            }
                        }
                    } catch {
                        Write-AbrDebugLog "Library perm fetch failed: $($List.Title): $($_.Exception.Message)" 'WARN' 'PERMS'
                    }
                }
            } catch {
                Write-AbrDebugLog "List fetch failed for $SiteUrl : $($_.Exception.Message)" 'WARN' 'PERMS'
            }
            #endregion

            # Disconnect from this site (reconnect to admin for next iteration)
            Disconnect-PnPOnline -Connection $SiteConn -ErrorAction SilentlyContinue

        } catch {
            Write-AbrDebugLog "Could not connect to $SiteUrl for permissions: $($_.Exception.Message)" 'WARN' 'PERMS'
            # Reconnect to admin URL for the next site
            try {
                Connect-PnPOnline -Url $script:TenantAdminUrl -ClientId $script:Options.PnP.ClientId `
                    -Interactive -ErrorAction SilentlyContinue | Out-Null
            } catch { }
        }
    }

    # Reconnect to admin URL after iterating all sites
    try {
        Connect-PnPOnline -Url $script:TenantAdminUrl -ClientId $script:Options.PnP.ClientId `
            -Interactive -ErrorAction SilentlyContinue | Out-Null
    } catch { }

    # Add to Excel sheets
    if ($RoleAssignments.Count -gt 0) {
        $script:ExcelSheets['Site Role Assignments'] = $RoleAssignments
        Write-Host " - Site Role Assignments: $($RoleAssignments.Count) rows" -ForegroundColor DarkGray
    }
    if ($GroupMembers.Count -gt 0) {
        $script:ExcelSheets['SP Groups & Members'] = $GroupMembers
        Write-Host " - SP Groups & Members: $($GroupMembers.Count) rows" -ForegroundColor DarkGray
    }
    if ($LibraryPerms.Count -gt 0) {
        $script:ExcelSheets['Library Permissions'] = $LibraryPerms
        Write-Host " - Library Permissions: $($LibraryPerms.Count) rows" -ForegroundColor DarkGray
    }

    if ($Capped) {
        Write-Host " - NOTE: Permissions collected for first $MaxSites of $TotalSites sites." -ForegroundColor Yellow
        Write-Host " To increase coverage, raise InfoLevel.SiteCollections or reduce site count." -ForegroundColor Yellow
    }
}