<# .SYNOPSIS SharePoint Production report for the farm.SharePoint Production Toolkit - Script 22 - SharePoint Cumulative Updates Report Enhancements included: - CU build number -> automatic CU label mapping - Out-of-date detection vs latest known CU - Upgrade Ready / Not Ready export flag This script is safe to run in production and does not make changes. .PARAMETER OutputCsv Full path to the detail CSV output. .PARAMETER NoPrompt Optional. Skips the confirmation prompt. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$OutputCsv, [switch]$NoPrompt ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Write-Host "" Write-Host "SCRIPT 22 - SHAREPOINT CUMULATIVE UPDATES REPORT" -ForegroundColor Cyan Write-Host "READ-ONLY. NO CHANGES ARE MADE." -ForegroundColor Green Write-Host "" # ------------------------------------------------------------ # Load SharePoint PowerShell snap-in # ------------------------------------------------------------ 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 in SharePoint Management Shell. Error: $($_.Exception.Message)" } # ------------------------------------------------------------ # Output setup # ------------------------------------------------------------ $outDir = Split-Path -Path $OutputCsv -Parent if ([string]::IsNullOrWhiteSpace($outDir)) { throw "OutputCsv must be a full path. Example: C:\Temp\SharePoint_CU_Report.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.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) # ------------------------------------------------------------ # Error / log helpers # ------------------------------------------------------------ $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 = "[{0}] {1}" -f $stamp, $Message $runMessages.Add($line) | Out-Null Write-Host $line } if (-not $NoPrompt) { Write-Host "This script reads SharePoint build / update information and writes reports only." -ForegroundColor Yellow $resp = Read-Host "Type YES to continue" if ($resp -ne "YES") { Write-Host "Cancelled by user." -ForegroundColor Yellow return } } # ------------------------------------------------------------ # Verified CU mapping table # Only includes builds explicitly verified from current sources. # ------------------------------------------------------------ $KnownCUMap = @( [pscustomobject]@{ ProductFamily = "SharePoint Server 2019" BuildVersion = "16.0.10417.20047" CULabel = "September 2025 CU" ReleaseDate = [datetime]"2025-09-09" }, [pscustomobject]@{ ProductFamily = "SharePoint Server 2019" BuildVersion = "16.0.10417.20114" CULabel = "April 2026 CU" ReleaseDate = [datetime]"2026-04-14" }, [pscustomobject]@{ ProductFamily = "SharePoint Server Subscription Edition" BuildVersion = "16.0.19127.20100" CULabel = "September 2025 CU" ReleaseDate = [datetime]"2025-09-09" }, [pscustomobject]@{ ProductFamily = "SharePoint Server Subscription Edition" BuildVersion = "16.0.19725.20210" CULabel = "April 2026 CU" ReleaseDate = [datetime]"2026-04-14" } ) # Latest known CU per product family from the verified table above $LatestKnownByFamily = @{} $KnownCUMap | Group-Object ProductFamily | ForEach-Object { $latest = $_.Group | Sort-Object ReleaseDate -Descending | Select-Object -First 1 $LatestKnownByFamily[$_.Name] = $latest } # ------------------------------------------------------------ # Helper functions # ------------------------------------------------------------ function Convert-ToVersionObject { param([string]$BuildVersion) try { return [version]$BuildVersion } catch { return $null } } function Get-ProductFamilyFromBuild { param([string]$BuildVersion) if ([string]::IsNullOrWhiteSpace($BuildVersion)) { return "Unknown" } # Exact match first $exact = $KnownCUMap | Where-Object { $_.BuildVersion -eq $BuildVersion } | Select-Object -First 1 if ($exact) { return $exact.ProductFamily } # Prefix match for known families if ($BuildVersion -like "16.0.10417.*") { return "SharePoint Server 2019" } if (($BuildVersion -like "16.0.19127.*") -or ($BuildVersion -like "16.0.19725.*")) { return "SharePoint Server Subscription Edition" } return "Unknown" } function Get-CULabelFromBuild { param([string]$BuildVersion) $match = $KnownCUMap | Where-Object { $_.BuildVersion -eq $BuildVersion } | Select-Object -First 1 if ($match) { return $match.CULabel } return "Unknown / Not In Mapping Table" } function Get-LatestKnownBuild { param([string]$ProductFamily) if ($LatestKnownByFamily.ContainsKey($ProductFamily)) { return $LatestKnownByFamily[$ProductFamily].BuildVersion } return "" } function Get-LatestKnownCULabel { param([string]$ProductFamily) if ($LatestKnownByFamily.ContainsKey($ProductFamily)) { return $LatestKnownByFamily[$ProductFamily].CULabel } return "" } function Get-IsOutOfDate { param( [string]$CurrentBuild, [string]$ProductFamily ) if ([string]::IsNullOrWhiteSpace($CurrentBuild)) { return $true } if (-not $LatestKnownByFamily.ContainsKey($ProductFamily)) { return $true } $current = Convert-ToVersionObject -BuildVersion $CurrentBuild $latest = Convert-ToVersionObject -BuildVersion $LatestKnownByFamily[$ProductFamily].BuildVersion if (($null -eq $current) -or ($null -eq $latest)) { return $true } return ($current -lt $latest) } function Get-UpgradeReadyFlag { param( [string]$ProductFamily, [string]$CurrentBuild, [bool]$IsOutOfDate ) if ($ProductFamily -eq "Unknown") { return "Not Ready" } if ([string]::IsNullOrWhiteSpace($CurrentBuild)) { return "Not Ready" } if ($IsOutOfDate) { return "Not Ready" } return "Ready" } function Get-RiskLevel { param( [string]$UpgradeReady, [string]$CurrentBuildLabel ) if ($UpgradeReady -eq "Not Ready") { return "High" } if ($CurrentBuildLabel -like "Unknown*") { return "Medium" } 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]$UpgradeReady, [string]$CurrentBuildLabel, [string]$LatestKnownCULabel ) if ($UpgradeReady -eq "Not Ready") { if ($CurrentBuildLabel -like "Unknown*") { return "Current build was not matched in the verified mapping table. Review build level and update mapping before upgrade readiness decisions." } return ("Patch this server to the latest known mapped CU ({0}) before treating it as upgrade-ready." -f $LatestKnownCULabel) } return "Server is at the latest known mapped CU level and is marked upgrade-ready." } # ------------------------------------------------------------ # Main execution # ------------------------------------------------------------ $results = New-Object System.Collections.Generic.List[object] try { Log-Message "Retrieving farm build version..." $farmBuildVersion = (Get-SPFarm).BuildVersion.ToString() } catch { Add-ErrorRecord -Stage "FarmBuild" -Scope "Farm" -ObjectTitle "Get-SPFarm" -Message $_.Exception.Message throw } try { Log-Message "Retrieving farm servers..." $servers = Get-SPServer | Select-Object Address, Role, Version } catch { Add-ErrorRecord -Stage "Servers" -Scope "Farm" -ObjectTitle "Get-SPServer" -Message $_.Exception.Message throw } # Optional product retrieval for reference / diagnostics try { Log-Message "Retrieving installed SharePoint products..." $null = Get-SPProduct } catch { Add-ErrorRecord -Stage "Products" -Scope "Farm" -ObjectTitle "Get-SPProduct" -Message $_.Exception.Message } foreach ($server in $servers) { try { $serverName = [string]$server.Address $serverRole = [string]$server.Role $serverVersion = [string]$server.Version $productFamily = Get-ProductFamilyFromBuild -BuildVersion $serverVersion $currentCULabel = Get-CULabelFromBuild -BuildVersion $serverVersion $latestKnownBuild = Get-LatestKnownBuild -ProductFamily $productFamily $latestKnownCULabel= Get-LatestKnownCULabel -ProductFamily $productFamily $isOutOfDate = Get-IsOutOfDate -CurrentBuild $serverVersion -ProductFamily $productFamily $upgradeReady = Get-UpgradeReadyFlag -ProductFamily $productFamily -CurrentBuild $serverVersion -IsOutOfDate $isOutOfDate $riskLevel = Get-RiskLevel -UpgradeReady $upgradeReady -CurrentBuildLabel $currentCULabel $results.Add([pscustomobject]@{ ServerName = $serverName ServerRole = $serverRole ServerBuildVersion = $serverVersion FarmBuildVersion = $farmBuildVersion ProductFamily = $productFamily CurrentCULabel = $currentCULabel LatestKnownBuild = $latestKnownBuild LatestKnownCULabel = $latestKnownCULabel IsOutOfDate = $isOutOfDate UpgradeReady = $upgradeReady RiskLevel = $riskLevel Score = Get-Score -RiskLevel $riskLevel Category = "CumulativeUpdates" ActionRecommendation= Get-Recommendation -UpgradeReady $upgradeReady -CurrentBuildLabel $currentCULabel -LatestKnownCULabel $latestKnownCULabel }) | Out-Null } catch { Add-ErrorRecord -Stage "ProcessServer" -Scope "Server" -ObjectTitle $server.Address -Message $_.Exception.Message } } # ------------------------------------------------------------ # Export reports # ------------------------------------------------------------ try { $results | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8 } catch { throw "Failed to export detail report. $($_.Exception.Message)" } try { $results | Group-Object UpgradeReady, RiskLevel | ForEach-Object { $parts = $_.Name -split ', ' [pscustomobject]@{ UpgradeReady = $parts[0] RiskLevel = $parts[1] Count = $_.Count } } | Export-Csv -Path $summaryPath -NoTypeInformation -Encoding UTF8 } catch { Add-ErrorRecord -Stage "ExportSummary" -Scope $summaryPath -ObjectTitle "SummaryExport" -Message $_.Exception.Message } try { $runMessages | Set-Content -Path $logPath -Encoding UTF8 } catch { Add-ErrorRecord -Stage "ExportRunLog" -Scope $logPath -ObjectTitle "RunLog" -Message $_.Exception.Message } 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 "Failed to export error report." -ForegroundColor Red } 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 Write-Host "" Write-Host "Complete." -ForegroundColor Green .DESCRIPTION READ-ONLY.