Problem overview
Manually verifying each component required for Windows 11—such as disk space, memory, TPM version, and CPU capabilities—can be both time-consuming and prone to human error. This script streamlines that process by automatically detecting and reporting hardware readiness, helping IT teams and MSPs quickly identify machines suitable for Windows 11 upgrades.
<#
Level Library
Windows 11 25H2 Upgrade Readiness Check
Checks whether a Windows 11 23H2 device appears ready for a Windows 11 25H2 feature update.
Exit codes:
0 = Ready / already on 25H2
1 = Not ready / unsupported / not applicable
#>
$exitCode = 0
function Write-Color {
param(
[string]$Message,
[string]$Color
)
$colorHash = @{
'Red' = [ConsoleColor]::Red
'Green' = [ConsoleColor]::Green
'Yellow' = [ConsoleColor]::Yellow
}
Write-Host $Message -ForegroundColor $colorHash[$Color]
}
function Print-CheckResult {
param(
[string]$CheckName,
[string]$Status,
[string]$Details
)
switch ($Status) {
"PASS" { Write-Color "$($CheckName): $($Status); $($Details)" "Green" }
"FAIL" { Write-Color "$($CheckName): $($Status); $($Details)" "Red" }
default { Write-Color "$($CheckName): $($Status); $($Details)" "Yellow" }
}
}
# OS / Version Guard
try {
$os = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
$productName = $os.ProductName
$displayVersion = $os.DisplayVersion
$currentBuild = $os.CurrentBuild
$ubr = $os.UBR
Print-CheckResult "OS" "PASS" "Detected $productName $displayVersion, Build $currentBuild.$ubr"
if ($productName -notlike "*Windows 11*") {
Print-CheckResult "OS" "FAIL" "This check is intended for Windows 11 devices only."
exit 1
}
if ($displayVersion -eq "25H2") {
Print-CheckResult "OS" "PASS" "Device is already running Windows 11 25H2."
exit 0
}
if ($displayVersion -ne "23H2") {
Print-CheckResult "OS" "FAIL" "Expected Windows 11 23H2, but found $displayVersion."
exit 1
}
}
catch {
Print-CheckResult "OS" "FAIL" "Unable to determine Windows version. $($_.Exception.Message)"
exit 1
}
[int]$MinOSDiskSizeGB = 64
[int]$MinMemoryGB = 4
[uint32]$MinClockSpeedMHz = 1000
[uint32]$MinLogicalCores = 2
[uint16]$RequiredAddressWidth = 64
# Storage
try {
$osDrive = Get-WmiObject -Class Win32_OperatingSystem | Select-Object -ExpandProperty SystemDrive
$osDriveSize = Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceID='$osDrive'" |
Select-Object @{Name = "SizeGB"; Expression = { $_.Size / 1GB -as [int] } }
if ($null -eq $osDriveSize) {
Print-CheckResult "Storage" "FAIL" "Storage is null"
$exitCode = 1
}
elseif ($osDriveSize.SizeGB -lt $MinOSDiskSizeGB) {
Print-CheckResult "Storage" "FAIL" "OSDiskSize=$($osDriveSize.SizeGB)GB, Minimum Required=$MinOSDiskSizeGB GB"
$exitCode = 1
}
else {
Print-CheckResult "Storage" "PASS" "OSDiskSize=$($osDriveSize.SizeGB)GB"
}
}
catch {
Print-CheckResult "Storage" "UNDETERMINED" "Exception: $($_.Exception.Message)"
$exitCode = 1
}
# Memory
try {
$memory = Get-WmiObject Win32_PhysicalMemory |
Measure-Object -Property Capacity -Sum |
Select-Object @{Name = "SizeGB"; Expression = { $_.Sum / 1GB -as [int] } }
if ($null -eq $memory) {
Print-CheckResult "Memory" "FAIL" "Memory is null"
$exitCode = 1
}
elseif ($memory.SizeGB -lt $MinMemoryGB) {
Print-CheckResult "Memory" "FAIL" "SystemMemory=$($memory.SizeGB)GB, Minimum Required=$MinMemoryGB GB"
$exitCode = 1
}
else {
Print-CheckResult "Memory" "PASS" "SystemMemory=$($memory.SizeGB)GB"
}
}
catch {
Print-CheckResult "Memory" "UNDETERMINED" "Exception: $($_.Exception.Message)"
$exitCode = 1
}
# TPM
try {
$tpm = Get-Tpm
if ($null -eq $tpm -or -not $tpm.TpmPresent) {
Print-CheckResult "TPM" "FAIL" "TPM not present"
$exitCode = 1
}
else {
$tpmVersion = Get-WmiObject -Class Win32_Tpm -Namespace root\CIMV2\Security\MicrosoftTpm |
Select-Object -ExpandProperty SpecVersion
if ([string]::IsNullOrWhiteSpace($tpmVersion)) {
Print-CheckResult "TPM" "FAIL" "TPMVersion=null"
$exitCode = 1
}
else {
$majorVersion = $tpmVersion.Split(",")[0] -as [int]
if ($majorVersion -lt 2) {
Print-CheckResult "TPM" "FAIL" "TPMVersion=$tpmVersion"
$exitCode = 1
}
else {
Print-CheckResult "TPM" "PASS" "TPMVersion=$tpmVersion"
}
}
}
}
catch {
Print-CheckResult "TPM" "UNDETERMINED" "Exception: $($_.Exception.Message)"
$exitCode = 1
}
# CPU
try {
$cpuDetails = @(Get-WmiObject -Class Win32_Processor)[0]
if ($null -eq $cpuDetails) {
Print-CheckResult "Processor" "FAIL" "CPU details are null"
$exitCode = 1
}
else {
$processorCheckFailed = $false
if ($null -eq $cpuDetails.AddressWidth -or $cpuDetails.AddressWidth -ne $RequiredAddressWidth) {
Print-CheckResult "Processor" "FAIL" "AddressWidth=$($cpuDetails.AddressWidth), Required=$RequiredAddressWidth"
$processorCheckFailed = $true
}
if ($null -eq $cpuDetails.MaxClockSpeed -or $cpuDetails.MaxClockSpeed -lt $MinClockSpeedMHz) {
Print-CheckResult "Processor" "FAIL" "MaxClockSpeed=$($cpuDetails.MaxClockSpeed)MHz, Minimum Required=$MinClockSpeedMHz MHz"
$processorCheckFailed = $true
}
if ($null -eq $cpuDetails.NumberOfLogicalProcessors -or $cpuDetails.NumberOfLogicalProcessors -lt $MinLogicalCores) {
Print-CheckResult "Processor" "FAIL" "LogicalCores=$($cpuDetails.NumberOfLogicalProcessors), Minimum Required=$MinLogicalCores"
$processorCheckFailed = $true
}
if ($cpuDetails.AddressWidth -eq $RequiredAddressWidth `
-and $cpuDetails.MaxClockSpeed -ge $MinClockSpeedMHz `
-and $cpuDetails.NumberOfLogicalProcessors -ge $MinLogicalCores) {
Print-CheckResult "Processor" "PASS" "$($cpuDetails.Name)"
}
else {
$exitCode = 1
}
}
}
catch {
Print-CheckResult "Processor" "UNDETERMINED" "Exception: $($_.Exception.Message)"
$exitCode = 1
}
# Secure Boot
try {
$secureBootEnabled = Confirm-SecureBootUEFI
if ($secureBootEnabled -eq $true) {
Print-CheckResult "SecureBoot" "PASS" "Secure Boot is enabled"
}
else {
Print-CheckResult "SecureBoot" "FAIL" "Secure Boot is not enabled"
$exitCode = 1
}
}
catch [System.PlatformNotSupportedException] {
Print-CheckResult "SecureBoot" "FAIL" "Secure Boot is not supported or device is not using UEFI"
$exitCode = 1
}
catch [System.UnauthorizedAccessException] {
Print-CheckResult "SecureBoot" "UNDETERMINED" "Unable to determine Secure Boot state due to access restrictions"
$exitCode = 1
}
catch {
Print-CheckResult "SecureBoot" "UNDETERMINED" "Exception: $($_.Exception.Message)"
$exitCode = 1
}
# Final Result
if ($exitCode -eq 0) {
Write-Color "All checks passed. Device appears ready for Windows 11 25H2." "Green"
exit 0
}
else {
Write-Color "One or more checks failed. Device is not ready for Windows 11 25H2." "Red"
exit 1
} This script checks critical Windows 11 eligibility parameters, including available storage, system memory, TPM presence and version, processor architecture, and Secure Boot availability. It provides clear pass/fail feedback for each requirement, then consolidates the results into an overall readiness status, allowing you to see at a glance if a system can safely upgrade.
If any check fails, the script calls attention to that failure so you can investigate or remediate the issue. It also identifies indeterminate factors when certain details can't be retrieved, making troubleshooting straightforward.
Use cases
- Quickly evaluating a fleet of devices for Windows 11 upgrade readiness
- Identifying hardware deficits (e.g., insufficient disk space or missing TPM) prior to scheduled migrations
- Generating hardware compliance reports for clients or management
- Integrating with an RMM to automate upgrade pre-checks across multiple endpoints
Recommendations
- We’d recommend reviewing our Windows 11 Upgrade Automation, which makes use of this script as well.
- Test in a lab environment before running on production systems
- Configure a script-based monitor in Level to trigger this check on demand when you need to validate a specific endpoint
- Alternatively, create a scheduled automation in Level to run this script regularly and track hardware readiness over time
- Ensure devices are connected and powered to avoid incomplete checks
- Review the script output logs for detailed pass/fail statuses and potential remediation steps