EndOfSupportCheck.ps1
|
<#PSScriptInfo
.VERSION 1.2026.1003.1047 .GUID 59130cb5-dea6-4453-a671-e4e275635282 .AUTHOR CodeFirst .COMPANYNAME CodeFirst Ltd .COPYRIGHT (c) CodeFirst Ltd. MIT licence. .TAGS EndOfSupport EndOfLife Lifecycle Inventory Windows WindowsServer SQLServer DotNet Office Audit .LICENSEURI https://github.com/graham-codefirst/end-of-support-check/blob/main/LICENSE .PROJECTURI https://www.codefirst.co.uk/tools/end-of-support-check/ .ICONURI https://www.codefirst.co.uk/logo.png .RELEASENOTES Support dates as of 2026-10-03. Full list: https://www.codefirst.co.uk/end-of-life/ #> <# .SYNOPSIS Lists the Windows, SQL Server, .NET and Office versions on this computer and the date each stops receiving security fixes. .DESCRIPTION Reads the Windows version, the registry keys for installed SQL Server instances, .NET Framework versions and Office, and the output of "dotnet --list-runtimes". Prints each with its end-of-support date and status. Read-only: changes nothing, needs no administrator rights and sends nothing anywhere. The only file it writes is the CSV asked for with -CsvPath. Dates come from https://www.codefirst.co.uk/end-of-life/ and are refreshed in each release. Check the vendor's own page before relying on a date for a decision. .PARAMETER CsvPath Optional path to save the results as CSV. .EXAMPLE EndOfSupportCheck.ps1 .EXAMPLE EndOfSupportCheck.ps1 -CsvPath .\support-status.csv .LINK https://www.codefirst.co.uk/tools/end-of-support-check/ #> <# End-of-support check CodeFirst Limited - https://www.codefirst.co.uk/tools/end-of-support-check/ Lists the Windows, SQL Server, .NET and Office versions found on this computer and the date each one stops receiving security fixes. What it reads: - the Windows version - the registry keys that record installed SQL Server instances, .NET Framework versions and Office - the output of "dotnet --list-runtimes", if .NET is installed It changes nothing, needs no administrator rights and sends nothing anywhere. The only file it writes is the CSV you ask for with -CsvPath. Run: powershell -ExecutionPolicy Bypass -File .\end-of-support-check.ps1 Save: powershell -ExecutionPolicy Bypass -File .\end-of-support-check.ps1 -CsvPath .\support-status.csv Support dates generated on 2026-10-03 from https://www.codefirst.co.uk/end-of-life/ Check the vendor's own page before relying on a date for a decision. #> param([string]$CsvPath) $ErrorActionPreference = 'SilentlyContinue' $today = Get-Date # Product, the date free security fixes end, and the date any paid extension ends. $dates = @{ 'sql-2008' = @{ Name = 'SQL Server 2008 and 2008 R2'; End = '2019-07-09'; PaidEnd = '2022-07-12' } 'sql-2012' = @{ Name = 'SQL Server 2012'; End = '2022-07-12'; PaidEnd = '2025-07-08' } 'sql-2014' = @{ Name = 'SQL Server 2014'; End = '2024-07-09'; PaidEnd = '2027-07-12' } 'sql-2016' = @{ Name = 'SQL Server 2016'; End = '2026-07-14'; PaidEnd = '2029-07-17' } 'sql-2017' = @{ Name = 'SQL Server 2017'; End = '2027-10-12'; PaidEnd = $null } 'sql-2019' = @{ Name = 'SQL Server 2019'; End = '2030-01-08'; PaidEnd = $null } 'sql-2022' = @{ Name = 'SQL Server 2022'; End = '2033-01-11'; PaidEnd = $null } 'sql-2025' = @{ Name = 'SQL Server 2025'; End = '2036-01-06'; PaidEnd = $null } 'netfx-35' = @{ Name = '.NET Framework 3.5 SP1'; End = '2029-01-09'; PaidEnd = $null } 'netfx-40' = @{ Name = '.NET Framework 4.0, 4.5 and 4.5.1'; End = '2016-01-12'; PaidEnd = $null } 'netfx-461' = @{ Name = '.NET Framework 4.5.2, 4.6 and 4.6.1'; End = '2022-04-26'; PaidEnd = $null } 'netfx-462' = @{ Name = '.NET Framework 4.6.2'; End = '2027-01-12'; PaidEnd = $null } 'netfx-48' = @{ Name = '.NET Framework 4.7 to 4.8.1'; End = $null; PaidEnd = $null } 'netcore-21' = @{ Name = '.NET Core 2.1'; End = '2021-08-21'; PaidEnd = $null } 'netcore-31' = @{ Name = '.NET Core 3.1'; End = '2022-12-13'; PaidEnd = $null } 'net-5' = @{ Name = '.NET 5'; End = '2022-05-10'; PaidEnd = $null } 'net-6' = @{ Name = '.NET 6'; End = '2024-11-12'; PaidEnd = $null } 'net-7' = @{ Name = '.NET 7'; End = '2024-05-14'; PaidEnd = $null } 'net-8' = @{ Name = '.NET 8'; End = '2026-11-10'; PaidEnd = $null } 'net-9' = @{ Name = '.NET 9'; End = '2026-11-10'; PaidEnd = $null } 'net-10' = @{ Name = '.NET 10'; End = '2028-11-14'; PaidEnd = $null } 'ws-2003' = @{ Name = 'Windows Server 2003'; End = '2015-07-14'; PaidEnd = $null } 'ws-2008' = @{ Name = 'Windows Server 2008 and 2008 R2'; End = '2020-01-14'; PaidEnd = '2023-01-10' } 'ws-2012' = @{ Name = 'Windows Server 2012 and 2012 R2'; End = '2023-10-10'; PaidEnd = '2026-10-13' } 'ws-2016' = @{ Name = 'Windows Server 2016'; End = '2027-01-12'; PaidEnd = '2030-01-12' } 'ws-2019' = @{ Name = 'Windows Server 2019'; End = '2029-01-09'; PaidEnd = $null } 'ws-2022' = @{ Name = 'Windows Server 2022'; End = '2031-10-14'; PaidEnd = $null } 'ws-2025' = @{ Name = 'Windows Server 2025'; End = '2034-11-14'; PaidEnd = $null } 'access-2010' = @{ Name = 'Office and Access 2010'; End = '2020-10-13'; PaidEnd = $null } 'access-2013' = @{ Name = 'Office and Access 2013'; End = '2023-04-11'; PaidEnd = $null } 'access-2016' = @{ Name = 'Office and Access 2016'; End = '2025-10-14'; PaidEnd = $null } 'access-2019' = @{ Name = 'Office and Access 2019'; End = '2025-10-14'; PaidEnd = $null } 'access-2021' = @{ Name = 'Office and Access 2021'; End = '2026-10-13'; PaidEnd = $null } 'access-2024' = @{ Name = 'Office and Access 2024'; End = '2029-10-09'; PaidEnd = $null } } $rows = New-Object System.Collections.ArrayList function Add-Row([string]$Component, [string]$Found, [string]$Id) { $product = '' $ends = '' $paid = '' $status = 'Not tracked' if ($Id -and $dates.ContainsKey($Id)) { $d = $dates[$Id] $product = $d.Name if ($d.PaidEnd) { $paid = ([datetime]$d.PaidEnd).ToString('d MMM yyyy') } if ($d.End) { $end = [datetime]$d.End $ends = $end.ToString('d MMM yyyy') if ($end -lt $today) { $status = 'OUT OF SUPPORT' } elseif ($end -lt $today.AddDays(365)) { $status = 'Ends within a year' } else { $status = 'Supported' } } else { $ends = 'No end date announced' $status = 'Supported' } } [void]$rows.Add([pscustomobject]@{ Component = $Component Found = $Found Product = $product 'Security fixes end' = $ends 'Paid extension to' = $paid Status = $status }) } # ---- Windows ---- $os = Get-CimInstance Win32_OperatingSystem if (-not $os) { $os = Get-WmiObject Win32_OperatingSystem } if ($os) { $build = [int]$os.BuildNumber $found = ('{0}, build {1}' -f $os.Caption.Trim(), $build) if ([int]$os.ProductType -eq 1) { Add-Row 'Windows' ($found + ' (desktop edition; this check covers server versions)') '' } else { $id = '' if ($build -ge 26100) { $id = 'ws-2025' } elseif ($build -ge 20348) { $id = 'ws-2022' } elseif ($build -ge 17763) { $id = 'ws-2019' } elseif ($build -ge 14393) { $id = 'ws-2016' } elseif ($build -ge 9200) { $id = 'ws-2012' } elseif ($build -ge 6001) { $id = 'ws-2008' } elseif ($build -ge 3790) { $id = 'ws-2003' } Add-Row 'Windows Server' $found $id } } # ---- SQL Server (every installed instance, including Express) ---- $sqlMajor = @{ 10 = 'sql-2008'; 11 = 'sql-2012'; 12 = 'sql-2014'; 13 = 'sql-2016'; 14 = 'sql-2017'; 15 = 'sql-2019'; 16 = 'sql-2022'; 17 = 'sql-2025' } $seen = @{} foreach ($root in 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Microsoft SQL Server') { $names = Get-ItemProperty ($root + '\Instance Names\SQL') if (-not $names) { continue } foreach ($p in $names.PSObject.Properties) { if ('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider' -contains $p.Name) { continue } $instanceId = [string]$p.Value if ($seen.ContainsKey($instanceId)) { continue } $seen[$instanceId] = $true $setup = Get-ItemProperty ($root + '\' + $instanceId + '\Setup') if (-not $setup) { continue } $major = 0 if ($setup.Version) { $major = [int]($setup.Version.Split('.')[0]) } Add-Row 'SQL Server' ('Instance {0}: {1}, version {2}' -f $p.Name, $setup.Edition, $setup.Version) $sqlMajor[$major] } } # ---- .NET Framework ---- $v35 = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5' if ($v35 -and $v35.Install -eq 1) { Add-Row '.NET Framework' ('3.5 ({0})' -f $v35.Version) 'netfx-35' } $v4 = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' if ($v4 -and $v4.Release) { $r = [int]$v4.Release $name = '4.5'; $id = 'netfx-40' if ($r -ge 533320) { $name = '4.8.1'; $id = 'netfx-48' } elseif ($r -ge 528040) { $name = '4.8'; $id = 'netfx-48' } elseif ($r -ge 461808) { $name = '4.7.2'; $id = 'netfx-48' } elseif ($r -ge 461308) { $name = '4.7.1'; $id = 'netfx-48' } elseif ($r -ge 460798) { $name = '4.7'; $id = 'netfx-48' } elseif ($r -ge 394802) { $name = '4.6.2'; $id = 'netfx-462' } elseif ($r -ge 394254) { $name = '4.6.1'; $id = 'netfx-461' } elseif ($r -ge 393295) { $name = '4.6'; $id = 'netfx-461' } elseif ($r -ge 379893) { $name = '4.5.2'; $id = 'netfx-461' } elseif ($r -ge 378675) { $name = '4.5.1'; $id = 'netfx-40' } Add-Row '.NET Framework' ('{0} (release {1})' -f $name, $r) $id } # ---- Modern .NET runtimes ---- $dotnet = Get-Command dotnet if ($dotnet) { $netId = @{ '2.1' = 'netcore-21'; '3.1' = 'netcore-31'; '5.0' = 'net-5'; '6.0' = 'net-6'; '7.0' = 'net-7'; '8.0' = 'net-8'; '9.0' = 'net-9'; '10.0' = 'net-10' } $runtimes = & dotnet --list-runtimes 2>$null | Where-Object { $_ -like 'Microsoft.NETCore.App *' } $newest = @{} foreach ($line in $runtimes) { $version = ($line -split ' ')[1] $parts = $version.Split('.') $key = $parts[0] + '.' + $parts[1] if (-not $newest.ContainsKey($key) -or ([version]($version -replace '-.*$', '')) -gt ([version]($newest[$key] -replace '-.*$', ''))) { $newest[$key] = $version } } foreach ($key in ($newest.Keys | Sort-Object { [version]$_ })) { Add-Row '.NET runtime' ('.NET {0} (latest installed {1})' -f $key, $newest[$key]) $netId[$key] } } # ---- Office and Access ---- $c2r = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' if ($c2r -and $c2r.ProductReleaseIds) { foreach ($product in ($c2r.ProductReleaseIds -split ',')) { $id = 'access-2016' $label = $product if ($product -match 'O365|M365|Microsoft365') { $id = ''; $label = $product + ' (Microsoft 365 subscription, updated continuously)' } elseif ($product -match '2024') { $id = 'access-2024' } elseif ($product -match '2021') { $id = 'access-2021' } elseif ($product -match '2019') { $id = 'access-2019' } Add-Row 'Office' ('{0}, version {1}' -f $label, $c2r.VersionToReport) $id } } else { $officeMsi = @{ '14.0' = 'access-2010'; '15.0' = 'access-2013'; '16.0' = 'access-2016' } foreach ($v in '14.0', '15.0', '16.0') { foreach ($root in 'HKLM:\SOFTWARE\Microsoft\Office', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office') { $install = Get-ItemProperty ($root + '\' + $v + '\Common\InstallRoot') if ($install -and $install.Path) { Add-Row 'Office' ('Office {0} at {1}' -f $v, $install.Path) $officeMsi[$v]; break } } } } # ---- Report ---- Write-Host '' Write-Host ('End-of-support check for {0} on {1}' -f $env:COMPUTERNAME, $today.ToString('d MMM yyyy')) $rows | Format-Table -AutoSize -Wrap | Out-String -Width 220 | Write-Host $ended = @($rows | Where-Object { $_.Status -eq 'OUT OF SUPPORT' }).Count $soon = @($rows | Where-Object { $_.Status -eq 'Ends within a year' }).Count Write-Host ('{0} item(s) out of support, {1} ending within a year, {2} checked.' -f $ended, $soon, $rows.Count) Write-Host 'This lists what is installed on this computer only. Applications, databases on other servers and anything a supplier hosts are not covered.' Write-Host 'Dates and what they mean: https://www.codefirst.co.uk/end-of-life/' Write-Host 'For a second opinion, send this output (not your code or data) to contact@codefirst.co.uk.' if ($CsvPath) { $rows | Export-Csv -Path $CsvPath -NoTypeInformation Write-Host ('Saved to {0}' -f $CsvPath) } |