Public/Scan/Get-SPCOverPermissionedUser.ps1
|
function Get-SPCOverPermissionedUser { <# .SYNOPSIS Identifies over-permissioned users based on an Excessive Access Score (EAS) (Focus on Breadth of Access). .DESCRIPTION Scans site collections to determine user access levels. Calculates the Excessive Access Score (EAS) using the formula: EAS = (Full Control * 3) + (Edit * 2) + (Read * 1) Flags users with an EAS greater than 100 with a Red Alert. Concept: Over-Permissioned Users focus on "Breadth of Access" or "Blast Radius" (Chiều rộng quyền hạn). Risk: These users have access to a vast amount of resources across the tenant, increasing the risk of widespread data leakage or ransomware infection (data infection) if compromised. Note: This cmdlet may miss Site Collection Administrators (SCA) because it relies on standard Role Assignments, which do not always enumerate SCAs explicitly. .EXAMPLE Get-SPCOverPermissionedUser #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$SiteUrl ) begin { Write-Verbose "Validating connection..." if (Get-Command Test-SPCConnection -ErrorAction SilentlyContinue) { Test-SPCConnection } $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 } } } } } $tempResults = [System.Collections.Generic.List[PSCustomObject]]::new() } 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 Over-Permissioned 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 $roleAssignments = (Invoke-PnPSPRestMethod -Method Get -Url "/_api/web/roleassignments?`$expand=Member,RoleDefinitionBindings" -Connection $siteConnection -ErrorAction SilentlyContinue).value foreach ($ra in $roleAssignments) { if ($ra.Member.PrincipalType -in 'User', 1 -and $ra.Member.LoginName -match "\|membership\|") { $upn = $ra.Member.LoginName.Split("|")[-1] $roles = $ra.RoleDefinitionBindings.Name $accessLevel = "None" if ($roles -contains "Full Control") { $accessLevel = "Full Control" } elseif ($roles -contains "Edit" -or $roles -contains "Contribute") { $accessLevel = "Edit" } elseif ($roles -contains "Read" -or $roles -contains "View Only") { $accessLevel = "Read" } if ($accessLevel -ne "None") { $tempResults.Add([PSCustomObject]@{ UPN = $upn AccessLevel = $accessLevel }) } } } $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-GOPU-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-GOPU-002] $(Get-Date -Format 'o'): Error processing site collection '$url'. Resource: $url. Details: $($ex.Message)" -ErrorAction Continue $success = $true } } } } catch { Write-Error "[ERR-GOPU-003] $(Get-Date -Format 'o'): Error processing site collection '$url'. Resource: $url. Details: $($_.Exception.Message)" -ErrorAction Continue } } Write-Verbose "Calculating EAS..." $foundCount = 0 if ($tempResults.Count -gt 0) { $grouped = $tempResults | Group-Object -Property UPN $finalResults = foreach ($g in $grouped) { $fullControlCount = ($g.Group | Where-Object { $_.AccessLevel -eq 'Full Control' }).Count $editCount = ($g.Group | Where-Object { $_.AccessLevel -eq 'Edit' }).Count $readCount = ($g.Group | Where-Object { $_.AccessLevel -eq 'Read' }).Count $eas = ($fullControlCount * 3) + ($editCount * 2) + ($readCount * 1) $isRedAlert = $eas -gt 100 $obj = [PSCustomObject]@{ UPN = $g.Name FullControlCount = $fullControlCount EditCount = $editCount ReadCount = $readCount EAS = $eas IsRedAlert = $isRedAlert } $obj.PSObject.TypeNames.Insert(0, 'SPC.OverPermissionedUser') $obj } $sorted = $finalResults | Sort-Object -Property EAS -Descending $foundCount = $sorted.Count Write-Output $sorted } else { Write-Verbose "No 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-SPCOverPermissionedUser scan. Scanned $counter sites, found $foundCount users." -InformationAction Continue } } |