Private/Get-SPMSiteScan.ps1
|
function Add-SPMFolderNode { <# .SYNOPSIS Recursively adds folder nodes below a parent node up to the configured depth. .DESCRIPTION Each level is fetched fresh by URL (Get-PnPFolder -Includes Folders). Chaining Get-PnPProperty from a previously materialized folder can hit CSOM's instantiated-but-unpopulated collection state and silently return zero subfolders. Per folder only the cheap HasUniqueRoleAssignments flag is queried; role assignments are loaded only where it is true, otherwise the effective permissions of the parent are carried over. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$FolderUrl, [Parameter(Mandatory)] $ParentNode, [Parameter(Mandatory)] [int]$Level, [Parameter(Mandatory)] [int]$MaxLevel, [Parameter(Mandatory)] [System.Collections.Generic.List[object]]$Nodes, [Parameter(Mandatory)] [ref]$NextId, [Parameter(Mandatory)] [hashtable]$SiteContext ) if ($Level -gt $MaxLevel) { return } $parent = Invoke-SPMWithRetry { Get-PnPFolder -Url $FolderUrl -Includes Folders } $subFolders = @($parent.Folders) | Where-Object { $_.Name -and $_.Name -ne 'Forms' -and -not $_.Name.StartsWith('_') } | Sort-Object Name foreach ($sub in $subFolders) { Write-Progress -Id 2 -ParentId 1 -Activity 'Scanne Struktur' -Status $sub.ServerRelativeUrl $hasUnique = $false $item = $null try { $item = Invoke-SPMWithRetry { Get-PnPProperty -ClientObject $sub -Property ListItemAllFields } if ($item -and $item.Id) { $hasUnique = [bool](Invoke-SPMWithRetry { Get-PnPProperty -ClientObject $item -Property HasUniqueRoleAssignments }) } } catch { Write-Warning "Berechtigungs-Flag fuer '$($sub.ServerRelativeUrl)' nicht abrufbar: $($_.Exception.Message)" } $assignments = if ($hasUnique) { Get-SPMRoleAssignmentSet -Securable $item -SiteContext $SiteContext } else { $ParentNode.assignments } $node = [pscustomobject]@{ id = 'n{0}' -f $NextId.Value parentId = $ParentNode.id type = 'Folder' title = $sub.Name url = $sub.ServerRelativeUrl level = $Level hasUniquePermissions = $hasUnique assignments = $assignments } $Nodes.Add($node) $NextId.Value++ if ($Level -lt $MaxLevel) { Add-SPMFolderNode -FolderUrl $sub.ServerRelativeUrl -ParentNode $node -Level ($Level + 1) -MaxLevel $MaxLevel -Nodes $Nodes -NextId $NextId -SiteContext $SiteContext } } } function Get-SPMSiteScan { <# .SYNOPSIS Scans a single site (site -> document libraries -> folders) against the current PnP connection. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SiteUrl, [Parameter(Mandatory)] [ValidateRange(0, 10)] [int]$Depth, [Parameter(Mandatory)] [hashtable]$GroupCache ) $siteContext = @{ Principals = [ordered]@{} CustomRoles = @{} GroupCache = $GroupCache } $web = Invoke-SPMWithRetry { Get-PnPWeb } Write-Progress -Id 2 -ParentId 1 -Activity 'Scanne Struktur' -Status $web.Url $nodes = [System.Collections.Generic.List[object]]::new() $siteNode = [pscustomobject]@{ id = 'n0' parentId = $null type = 'Site' title = if ($web.Title) { $web.Title } else { $web.Url } url = $web.Url level = 0 hasUniquePermissions = $true assignments = (Get-SPMRoleAssignmentSet -Securable $web -SiteContext $siteContext) } $nodes.Add($siteNode) $nextId = [ref]1 if ($Depth -ge 1) { # System libraries are recognized by their URL leaf - titles are localized # (Style Library = "Formatbibliothek", SiteAssets = "Websiteobjekte", ...). $systemUrlLeafs = @('Style Library', 'SiteAssets', 'FormServerTemplates', 'SitePages', 'PreservationHoldLibrary', 'Teams Wiki Data', 'IWConvertedForms') $lists = @(Invoke-SPMWithRetry { Get-PnPList }) | Where-Object { [int]$_.BaseTemplate -eq 101 -and -not $_.Hidden } | Sort-Object Title foreach ($list in $lists) { $rootFolder = Invoke-SPMWithRetry { Get-PnPProperty -ClientObject $list -Property RootFolder } $urlLeaf = ($rootFolder.ServerRelativeUrl.TrimEnd('/') -split '/')[-1] if ($urlLeaf -in $systemUrlLeafs) { Write-Verbose "System-Bibliothek uebersprungen: $($list.Title) ($($rootFolder.ServerRelativeUrl))" continue } Write-Progress -Id 2 -ParentId 1 -Activity 'Scanne Struktur' -Status "Bibliothek: $($list.Title)" $hasUnique = [bool](Invoke-SPMWithRetry { Get-PnPProperty -ClientObject $list -Property HasUniqueRoleAssignments }) $assignments = if ($hasUnique) { Get-SPMRoleAssignmentSet -Securable $list -SiteContext $siteContext } else { $siteNode.assignments } $listNode = [pscustomobject]@{ id = 'n{0}' -f $nextId.Value parentId = 'n0' type = 'Library' title = $list.Title url = $rootFolder.ServerRelativeUrl level = 1 hasUniquePermissions = $hasUnique assignments = $assignments } $nodes.Add($listNode) $nextId.Value++ if ($Depth -ge 2) { Add-SPMFolderNode -FolderUrl $rootFolder.ServerRelativeUrl -ParentNode $listNode -Level 2 -MaxLevel $Depth -Nodes $nodes -NextId $nextId -SiteContext $siteContext } } } Write-Progress -Id 2 -ParentId 1 -Activity 'Scanne Struktur' -Completed $principals = @($siteContext.Principals.Values) # Invert name -> key into key -> name for the report footnotes. $customRoles = [ordered]@{} foreach ($entry in ($siteContext.CustomRoles.GetEnumerator() | Sort-Object Value)) { $customRoles[$entry.Value] = $entry.Key } return [pscustomobject]@{ url = $web.Url title = $siteNode.title stats = [pscustomobject]@{ nodeCount = $nodes.Count uniqueNodeCount = @($nodes | Where-Object { $_.hasUniquePermissions -and $_.level -gt 0 }).Count principalCount = $principals.Count sharingLinkCount = @($principals | Where-Object { $_.type -eq 'SharingLink' }).Count externalPrincipalCount = @($principals | Where-Object { $_.loginName -match '(?i)#ext#' -or $_.upn -match '(?i)#ext#' }).Count } customRoles = $customRoles principals = $principals nodes = $nodes.ToArray() } } |