Preparación para Windows 11
Usa los datos de Endpoint Analytics para decir qué equipos Windows pueden pasar a Windows 11 y qué requisito falla en el resto: TPM, Secure Boot, RAM, disco, procesador.
Ejemplo en México
Hospital privado en Veracruz, 500 personas
Dirección pide el presupuesto de equipos del próximo año y TI debe saber cuántas computadoras no pueden pasar a Windows 11.
.\get-windows11-readiness-report.ps1 -OnlyBlocked "true" -ExportToCsv "true"Un CSV de los equipos bloqueados y su causa. El resumen muestra que la mayoría falla por falta de TPM 2.0, lo que define cuántos reemplazar.
Cómo implementarlo
- Confirma que Endpoint Analytics está activo y que los equipos ya reportan datos (pueden tardar hasta 24 horas).
- Ejecuta el script; con -OnlyBlocked "true" ves solo los equipos que no pueden actualizarse.
- Exporta con -ExportToCsv "true" para tener el detalle por equipo.
- Agrupa por causa (por ejemplo RAM insuficiente) para armar el presupuesto de renovación y las olas de actualización.
Ver el código completo de get-windows11-readiness-report.ps1
<#
.TITLE
Get Windows 11 Readiness Report
.SYNOPSIS
Reports Windows 11 upgrade readiness for all Windows devices using Endpoint Analytics hardware signals.
.DESCRIPTION
This script reads the Endpoint Analytics work-from-anywhere device data to report
which Windows devices are eligible for Windows 11 and which hardware checks are
blocking the rest: TPM, Secure Boot, RAM, storage, processor family, speed, core
count, and 64-bit capability. It shows the tenant-level readiness summary plus a
per-device breakdown of failed checks, so upgrade waves and hardware refresh
budgets can be planned from real inventory data.
.TAGS
Devices,Reporting
.MINROLE
Intune Administrator
.PERMISSIONS
DeviceManagementManagedDevices.Read.All,DeviceManagementConfiguration.Read.All
.AUTHOR
Ugur Koc
.VERSION
1.2
.CHANGELOG
1.2 - Added Azure Automation contract validation, portal-safe boolean parameters, beta Graph endpoints, and terminating paging errors
1.1 - Azure Automation now records script progress, outcomes, and summaries in job history
1.0 - Initial release
.LASTUPDATE
2026-07-30
.EXAMPLE
.\get-windows11-readiness-report.ps1
Shows the tenant readiness summary and all devices with failed upgrade checks
.EXAMPLE
.\get-windows11-readiness-report.ps1 -ExportToCsv "true"
Exports the full per-device readiness data to a timestamped CSV file
.EXAMPLE
.\get-windows11-readiness-report.ps1 -OnlyBlocked "true"
Lists only devices that are not eligible for the Windows 11 upgrade
.NOTES
- Requires Microsoft.Graph.Authentication module
- Endpoint Analytics must be enabled and collecting data; without it the report is empty
- Devices need to report analytics data for up to 24 hours after enrollment before appearing
- Uses beta Graph endpoints because the work-from-anywhere analytics surface is not exposed on v1.0
- Local interactive sign-in uses the MgGraphCommunity module to avoid the Graph SDK's mandatory WAM broker on Windows
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false, HelpMessage = "Show only devices that are blocked from upgrading")]
[ValidateSet("true", "false", "1", "0", '$true', '$false')]
[string]$OnlyBlocked,
[Parameter(Mandatory = $false, HelpMessage = "Export results to CSV")]
[ValidateSet("true", "false", "1", "0", '$true', '$false')]
[string]$ExportToCsv,
[Parameter(Mandatory = $false, HelpMessage = "Output path for exports")]
[string]$OutputPath = ".",
[Parameter(Mandatory = $false, HelpMessage = "Force module installation without prompting")]
[ValidateSet("true", "false", "1", "0", '$true', '$false')]
[string]$ForceModuleInstall
)
# Normalize the local module-install override for Azure Automation parameter binding.
$forceModuleInstallRaw = [string]$ForceModuleInstall
Remove-Variable -Name ForceModuleInstall
if ([string]::IsNullOrWhiteSpace($forceModuleInstallRaw)) {
$ForceModuleInstall = $false
}
elseif ($forceModuleInstallRaw.Trim().ToLowerInvariant() -in @("true", "1", '$true')) {
$ForceModuleInstall = $true
}
elseif ($forceModuleInstallRaw.Trim().ToLowerInvariant() -in @("false", "0", '$false')) {
$ForceModuleInstall = $false
}
else {
throw "Parameter 'ForceModuleInstall' accepts only true, false, 1, 0, $true, or $false."
}
# Azure Automation supplies portal parameter values as strings. Normalize the
# public boolean parameters once so local and runbook execution use real booleans.
foreach ($runbookBooleanParameter in @('OnlyBlocked', 'ExportToCsv')) {
$runbookBooleanRaw = [string](Get-Variable -Name $runbookBooleanParameter -ValueOnly)
Remove-Variable -Name $runbookBooleanParameter
if ([string]::IsNullOrWhiteSpace($runbookBooleanRaw)) {
Set-Variable -Name $runbookBooleanParameter -Value $false
continue
}
switch ($runbookBooleanRaw.Trim().ToLowerInvariant()) {
{ $_ -in @("true", "1", '$true') } {
Set-Variable -Name $runbookBooleanParameter -Value $true
}
{ $_ -in @("false", "0", '$false') } {
Set-Variable -Name $runbookBooleanParameter -Value $false
}
default {
throw "Parameter '$runbookBooleanParameter' accepts only true, false, 1, 0, $true, or $false."
}
}
}
# ============================================================================
# ENVIRONMENT DETECTION AND SETUP
# ============================================================================
function Initialize-RequiredModule {
param(
[string[]]$ModuleNames,
[bool]$IsAutomationEnvironment,
[bool]$ForceInstall = $false
)
foreach ($ModuleName in $ModuleNames) {
Write-Verbose "Checking module: $ModuleName"
$module = Get-Module -ListAvailable -Name $ModuleName | Select-Object -First 1
if (-not $module) {
if ($IsAutomationEnvironment) {
throw "Module '$ModuleName' is not available in Azure Automation"
}
else {
Write-Information "Module '$ModuleName' not found. Installing..." -InformationAction Continue
if (-not $ForceInstall) {
$response = Read-Host "Install module '$ModuleName'? (Y/N)"
if ($response -notmatch '^[Yy]') {
throw "Module '$ModuleName' is required but installation was declined."
}
}
try {
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
$scope = if ($isAdmin) { "AllUsers" } else { "CurrentUser" }
Install-Module -Name $ModuleName -Scope $scope -Force -AllowClobber -Repository PSGallery
Write-Information "✓ Successfully installed '$ModuleName'" -InformationAction Continue
}
catch {
throw "Failed to install module '$ModuleName': $($_.Exception.Message)"
}
}
}
Import-Module -Name $ModuleName -Force -ErrorAction Stop
}
}
# Detect execution environment
$IsAzureAutomation = $null -ne $PSPrivateMetadata.JobId.Guid
# Initialize required modules
$RequiredModules = @("Microsoft.Graph.Authentication")
# MgGraphCommunity gives WAM-free interactive sign-in for local runs
if (-not $IsAzureAutomation) {
$RequiredModules += "MgGraphCommunity"
}
try {
Initialize-RequiredModule -ModuleNames $RequiredModules -IsAutomationEnvironment $IsAzureAutomation -ForceInstall $ForceModuleInstall
Write-Verbose "✓ All required modules are available"
}
catch {
Write-Error "Module initialization failed: $_"
exit 1
}
# ============================================================================
# AUTHENTICATION
# ============================================================================
try {
if ($IsAzureAutomation) {
Write-Output "Connecting to Microsoft Graph using Managed Identity..."
Connect-MgGraph -Identity -NoWelcome -ErrorAction Stop
}
else {
Write-Output "Connecting to Microsoft Graph..."
$Scopes = @(
"DeviceManagementManagedDevices.Read.All",
"DeviceManagementConfiguration.Read.All"
)
Connect-MgGraphCommunity -Scopes $Scopes -NoWelcome -ErrorAction Stop
}
Write-Output "✓ Successfully connected to Microsoft Graph"
}
catch {
Write-Error "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
exit 1
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
function Get-MgGraphAllPage {
param(
[string]$Uri,
[int]$DelayMs = 100
)
$allResults = @()
$nextLink = $Uri
do {
try {
if ($allResults.Count -gt 0) {
Start-Sleep -Milliseconds $DelayMs
}
$response = Invoke-MgGraphRequest -Uri $nextLink -Method GET
if ($null -ne $response.value) {
$allResults += $response.value
}
else {
$allResults += $response
}
$nextLink = $response.'@odata.nextLink'
}
catch {
if ($_.Exception.Message -like "*429*") {
Write-Information "Rate limit hit, waiting 60 seconds..." -InformationAction Continue
Start-Sleep -Seconds 60
continue
}
throw "Error fetching data: $($_.Exception.Message)"
}
} while ($nextLink)
return $allResults
}
function Get-FailedCheck {
param([object]$Device)
# Each check property is TRUE when the check FAILED
$checkMap = [ordered]@{
osCheckFailed = "OS version"
processor64BitCheckFailed = "64-bit processor"
processorFamilyCheckFailed = "Processor family"
processorCoreCountCheckFailed = "Processor core count"
processorSpeedCheckFailed = "Processor speed"
ramCheckFailed = "RAM"
secureBootCheckFailed = "Secure Boot"
storageCheckFailed = "Storage"
tpmCheckFailed = "TPM 2.0"
}
$failed = foreach ($check in $checkMap.Keys) {
if ($Device.$check -eq $true) { $checkMap[$check] }
}
return @($failed)
}
# ============================================================================
# MAIN SCRIPT LOGIC
# ============================================================================
try {
# Tenant-level readiness summary
Write-Output "Retrieving tenant hardware readiness summary..."
$readinessSummary = $null
try {
$readinessSummary = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/userExperienceAnalyticsWorkFromAnywhereHardwareReadinessMetric" -Method GET
}
catch {
Write-Warning "Could not read the hardware readiness summary: $($_.Exception.Message)"
}
# Per-device work-from-anywhere data
Write-Output "Retrieving per-device readiness data..."
$selectFields = "id,deviceName,serialNumber,manufacturer,model,ownership,managedBy,osDescription,osVersion,upgradeEligibility,osCheckFailed,processor64BitCheckFailed,processorFamilyCheckFailed,processorCoreCountCheckFailed,processorSpeedCheckFailed,ramCheckFailed,secureBootCheckFailed,storageCheckFailed,tpmCheckFailed"
$devices = Get-MgGraphAllPage -Uri "https://graph.microsoft.com/beta/deviceManagement/userExperienceAnalyticsWorkFromAnywhereMetrics/allDevices/metricDevices?`$select=$selectFields"
if (@($devices).Count -eq 0) {
Write-Warning "No work-from-anywhere analytics data found. Endpoint Analytics may not be enabled, or devices have not reported yet."
return
}
Write-Output "✓ Found analytics data for $(@($devices).Count) devices"
[System.Collections.Generic.List[Object]]$report = @()
foreach ($device in $devices) {
$failedChecks = Get-FailedCheck -Device $device
$report.Add([PSCustomObject]@{
DeviceName = $device.deviceName
SerialNumber = $device.serialNumber
Manufacturer = $device.manufacturer
Model = $device.model
Ownership = $device.ownership
OsDescription = $device.osDescription
OsVersion = $device.osVersion
UpgradeEligibility = $device.upgradeEligibility
FailedChecks = ($failedChecks -join "; ")
FailedCheckCount = $failedChecks.Count
})
}
if ($OnlyBlocked) {
$report = [System.Collections.Generic.List[Object]]@($report | Where-Object { $_.UpgradeEligibility -notin @("capable", "upgraded") })
}
# ----- Display results -----
Write-Output "`nWINDOWS 11 READINESS REPORT"
Write-Output ("=" * 50)
Write-Output "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Output ("=" * 50)
if ($readinessSummary) {
Write-Output "`nTenant summary (Endpoint Analytics):"
Write-Output " Total devices: $($readinessSummary.totalDeviceCount)"
Write-Output " Upgrade eligible: $($readinessSummary.upgradeEligibleDeviceCount)"
}
foreach ($eligibilityGroup in ($report | Group-Object -Property UpgradeEligibility | Sort-Object Name)) {
$groupLabel = if ($eligibilityGroup.Name) { $eligibilityGroup.Name } else { "unknown" }
Write-Output "`n[$groupLabel] $($eligibilityGroup.Count) device(s)"
foreach ($row in ($eligibilityGroup.Group | Sort-Object FailedCheckCount -Descending)) {
$line = " $($row.DeviceName) | $($row.Manufacturer) $($row.Model) | $($row.OsVersion)"
if ($row.FailedChecks) {
$line += " | blocked by: $($row.FailedChecks)"
}
Write-Output $line
}
}
# Summary of the most common blockers
$blockedDevices = @($report | Where-Object { $_.FailedCheckCount -gt 0 })
if ($blockedDevices.Count -gt 0) {
Write-Output "`nMost common blocking checks:"
$allFailures = $blockedDevices | ForEach-Object { $_.FailedChecks -split "; " }
foreach ($failureGroup in ($allFailures | Group-Object | Sort-Object Count -Descending)) {
Write-Output " $($failureGroup.Name): $($failureGroup.Count) devices"
}
}
Write-Output "`n"
Write-Output ("=" * 50)
Write-Output "Summary: $($report.Count) devices reported, $($blockedDevices.Count) with failed hardware checks"
Write-Output ("=" * 50)
# Export to CSV if requested
if ($ExportToCsv) {
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$csvPath = Join-Path $OutputPath "Windows11_Readiness_$timestamp.csv"
$report | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Output "✓ CSV report saved: $csvPath"
}
}
catch {
Write-Error "Script execution failed: $($_.Exception.Message)"
exit 1
}
finally {
try {
$null = Disconnect-MgGraph
Write-Output "✓ Disconnected from Microsoft Graph"
}
catch {
Write-Verbose "Graph disconnection completed"
}
}
# Copia sin cambios de https://github.com/ugurkocde/IntuneAutomation (commit 797bb2e).
# MIT License
#
# Copyright (c) 2025 Ugur Koc | Microsoft MVP
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.