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 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]$OutputPath, [string]$ClientId, [string]$Tenant, [string]$CertificatePath, [securestring]$CertificatePassword, [string]$Thumbprint, [switch]$Interactive, [switch]$PassThru ) if ($CertificatePath -and -not $Tenant) { throw 'Bei Zertifikats-Authentifizierung wird -Tenant benoetigt (z. B. 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 gefunden." } 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)) } catch { if ($Interactive -and $_.Exception.Message -match '(?i)unauthorized') { $hint = "Zugriff verweigert fuer '$url'. Fuer -Interactive (delegierter Login) braucht die App-Registrierung " + "die delegierte SharePoint-Berechtigung 'AllSites.FullControl' (Register-SPPermissionApp legt sie ab v0.1.2 mit an; " + "bei bestehenden Apps im Entra-Portal ergaenzen und Admin-Consent erteilen). " + "Alternativ Zertifikats-Authentifizierung verwenden: -CertificatePath/-Thumbprint zusammen mit -Tenant. " + "Urspruenglicher Fehler: $($_.Exception.Message)" if ($PSCmdlet.ParameterSetName -eq 'Tenant') { Write-Warning $hint } else { throw $hint } } elseif ($PSCmdlet.ParameterSetName -eq 'Tenant') { Write-Warning "Site '$url' konnte nicht gescannt werden: $($_.Exception.Message)" } else { throw } } } Write-Progress -Id 1 -Activity 'SPPermissionMatrix-Scan' -Completed if ($sites.Count -eq 0) { throw 'Keine Site konnte gescannt werden.' } $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 gespeichert: $OutputPath" if ($PassThru) { return $scan } return (Get-Item -Path $OutputPath) } |