End-of-support check
A free script that lists the versions of Windows, SQL Server, .NET and Office on a Windows machine, and the date each one stops receiving security fixes. It runs on your machine and sends nothing anywhere.
No form, no sign-up. Dates in the script were generated on 3 October 2026 from our end-of-support pages.
How to run it
It works with the PowerShell already on every Windows Server from 2012 onwards.
- Download the script and copy it to the server you want to check.
- Open PowerShell in the folder you saved it to. You do not need to run it as an administrator.
- Run:
powershell -ExecutionPolicy Bypass -File .\end-of-support-check.ps1 To keep the result as a spreadsheet, add a file name:
powershell -ExecutionPolicy Bypass -File .\end-of-support-check.ps1 -CsvPath .\support-status.csv The -ExecutionPolicy Bypass part applies to this one command only. It lets a downloaded script run without changing any setting on the server.
What the result looks like
An example for an imaginary server, with each status worked out as of today.
| Component | Found | Security fixes end | Status |
|---|---|---|---|
| Windows Server | Windows Server 2016 Standard, build 14393 | 12 January 2027 | Ending within a year |
| SQL Server | Instance MSSQLSERVER: Standard Edition, version 13.2.5026.0 | 14 July 2026 | Support ended |
| SQL Server | Instance ACCOUNTS: Express Edition, version 12.3.6024.0 | 9 July 2024 | Support ended |
| .NET Framework | 4.8 (release 528049) | No end date announced | Supported |
| .NET runtime | .NET 6.0 (latest installed 6.0.36) | 12 November 2024 | Support ended |
The second database in the example is the kind people miss: a copy of SQL Server Express installed years ago by another product, which nobody thinks of as a database server.
What it reads
- The Windows version
- The name and build number of the operating system.
- Installed SQL Server instances
- From the registry keys SQL Server writes when it is installed, including Express instances put there by other products.
- .NET Framework and .NET
- The .NET Framework release recorded in the registry, and the output of dotnet --list-runtimes.
- Office and Access
- The installed Office products, from the registry.
What it does not do
- It does not send anything
- There is no network call in the script. The result appears on your screen and, if you ask for it, in a file you choose.
- It does not change anything
- It only reads. It needs no administrator rights and installs nothing.
- It does not read your data
- It never opens a database, a document or your source code.
- It does not see other machines
- Run it on each server you want to check. Anything a supplier hosts for you is not covered.
What to do with the result
Anything marked out of support, or ending within a year, has a page on this site that explains what stopped and what the options are. Start with the end-of-support dates.
If you would like a second opinion, send the output to contact@codefirst.co.uk. Send the table, not your code or data. An engineer will reply with what we would look at first, at no charge.
The script tells you what is installed. It cannot tell you what your applications depend on, or what would break in an upgrade. That is what a code audit is for.
The whole script
You should not run a script from the internet without reading it. This is exactly what the download contains.
Show the script (217 lines)
<#
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)
}
Found something out of support?
Tell us which version, what depends on it and how it is hosted. We will set out the upgrade route and what it is likely to involve.