Public/Scan/Get-SPCPrivilegedUser.ps1
|
function Get-SPCPrivilegedUser { <# .SYNOPSIS Gets the top 20 privileged users across the SharePoint tenant. .DESCRIPTION This cmdlet scans all site collections to identify users who are Site Collection Administrators, members of the default Owners group, or have direct 'Full Control' assignments. It aggregates this data by UserPrincipalName and returns the top 20 users with the most privileged access. .EXAMPLE Get-SPCPrivilegedUser #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$SiteUrl ) begin { Write-Verbose "Validating connection..." if (Get-Command Test-SPCConnection -ErrorAction SilentlyContinue) { Test-SPCConnection } $tempResults = [System.Collections.Generic.List[PSCustomObject]]::new() $connectToSite = { param([string] $SiteUrl, [PSCustomObject] $Ctx) $tenantId = if ($Ctx.TenantName -match '\.') { $Ctx.TenantName } else { "$($Ctx.TenantName).onmicrosoft.com" } switch ($Ctx.AuthMethod) { 'Interactive' { $token = Get-PnPAccessToken -ResourceTypeName SharePoint -Connection $Ctx.PnPContext Connect-PnPOnline -Url $SiteUrl -AccessToken $token -ReturnConnection } 'AppOnly' { if ($Ctx._CertificatePath) { Connect-PnPOnline -Url $SiteUrl -ClientId $Ctx._ClientId ` -Tenant $tenantId ` -CertificatePath $Ctx._CertificatePath ` -CertificatePassword $Ctx._CertificatePassword -ReturnConnection } elseif ($Ctx._CertificateThumbprint) { Connect-PnPOnline -Url $SiteUrl -ClientId $Ctx._ClientId ` -Tenant $tenantId ` -Thumbprint $Ctx._CertificateThumbprint -ReturnConnection } else { $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Ctx._ClientSecret) try { $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) Connect-PnPOnline -Url $SiteUrl -ClientId $Ctx._ClientId ` -ClientSecret $plain -ReturnConnection } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) $plain = $null } } } } } } process { try { $sitesToScan = @() if ($SiteUrl) { $sitesToScan += $SiteUrl } else { Write-Verbose "Fetching all tenant sites..." $sitesToScan = Get-PnPTenantSite -Connection $script:SPCContext.PnPContext | Select-Object -ExpandProperty Url } $totalSites = $sitesToScan.Count $counter = 0 foreach ($url in $sitesToScan) { $counter++ Write-Progress -Activity "Scanning Sites for Privileged Users" -Status "Processing site $url ($counter / $totalSites)" -PercentComplete (($counter / $totalSites) * 100) Write-Verbose "Scanning site: $url" try { $retryCount = 0 $maxRetries = 5 $success = $false while (-not $success -and $retryCount -lt $maxRetries) { try { $siteConnection = & $connectToSite -SiteUrl $url -Ctx $script:SPCContext # 1. SCAs - Use Get-PnPSiteCollectionAdmin to avoid large list thresholds $scas = Get-PnPSiteCollectionAdmin -Connection $siteConnection -ErrorAction SilentlyContinue foreach ($sca in $scas) { if (-not [string]::IsNullOrEmpty($sca.LoginName) -and $sca.LoginName -match "\|membership\|") { $upn = $sca.LoginName.Split("|")[-1] $tempResults.Add([PSCustomObject]@{ UPN = $upn SiteUrl = $url PermissionSource = "SCA" }) } } # 2. Owners Group $ownersGroup = Get-PnPGroup -AssociatedOwnerGroup -Connection $siteConnection -ErrorAction SilentlyContinue if ($ownersGroup) { $ownerMembers = Get-PnPGroupMember -Group $ownersGroup.Title -Connection $siteConnection -ErrorAction SilentlyContinue foreach ($member in $ownerMembers) { if (-not [string]::IsNullOrEmpty($member.LoginName) -and $member.LoginName -match "\|membership\|") { $upn = $member.LoginName.Split("|")[-1] $tempResults.Add([PSCustomObject]@{ UPN = $upn SiteUrl = $url PermissionSource = "Owner" }) } } } # 3. Direct Assignments (Full Control) # Use REST API to bypass missing Get-PnPRoleAssignment in PnP 3.x $roleAssignments = (Invoke-PnPSPRestMethod -Method Get -Url "/_api/web/roleassignments?`$expand=Member,RoleDefinitionBindings" -Connection $siteConnection -ErrorAction SilentlyContinue).value foreach ($ra in $roleAssignments) { $isFullControl = $ra.RoleDefinitionBindings | Where-Object { $_.Name -eq 'Full Control' } if ($isFullControl) { if ($ra.Member.PrincipalType -in 'User', 1 -and $ra.Member.LoginName -match "\|membership\|") { $upn = $ra.Member.LoginName.Split("|")[-1] $tempResults.Add([PSCustomObject]@{ UPN = $upn SiteUrl = $url PermissionSource = "Direct" }) } } } $success = $true } catch { $ex = $_.Exception if ($ex.Message -match '429|503' -or $_.FullyQualifiedErrorId -match '429|503') { $retryCount++ $retryAfter = $null if ($ex.Response -and $ex.Response.Headers -and $ex.Response.Headers["Retry-After"]) { $retryAfter = $ex.Response.Headers["Retry-After"] } if ($retryAfter) { $waitTime = [int]$retryAfter } else { $waitTime = [Math]::Pow(2, $retryCount) } Write-Verbose "Throttled (429/503). Retrying in $waitTime seconds (Attempt $retryCount of $maxRetries)..." Start-Sleep -Seconds $waitTime if ($retryCount -eq $maxRetries) { Write-Error "[ERR-GPU-001] $(Get-Date -Format 'o'): Failed to process site collection '$url' after $maxRetries attempts due to throttling. Resource: $url." -ErrorAction Continue } } else { Write-Error "[ERR-GPU-002] $(Get-Date -Format 'o'): Error processing site collection '$url'. Resource: $url. Details: $($ex.Message)" -ErrorAction Continue $success = $true # break out of retry loop for non-throttling errors } } } } catch { Write-Error "[ERR-GPU-003] $(Get-Date -Format 'o'): Error processing site collection '$url'. Resource: $url. Details: $($_.Exception.Message)" -ErrorAction Continue } } Write-Verbose "Aggregating results..." $foundCount = 0 if ($tempResults.Count -gt 0) { $grouped = $tempResults | Group-Object -Property UPN $finalResults = foreach ($g in $grouped) { $obj = [PSCustomObject]@{ UPN = $g.Name SiteCount = $g.Count Sites = $g.Group.SiteUrl | Select-Object -Unique PermissionSources = $g.Group.PermissionSource | Select-Object -Unique } $obj.PSObject.TypeNames.Insert(0, 'SPC.PrivilegedUser') $obj } $top20 = @($finalResults | Sort-Object -Property SiteCount -Descending) if ($top20.Count -gt 20) { $top20 = $top20[0..19] } $foundCount = $top20.Count Write-Output $top20 } else { Write-Verbose "No privileged users found." Write-Output @() } } catch { $errCode = "ERR-AUTH-001" $exMsg = "[$errCode] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ssZ'): Terminating error occurred. $($_.Exception.Message)" Write-Error -Message $exMsg -Exception $_.Exception -ErrorId $errCode -ErrorAction Stop } } end { if ($null -eq $counter) { $counter = 0 } if ($null -eq $foundCount) { $foundCount = 0 } Write-Information -MessageData "Completed Get-SPCPrivilegedUser scan. Scanned $counter sites, found $foundCount privileged users." -InformationAction Continue } } |