Public/Invoke-SPPermissionScan.ps1
|
function Invoke-SPPermissionScan { <# .SYNOPSIS Scans the permissions of a SharePoint site (or all sites of a tenant) into a JSON file. .DESCRIPTION Walks site -> document libraries -> folders down to -Depth. Per node only the cheap HasUniqueRoleAssignments flag is queried; role assignments are loaded only where inheritance is broken - the matrix still shows effective permissions on every row. Group members are resolved transitively (SharePoint groups and nested Entra ID groups via Graph, one resolution per group thanks to a scan-wide cache). HTTP 429/503 throttling is retried honoring Retry-After. .PARAMETER SiteUrl One or more site URLs to scan. .PARAMETER AllSites Scans all (non-system) sites of the tenant. Requires -TenantAdminUrl. .PARAMETER TenantAdminUrl URL of the SharePoint admin center, e.g. https://contoso-admin.sharepoint.com .PARAMETER Depth 0 = site only, 1 = + document libraries, 2 = + first folder level, and so on. Default: 1. .PARAMETER IncludeLibrary Only scan libraries whose title matches one of these wildcard patterns. .PARAMETER ExcludeLibrary Skip libraries whose title matches one of these wildcard patterns. .PARAMETER OutputPath Path of the JSON output file. Default: .\SPPermissionScan_<timestamp>.json .PARAMETER ClientId Entra app registration to authenticate with (see Register-SPPermissionApp). Without -ClientId an existing Connect-PnPOnline connection is reused (single site only). .PARAMETER PassThru Returns the scan object instead of the JSON FileInfo. .EXAMPLE Invoke-SPPermissionScan -SiteUrl https://contoso.sharepoint.com/sites/hr -Depth 2 -ClientId $appId -Interactive .EXAMPLE Invoke-SPPermissionScan -AllSites -TenantAdminUrl https://contoso-admin.sharepoint.com -Depth 1 ` -ClientId $appId -Tenant contoso.onmicrosoft.com -CertificatePath .\app.pfx #> [CmdletBinding(DefaultParameterSetName = 'Site')] param( [Parameter(Mandatory, ParameterSetName = 'Site', Position = 0)] [string[]]$SiteUrl, [Parameter(Mandatory, ParameterSetName = 'Tenant')] [switch]$AllSites, [Parameter(Mandatory, ParameterSetName = 'Tenant')] [string]$TenantAdminUrl, [ValidateRange(0, 10)] [int]$Depth = 1, [string[]]$IncludeLibrary = @(), [string[]]$ExcludeLibrary = @(), [string]$OutputPath, [string]$ClientId, [string]$Tenant, [string]$CertificatePath, [securestring]$CertificatePassword, [string]$Thumbprint, [switch]$Interactive, [switch]$PassThru ) if ($CertificatePath -and -not $Tenant) { throw 'Certificate authentication requires -Tenant (e.g. contoso.onmicrosoft.com).' } $auth = @{ ClientId = $ClientId Tenant = $Tenant CertificatePath = $CertificatePath CertificatePassword = $CertificatePassword Thumbprint = $Thumbprint Interactive = $Interactive.IsPresent } $targetUrls = @() if ($PSCmdlet.ParameterSetName -eq 'Tenant') { Connect-SPMSite -Url $TenantAdminUrl -Auth $auth $excludedTemplates = '^(SRCHCEN|SPSMSITEHOST|APPCATALOG|POINTPUBLISHINGHUB|POINTPUBLISHINGTOPIC|EDISC|REDIRECTSITE|TEAMCHANNEL)' $targetUrls = @(Invoke-SPMWithRetry { Get-PnPTenantSite } | Where-Object { $_.Template -notmatch $excludedTemplates } | Select-Object -ExpandProperty Url) Write-Verbose "Tenant scan: $($targetUrls.Count) sites found." } else { $targetUrls = @($SiteUrl) } $groupCache = @{} $sites = [System.Collections.Generic.List[object]]::new() $index = 0 foreach ($url in $targetUrls) { $index++ $percent = [int](($index - 1) / [Math]::Max($targetUrls.Count, 1) * 100) Write-Progress -Id 1 -Activity 'SPPermissionMatrix scan' -Status "Site $index/$($targetUrls.Count): $url" -PercentComplete $percent try { Connect-SPMSite -Url $url -Auth $auth $sites.Add((Get-SPMSiteScan -SiteUrl $url -Depth $Depth -GroupCache $groupCache -IncludeLibrary $IncludeLibrary -ExcludeLibrary $ExcludeLibrary)) } catch { if ($Interactive -and $_.Exception.Message -match '(?i)unauthorized') { $hint = "Access denied for '$url'. -Interactive (delegated login) requires the app registration to hold " + "the delegated SharePoint permission 'AllSites.FullControl' (Register-SPPermissionApp adds it since v0.1.2; " + "for existing apps add it in the Entra portal and grant admin consent). " + "Alternatively use certificate authentication: -CertificatePath/-Thumbprint together with -Tenant. " + "Original error: $($_.Exception.Message)" if ($PSCmdlet.ParameterSetName -eq 'Tenant') { Write-Warning $hint } else { throw $hint } } elseif ($PSCmdlet.ParameterSetName -eq 'Tenant') { Write-Warning "Site '$url' could not be scanned: $($_.Exception.Message)" } else { throw } } } Write-Progress -Id 1 -Activity 'SPPermissionMatrix scan' -Completed if ($sites.Count -eq 0) { throw 'No site could be scanned.' } $moduleVersion = (Get-Module -Name SPPermissionMatrix | Select-Object -First 1).Version $scan = [pscustomobject]@{ schemaVersion = '1.0' generator = "SPPermissionMatrix $moduleVersion" scanDate = (Get-Date).ToUniversalTime().ToString('o') depth = $Depth siteCount = $sites.Count sites = $sites.ToArray() } if (-not $OutputPath) { $OutputPath = Join-Path (Get-Location) ('SPPermissionScan_{0:yyyyMMdd_HHmmss}.json' -f (Get-Date)) } $scan | ConvertTo-Json -Depth 64 | Set-Content -Path $OutputPath -Encoding utf8 Write-Verbose "Scan saved: $OutputPath" if ($PassThru) { return $scan } return (Get-Item -Path $OutputPath) } |