Src/Private/Get-AbrSPSiteCollections.ps1
|
function Get-AbrSPSiteCollections { <# .SYNOPSIS Documents SharePoint Online site collections - inventory, storage, permissions, and settings. .DESCRIPTION Collects and reports on: - Tenant storage quota summary (total, used, available) - All site collections (URL, template, owner, storage, sharing level, last modified) - Site permissions & access (site admins, external sharing per site) - Inactive sites (no activity in 90+ days) - CIS compliance checks .NOTES Version: 0.1.2 Author: Pai Wei Sing #> [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [string]$TenantId ) begin { Write-PScriboMessage -Message "Collecting SharePoint Site Collections for $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Site Collections' } process { #region Pre-compute values outside Section{} to avoid PScribo scope loss $StorageQuotaAutoGrowEnabled = $false $AllSites = @() $SiteCount = 0 $InactiveSiteCount = 0 $TotalStorageGB = 0 $TotalQuotaGB = 0 $SiteObj = [System.Collections.ArrayList]::new() $SitePermObj = [System.Collections.ArrayList]::new() if ($script:PnPAvailable) { try { Write-Host " - Retrieving site collections (this may take a moment for large tenants)..." $AllSites = @(Get-PnPTenantSite -ErrorAction Stop | Where-Object { $_.Url -notlike '*/personal/*' } | Sort-Object Url) $SiteCount = $AllSites.Count Write-Host " - Found $SiteCount non-personal site collections." $SPTenantForStorage = Get-PnPTenant -ErrorAction SilentlyContinue if ($SPTenantForStorage) { $StorageQuotaAutoGrowEnabled = ($SPTenantForStorage.StorageQuotaAllocated -gt 0) $TotalQuotaGB = if ($SPTenantForStorage.StorageQuota -gt 0) { [math]::Round($SPTenantForStorage.StorageQuota / 1024, 0) } else { 0 } $TotalUsedGB = if ($SPTenantForStorage.StorageQuotaUsed -gt 0) { [math]::Round($SPTenantForStorage.StorageQuotaUsed / 1024, 2) } else { 0 } $TotalAvailGB = if ($TotalQuotaGB -gt 0) { $TotalQuotaGB - $TotalUsedGB } else { 0 } } foreach ($Site in $AllSites) { $StorageUsedGB = if ($Site.StorageUsageCurrent -gt 0) { [math]::Round($Site.StorageUsageCurrent / 1024, 2) } else { 0 } $StorageQuotaGB = if ($Site.StorageMaximumLevel -gt 0) { [math]::Round($Site.StorageMaximumLevel / 1024, 0) } else { 'Auto' } $StoragePct = if ($Site.StorageMaximumLevel -gt 0) { "$([math]::Round(($Site.StorageUsageCurrent / $Site.StorageMaximumLevel) * 100, 1))%" } else { '--' } $LastActivity = if ($Site.LastContentModifiedDate -and $Site.LastContentModifiedDate -ne [DateTime]::MinValue) { $Site.LastContentModifiedDate.ToString('yyyy-MM-dd') } else { 'Never / Unknown' } $DaysSinceActivity = if ($Site.LastContentModifiedDate -and $Site.LastContentModifiedDate -ne [DateTime]::MinValue) { ([DateTime]::UtcNow - $Site.LastContentModifiedDate).Days } else { 9999 } if ($DaysSinceActivity -gt 90) { $InactiveSiteCount++ } $TotalStorageGB += $StorageUsedGB $SharingLevel = ConvertTo-SPEnumString $Site.SharingCapability 'Unknown' $siteInObj = [ordered] @{ 'Site URL' = $Site.Url 'Title' = if ($Site.Title) { $Site.Title } else { '--' } 'Template' = $Site.Template 'Owner' = if ($Site.Owner) { $Site.Owner } else { '--' } 'Storage Used (GB)' = $StorageUsedGB 'Quota (GB)' = $StorageQuotaGB 'Storage %' = $StoragePct 'Last Modified' = $LastActivity 'External Sharing' = $SharingLevel 'Locked' = ($Site.LockState -ne 'Unlock') } $SiteObj.Add([pscustomobject](ConvertTo-HashToYN $siteInObj)) | Out-Null # Permissions & access per site $permInObj = [ordered] @{ 'Site URL' = $Site.Url 'Title' = if ($Site.Title) { $Site.Title } else { '--' } 'Primary Admin' = if ($Site.Owner) { $Site.Owner } else { '--' } 'External Sharing' = $SharingLevel 'Sharing Blocked' = ($Site.SharingCapability -eq 'Disabled') 'Anonymous Links' = ($SharingLevel -like '*Anyone*') 'Locked' = ($Site.LockState -ne 'Unlock') } $SitePermObj.Add([pscustomobject](ConvertTo-HashToYN $permInObj)) | Out-Null } } catch { Write-AbrDebugLog "Site collection retrieval failed: $($_.Exception.Message)" 'WARN' 'SITES' } } #endregion Section -Style Heading2 'Site Collections' { Paragraph "The following section documents all SharePoint Online site collections for tenant $TenantId, including storage consumption, permissions, sharing configuration, and governance settings." BlankLine if (-not $script:PnPAvailable) { Paragraph " [!] PnP.PowerShell is not available. Site collection data requires a PnP connection." } else { #region Tenant Storage Summary Section -Style Heading3 'Storage Summary' { Paragraph "Tenant-level SharePoint Online storage allocation and consumption." BlankLine $StorSumObj = [System.Collections.ArrayList]::new() $storInObj = [ordered] @{ 'Total Tenant Quota (GB)' = if ($TotalQuotaGB -gt 0) { $TotalQuotaGB } else { 'Auto-managed by Microsoft' } 'Total Storage Used (GB)' = $TotalStorageGB 'Available Storage (GB)' = if ($TotalQuotaGB -gt 0) { $TotalAvailGB } else { 'Auto-managed' } 'Total Site Collections' = $SiteCount 'Inactive Sites (90+ days)' = $InactiveSiteCount 'Sites with High Storage Usage (70%+)' = @($SiteObj | Where-Object { $p = $_.'Storage %' -replace '%','' $p -ne '--' -and [double]$p -ge 70 }).Count } $StorSumObj.Add([pscustomobject]$storInObj) | Out-Null $StorSumTableParams = @{ Name = "Tenant Storage Summary - $TenantId"; List = $true; ColumnWidths = 50, 50 } if ($Report.ShowTableCaptions) { $StorSumTableParams['Caption'] = "- $($StorSumTableParams.Name)" } $StorSumObj | Table @StorSumTableParams } BlankLine #endregion #region Site Collection Inventory if ($SiteObj.Count -gt 0) { Section -Style Heading3 'Site Collection Inventory' { Paragraph "All SharePoint Online site collections. Total: $SiteCount | Inactive (90+ days): $InactiveSiteCount" BlankLine if ($InfoLevel.SiteCollections -ge 2) { $null = (& { if ($HealthCheck.SharePoint.SiteCollections) { $null = ($SiteObj | Where-Object { $p = $_.'Storage %' -replace '%',''; $p -ne '--' -and [double]$p -ge 90 } | Set-Style -Style Critical | Out-Null) $null = ($SiteObj | Where-Object { $p = $_.'Storage %' -replace '%',''; $p -ne '--' -and [double]$p -ge 70 -and [double]$p -lt 90 } | Set-Style -Style Warning | Out-Null) $null = ($SiteObj | Where-Object { $_.'External Sharing' -like '*Anyone*' } | Set-Style -Style Warning | Out-Null) } }) $SiteTableParams = @{ Name = "Site Collection Inventory - $TenantId"; ColumnWidths = 28, 13, 9, 12, 7, 5, 6, 8, 5, 7 } if ($Report.ShowTableCaptions) { $SiteTableParams['Caption'] = "- $($SiteTableParams.Name)" } $SiteObj | Table @SiteTableParams } else { # InfoLevel 1: summary only $SumObj = [System.Collections.ArrayList]::new() $sumInObj = [ordered] @{ 'Total Site Collections' = $SiteCount 'Inactive Sites (90+ days)' = $InactiveSiteCount 'Sites with External Sharing (Anyone)' = @($SiteObj | Where-Object { $_.'External Sharing' -like '*Anyone*' }).Count 'Sites with High Storage (70%+)' = @($SiteObj | Where-Object { $p = $_.'Storage %' -replace '%',''; $p -ne '--' -and [double]$p -ge 70 }).Count } $SumObj.Add([pscustomobject]$sumInObj) | Out-Null $SumTableParams = @{ Name = "Site Collection Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $SumTableParams['Caption'] = "- $($SumTableParams.Name)" } $SumObj | Table @SumTableParams } $script:ExcelSheets['Site Collections'] = $SiteObj } BlankLine #region Site Permissions & Access Section -Style Heading3 'Site Permissions & Access' { Paragraph "External sharing configuration and access settings per site collection. Sites flagged as 'Anyone' allow unauthenticated anonymous access and should be reviewed." BlankLine Paragraph "Detailed permissions (users, groups, permission levels, and library-level unique permissions) are exported to the Excel workbook -- see sheets: 'Site Role Assignments', 'SP Groups & Members', and 'Library Permissions'." BlankLine # Summary counts $DisabledCount = @($SitePermObj | Where-Object { $_.'External Sharing' -eq 'Disabled' }).Count $AnyoneCount = @($SitePermObj | Where-Object { $_.'Anonymous Links' -eq 'Yes' }).Count $GuestOnlyCount = @($SitePermObj | Where-Object { $_.'External Sharing' -like '*Guest*' -or $_.'External Sharing' -like '*External*' }).Count $LockedCount = @($SitePermObj | Where-Object { $_.'Locked' -eq 'Yes' }).Count $PermSumObj = [System.Collections.ArrayList]::new() $permSumInObj = [ordered] @{ 'Total Sites' = $SiteCount 'External Sharing Disabled' = $DisabledCount 'Anonymous (Anyone) Links' = $AnyoneCount 'Guest/External User Sharing' = $GuestOnlyCount 'Locked Sites' = $LockedCount } $PermSumObj.Add([pscustomobject]$permSumInObj) | Out-Null $null = (& { if ($HealthCheck.SharePoint.SiteCollections) { $null = ($PermSumObj | Where-Object { $_.'Anonymous (Anyone) Links' -gt 0 } | Set-Style -Style Critical | Out-Null) } }) $PermSumTableParams = @{ Name = "Permissions Summary - $TenantId"; List = $true; ColumnWidths = 50, 50 } if ($Report.ShowTableCaptions) { $PermSumTableParams['Caption'] = "- $($PermSumTableParams.Name)" } $PermSumObj | Table @PermSumTableParams BlankLine if ($InfoLevel.SiteCollections -ge 2) { $null = (& { if ($HealthCheck.SharePoint.SiteCollections) { $null = ($SitePermObj | Where-Object { $_.'Anonymous Links' -eq 'Yes' } | Set-Style -Style Critical | Out-Null) $null = ($SitePermObj | Where-Object { $_.'Sharing Blocked' -eq 'Yes' } | Set-Style -Style OK | Out-Null) } }) $PermTableParams = @{ Name = "Site Permissions & Access - $TenantId"; ColumnWidths = 30, 15, 17, 16, 8, 9, 5 } if ($Report.ShowTableCaptions) { $PermTableParams['Caption'] = "- $($PermTableParams.Name)" } $SitePermObj | Table @PermTableParams $script:ExcelSheets['Site Permissions'] = $SitePermObj } } #endregion } #endregion #region CIS Baseline Assessment if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Site Collection Governance" $CISDefs = Get-AbrSPCISChecks -Section 'SiteCollections' $CISVars = @{ StorageQuotaAutoGrowEnabled = $StorageQuotaAutoGrowEnabled } $CISChecks = Build-AbrSPComplianceChecks -Definitions $CISDefs -Framework 'CIS' -CallerVariables $CISVars New-AbrSPCISAssessmentTable -Checks $CISChecks -Name 'Site Collections' -TenantId $TenantId foreach ($row in $CISChecks) { $null = $script:CISAllChecks.Add([pscustomobject](@{ Section = 'Site Collections' } + ($row | ConvertTo-HashTableSP))) } } #endregion #region Detailed Permissions (Excel export -- InfoLevel 2 only) # Collects site role assignments, SP group membership, and library-level # unique permissions for all sites. Excel-only -- too large for Word report. if ($InfoLevel.SiteCollections -ge 2 -and $AllSites.Count -gt 0) { # Read MaxPermissionSites from JSON config (default 30, set 0 to skip) $MaxPerms = 30 try { if ($null -ne $script:Options.MaxPermissionSites) { $MaxPerms = [int]$script:Options.MaxPermissionSites } } catch { } if ($MaxPerms -gt 0) { Write-Host " - Collecting detailed site permissions for Excel export (max $MaxPerms sites)..." Get-AbrSPSitePermissions ` -TenantId $TenantId ` -Sites $AllSites ` -MaxSites $MaxPerms } else { Write-Host " - Site permission collection skipped (Options.MaxPermissionSites = 0)" -ForegroundColor DarkGray } } #endregion } } } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Site Collections' } } |