<# .SYNOPSIS SharePoint Production Toolkit -(SharePoint Production Toolkit - Script 01 - Site Inventory Report [Parameter(Mandatory = $true)] [string]$WebAppUrl, [Parameter(Mandatory = $true)] [string]$OutputCsv, [string]$SiteCollectionUrl, [switch]$NoPrompt, [switch]$Remediate ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Write-Host "" Write-Host "SCRIPT 01 - SITE INVENTORY REPORT" -ForegroundColor Cyan Write-Host "READ-ONLY. NO CHANGES ARE MADE." -ForegroundColor Green Write-Host "" try { if (-not (Get-PSSnapin -Name "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue)) { Add-PSSnapin "Microsoft.SharePoint.PowerShell" } } catch { throw "Unable to load SharePoint snap-in. Run this in SharePoint Management Shell. Error: $($_.Exception.Message)" } $outDir = Split-Path -Path $OutputCsv -Parent if ([string]::IsNullOrWhiteSpace($outDir)) { throw "OutputCsv must be a full path. Example: C:\Temp\Site_Inventory.csv" } if (-not (Test-Path -Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null } $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss") $baseName = [System.IO.Path]::GetFileNameWithoutExtension($OutputCsv) $summaryPath = Join-Path $outDir ("{0}_{1}_Summary_ByTemplate.csv" -f $baseName, $timestamp) $logPath = Join-Path $outDir ("{0}_{1}_RunLog.txt" -f $baseName, $timestamp) $errorPath = Join-Path $outDir ("{0}_{1}_Errors.csv" -f $baseName, $timestamp) $errors = New-Object System.Collections.Generic.List[object] function Add-ErrorRecord { param( [string]$Stage, [string]$Scope, [string]$ObjectTitle, [string]$Message ) $errors.Add([pscustomobject]@{ Timestamp = Get-Date Stage = $Stage Scope = $Scope ObjectTitle = $ObjectTitle Message = $Message }) | Out-Null } $runMessages = New-Object System.Collections.Generic.List[string] function Log-Message { param([string]$Message) $stamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") $line = "[$stamp] $Message" $runMessages.Add($line) | Out-Null Write-Host $line } if ($Remediate) { Write-Host "Note: -Remediate was provided, but this script is report-only and will not make changes." -ForegroundColor Yellow } if (-not $NoPrompt) { Write-Host "This script scans SharePoint and writes reports only." -ForegroundColor Yellow Write-Host "It does not modify content, permissions, or configuration." -ForegroundColor Yellow $resp = Read-Host "Type YES to continue" if ($resp -ne "YES") { Write-Host "Cancelled by user." -ForegroundColor Yellow return } } function Resolve-SitesToScan { param( [string]$WebAppUrl, [string]$SiteCollectionUrl ) if (-not [string]::IsNullOrWhiteSpace($SiteCollectionUrl)) { return @(Get-SPSite -Identity $SiteCollectionUrl) } return @(Get-SPSite -WebApplication $WebAppUrl -Limit All) } function Get-RiskLevel { param( [datetime]$LastModified, [double]$SizeMB, [int]$WebCount ) $daysInactive = 0 try { $daysInactive = (New-TimeSpan -Start $LastModified -End (Get-Date)).Days } catch { $daysInactive = 9999 } if ($daysInactive -gt 365 -or $SizeMB -gt 5120 -or $WebCount -gt 25) { return "High" } elseif ($daysInactive -gt 180 -or $SizeMB -gt 1024 -or $WebCount -gt 10) { return "Medium" } else { return "Low" } } function Get-Score { param([string]$RiskLevel) switch ($RiskLevel) { "High" { return 30 } "Medium" { return 60 } "Low" { return 90 } default { return 50 } } } function Get-Recommendation { param( [string]$RiskLevel, [datetime]$LastModified, [double]$SizeMB, [int]$WebCount ) $daysInactive = 0 try { $daysInactive = (New-TimeSpan -Start $LastModified -End (Get-Date)).Days } catch { $daysInactive = 9999 } if ($RiskLevel -eq "High") { return "Review for archive, cleanup, or phased migration planning. This site shows elevated size, inactivity, or structural complexity." } elseif ($RiskLevel -eq "Medium") { return "Validate site usage, ownership, and migration readiness before moving this site." } else { return "Good baseline migration candidate. Validate business value and include in migration planning." } } $results = New-Object System.Collections.Generic.List[object] try { $sitesToScan = Resolve-SitesToScan -WebAppUrl $WebAppUrl -SiteCollectionUrl $SiteCollectionUrl Log-Message ("Resolved {0} site collection(s) for inventory scan." -f $sitesToScan.Count) } catch { Add-ErrorRecord -Stage "ResolveSites" -Scope $WebAppUrl -ObjectTitle "Resolve-SitesToScan" -Message $_.Exception.Message throw } foreach ($site in $sitesToScan) { $rootWeb = $null try { Log-Message ("Scanning site collection: {0}" -f $site.Url) $rootWeb = $site.RootWeb $siteSizeMB = 0 try { $siteSizeMB = [math]::Round(($site.Usage.Storage / 1MB), 2) } catch { $siteSizeMB = 0 } $webCount = 1 try { $webCount = @($site.AllWebs).Count } catch { $webCount = 1 } $lastModified = $null try { $lastModified = $site.LastContentModifiedDate } catch { $lastModified = Get-Date "1900-01-01" } $contentDbName = "" try { $contentDbName = [string]$site.ContentDatabase.Name } catch { $contentDbName = "" } $owner = "" try { $owner = [string]$site.Owner } catch { $owner = "" } $riskLevel = Get-RiskLevel -LastModified $lastModified -SizeMB $siteSizeMB -WebCount $webCount $score = Get-Score -RiskLevel $riskLevel $recommendation = Get-Recommendation -RiskLevel $riskLevel -LastModified $lastModified -SizeMB $siteSizeMB -WebCount $webCount $results.Add([pscustomobject]@{ SiteCollectionUrl = $site.Url ObjectName = $rootWeb.Title RootWebTitle = $rootWeb.Title WebTemplate = [string]$rootWeb.WebTemplate WebTemplateId = [string]$rootWeb.WebTemplateId Owner = $owner ContentDatabase = $contentDbName SizeMB = $siteSizeMB WebCount = $webCount LastContentModified = $lastModified RiskLevel = $riskLevel Score = $score Category = "SiteInventory" ActionRecommendation = $recommendation }) | Out-Null } catch { Add-ErrorRecord -Stage "CollectSiteInventory" -Scope $WebAppUrl -ObjectTitle $site.Url -Message $_.Exception.Message Log-Message ("FAILED site collection scan: {0} - {1}" -f $site.Url, $_.Exception.Message) } finally { try { if ($rootWeb) { $rootWeb.Dispose() } } catch { } try { $site.Dispose() } catch { } } } try { $results | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8 $results | Group-Object WebTemplate | ForEach-Object { [pscustomobject]@{ WebTemplate = $_.Name Count = $_.Count } } | Export-Csv -Path $summaryPath -NoTypeInformation -Encoding UTF8 $runMessages | Set-Content -Path $logPath -Encoding UTF8 Write-Host "" Write-Host ("DETAIL REPORT: {0}" -f $OutputCsv) -ForegroundColor Green Write-Host ("SUMMARY REPORT: {0}" -f $summaryPath) -ForegroundColor Green Write-Host ("RUN LOG: {0}" -f $logPath) -ForegroundColor Green } catch { Write-Host "Failed to export report files." -ForegroundColor Red Write-Host $_.Exception.Message -ForegroundColor Red } try { if ($errors.Count -gt 0) { $errors | Export-Csv -Path $errorPath -NoTypeInformation -Encoding UTF8 Write-Host ("ERROR REPORT: {0}" -f $errorPath) -ForegroundColor Yellow } } catch { } Write-Host "" Write-Host "Complete." -ForegroundColor Green .DESCRIPTION READ-ONLY. Scans a SharePoint web application or a single site collection and exports a site inventory report for migration planning, governance, and modernization review. .PARAMETER WebAppUrl Target SharePoint Web Application URL. .PARAMETER OutputCsv Full path to the detail CSV output file. .PARAMETER SiteCollectionUrl Optional. Limit the scan to a single site collection. .PARAMETER NoPrompt Optional. Skips the confirmation prompt. .PARAMETER Remediate Optional. Included for toolkit consistency only. This script does not make changes. #> [CmdletBinding()]