Get-AutopatchHealth.ps1
|
<#PSScriptInfo
.VERSION 2.1.2 .GUID 2c80336a-7d9b-41c0-8eb3-a80abef2dbb8 .AUTHOR Jeff Gilbert .COMPANYNAME .COPYRIGHT .TAGS Intune .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES v1.0.0 - 3.10.26 - Original published version v1.1.0 - 3.10.26 - Fixed typo in report output v2.0.0 - 3.23.26 - Added more checks and function documentation v2.0.1 - 3.25.26 - Removed deprecated network endpoint from connectivity checks v2.1.0 - 3.26.26 - Updated Get-Help documentation v2.1.1 - 4.1.26 - Incorporated tester feedback and updated Get-Help documentation v2.1.2 - 4.1.26 - Minor bug fixes and updated Get-Help documentation #> <# .SYNOPSIS Validates Windows Autopatch and Feature Update readiness by performing comprehensive device configuration, policy, service, registry, network, and scheduled task health checks. .DESCRIPTION This script performs a read only, end to end Windows Autopatch health assessment for Windows devices. It validates OS servicing branch, telemetry level, Intune enrollment and IME activity, co‑management workloads, Windows Update policy authority, WSUS or registry blockers, required services, network endpoints, and scheduled tasks. It runs safely in SYSTEM or user context and outputs console results, a readiness summary, and an exit code suitable for Intune detection/remediation. GENERAL CONFIGURATION HEALTH CHECKS Operating System Release Branch: Ensures the device is on a supported GA/production channel (not Windows Insiders/Preview). Registry Settings: Checks for Autopatch‑blocking or WSUS‑redirecting values: - HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate -- DoNotConnectToWindowsUpdateInternetLocations, DisableWindowsUpdateAccess, WUServer, WUStatusServer - HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU -- NoAutoUpdate - Update source override keys: -- SetPolicyDrivenUpdateSourceForDriverUpdates, FeatureUpdates, OtherUpdates, QualityUpdates If set to '1' and a WUSserver is configured, the device tries to use WSUS instead of Autopatch. Telemetry: Reads the AllowTelemetry policy value from the local registry to confirm minimum level = **1 (Required)**. Intune Enrollment & IME Activity: Validates Intune enrollment indicators and IME log activity within the last 28 days. For comanaged devices, checks required workload ownership: - Windows Update policies - Device configuration - Office Click to Run apps Update Policy Authority: Reads the PolicySources value to ensure Intune/Autopatch is configured: HKLM\SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\PolicyState\PolicySources Values: 1=GPO, 2=SCCM, 4=Intune/Autopatch, 5=GPO+MDM (MDM wins), 6=SCCM+MDM (MDM wins). Also attempts to identify the Autopatch ring a device is assigned to (test, ring1, ring2, etc.) AUTOPATCH SERVICE HEALTH CHECKS Validates required Windows Update–related services: - BITS (downloads update payloads) - CryptSvc (signature validation) - DiagTrack (telemetry pipeline) - DoSvc (Delivery Optimization) - UsoSvc (update orchestration) - WaaSMedicSvc (repairs Windows Update stack) - wuauserv (core Windows Update client) NETWORK ENDPOINT CONNECTIVITY CHECKS Confirms reachability of required Autopatch and Microsoft endpoints: - mmdcustomer.microsoft.com (Microsoft Managed Desktop (MMD) / Windows Autopatch service endpoint) - mmdls.microsoft.com (part of the Autopatch logging and service communication layer) - login.windows.net (endpoint used to issue and refresh authentication tokens) - device.autopatch.microsoft.com (Service endpoint that must be reachable from Autopatch devices) - services.autopatch.microsoft.com (Service API endpoint that must be reachable for Autopatch functionality) - payloadprod*.blob.core.windows.net (Azure Blob Storage–backed endpoint used by Autopatch) - Global Device Listener: devicelistenerprod.microsoft.com (global Autopatch device listener communication endpoint required for Autopatch‑managed devices) - EU Device Listener: devicelistenprod.eudb.microsoft.com (EU tenants) (EU endpoint used instead of the global listener within the EU Data Boundary) WINDOWS UPDATE SCHEDULED TASK CHECKS Validates required scheduled tasks: - \Microsoft\Windows\WindowsUpdate\Scheduled Start (triggers scans/downloads) - \Microsoft\Windows\UpdateOrchestrator\Report policies (evaluates effective policy) .PARAMETER Remediation (Optional) Use this parameter to test the script's behavior as an Intune remediation: .\Get-AutopatchHealth.ps1 -Remediation There is also a line in the script's READ ME section to hard-code it to run as a remediation. Uncomment the line below (delete the #) in the READ ME section of the script: # $Remediation = $true This will change the format of the pass/fail images from ✅/❌ to $($script:Symbols.Fail)$($script:Symbols.Pass)/$($script:Symbols.Fail) so they will display properly in the remediation pre-remediation detection output. .PARAMETER EU (Optional) Use this if running the script against an EU tenant to check network endpoint connectivity: .\Get-AutopatchHealth.ps1 -EU You can also modify the script to include EU data network boundaries in network tests. Uncomment the line below in the READ ME section of the script (delete the #): # $EU = $true .EXAMPLE Test running the script as an Intune remediation. If actually using it as a remediation script instead of just testing, you need to un-comment the $Remediation line in READ ME. Modify the script invocation to: PS C:\> .\Get-AutopatchHealth.ps1 -Remediation .EXAMPLE Run network checks including EU Data Boundary endpoints: Modify the script invocation to: PS C:\> .\Get-AutopatchHealth.ps1 -EU .EXAMPLE Test running the script as an Intune remediation and run network checks including EU Data Boundary endpoints: Modify the script invocation to: PS C:\> .\Get-AutopatchHealth.ps1 -Remediation -EU .NOTES This script aligns with Microsoft guidance for: - Windows Autopatch onboarding - Windows Update for Business (WUfB) - Delivery Optimization network requirements #> param ( [switch]$Remediation, # Test running the script as a remediation: Get-AutopatchHealth.ps1 -Remediation [switch]$EU # Use to test EU Autopatch endpoint: Get-AutopatchHealth.ps1 -EU ) #-------------------------------------------------------- Functions --------------------------------------------------------- Function Use-NoUniCode { <# Use Unicdode symbols for console output running the script directly for the best visual experience. Do not use Unicode symbols for Intune remediations because the unicode symbols will not render as expected in the admin center. Use ASCII markers instead. Unicode gets stripped or mangled by the Intune reporting pipeline before the Admin Center renders it. Scripts are required to be UTF‑8 encoded and Intune uses a restricted text renderer that is effectively ASCII‑safe, not Unicode‑safe. Intune captures only standard output and that output is truncated to ~2048 characters. This is by design because remediation output is designed for machine status, CSV export, so Intune intentionally keeps this path plain‑text safe. #> if ($Remediation) { # NoUnicode $script:Symbols = @{ Pass = "[PASS]" Fail = "[FAIL]" Warn = "[WARN]" Info = "[INFO]" } } else { $script:Symbols = @{ Pass = "✅" Fail = "❌" Warn = "⚠️" Info = "ℹ️" } } } Function RU-Admin{ <# Determines if the current Windows identity is a member of the local Administrators group. If the session is not elevated, the function displays a warning indicating that running Autopatch health checks without administrative permissions can result in false‑negative reporting due to restricted access to system resources. #> $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) If (!($IsAdmin)){ Write-Host "-------------------------------------------------" -ForegroundColor Red Write-Host "$($script:Symbols.Fail) When running Autopatch health checks with limited permissions (non-admin)`nYou WILL encounter false negative reporting results." -ForegroundColor Red Write-Host "-------------------------------------------------" -ForegroundColor Red } } Function Test-Branch { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [String]$Branch ) $policyStatePath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\PolicyState' if (-not (Test-Path $policyStatePath)) { Write-Host "PolicyState registry path not found: $policyStatePath" -ForegroundColor Yellow return } $policyState = Get-ItemProperty -Path $policyStatePath -ErrorAction SilentlyContinue # ----------------------------- # BranchReadinessLevel # ----------------------------- $brl = [string]$policyState.BranchReadinessLevel $brlLabel = switch ($brl) { 'CB' { 'Current Branch' } 'B' { 'Business (SAC)' } 'RP' { 'Release Preview' } 'WIF' { 'Windows Insider Fast' } 'WIS' { 'Windows Insider Slow' } 'WIP' { 'Windows Insider Preview' } default { $brl } # fall back to raw value } If ($brl -eq $branch ){ Write-Host (" Operating System branch is {0} ({1})" -f $brl, $brlLabel) ; return $true } else { Write-Host (" $($script:Symbols.Fail) BranchReadinessLevel is NOT supported: {0} ({1})" -f $brl, $brlLabel) -ForegroundColor Red ; return $false } } Function Check-Registry { # ----------------------------- # AutopatchRegistryBlockerCheck # ----------------------------- $blockingKey = 0 # --- Paths (use Registry:: to avoid 32-bit WOW64 redirection issues) --- $regChecks = @( 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\DoNotConnectToWindowsUpdateInternetLocations', 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\DisableWindowsUpdateAccess', 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU\NoAutoUpdate' ) foreach ($path in $regChecks) { if (Test-Path $path) { Write-Host " Autopatch device registration blocking registry key value found: $path" -ForegroundColor Red ; $blockingKey++ } } if ($blockingKey -eq 0){ Write-Host " No Autopatch device registration blockers found" } # ----------------------------- # WSUS Server Keys Checks # ----------------------------- $SUPreg = 0 # --- Paths (use Registry:: to avoid 32-bit WOW64 redirection issues) --- $WSrvKeys = @{ Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; ValueName = "WUServer"; WSrvValue = $null }, @{ Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; ValueName = "WUStatusServer"; WSrvValue = $null } foreach ($Key in $WSrvKeys) { $reg = Get-ItemProperty -Path $Key.Path -ErrorAction SilentlyContinue if ([string]::IsNullOrWhiteSpace($reg.$($Key.ValueName))) { Write-Host " $($Key.ValueName) key value not found or blank " } else { Write-Host " $($Key.ValueName) value is configured" -ForegroundColor Red ; $SUPreg++ } # WSUS config registry value found } # The update source key values only apply if the WUServer and/or WUStatusServer values are present [1 means WSUS, 0 means Autopatch] $UpdateSourceKeys = @( @{ Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; ValueName = "SetPolicyDrivenUpdateSourceForDriverUpdates"; WSUSValue = "1" }, @{ Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; ValueName = "SetPolicyDrivenUpdateSourceForFeatureUpdates"; WSUSValue = "1" }, @{ Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; ValueName = "SetPolicyDrivenUpdateSourceForOtherUpdates"; WSUSValue = "1" }, @{ Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"; ValueName = "SetPolicyDrivenUpdateSourceForQualityUpdates"; WSUSValue = "1" } ) ForEach ($Key in $UpdateSourceKeys){ $value = Get-ItemProperty -Path $Key.Path -Name $Key.ValueName -ErrorAction SilentlyContinue if ($null -ne $value){ if ([string]$value.$($Key.ValueName) -eq $Key.WSUSValue){ Write-Host " $($Key.ValueName) is set to 1 (WSUS)" -ForegroundColor Yellow ; $SUPreg++ } # WSUS config registry value found } else { Write-Host " No WSUS update source policy registry values found" } # Summary If ( $SUPreg -eq 0 -and $blockingKey -eq 0 ){ return $true } else { return $false} } } Function Test-Telemetry { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateRange(0, 3)] [int]$minRequired ) $regPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' $valueName = 'AllowTelemetry' try { $val = (Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction Stop).$valueName $val = [int]$val $map = @{ 0 = 'Security (Optional)'; 1 = 'Basic'; 2 = 'Enhanced'; 3 = 'Required' } if ($val -ge $minRequired) { Write-Host " Telemetry is set to $($map[$val]) [$($val)]" ; return $true } else { return $false } } catch { Write-Host " $($script:Symbols.Fail) Policy value not found ($regPath\$valueName)." ; return $false } } Function Intune-Checks{ # ----------------------------- # MDMEnrollmentCheck # ----------------------------- $missingKey = 0 $paths = @( 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\IntuneManagementExtension', 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PolicyManager\current\device' ) foreach ($p in $paths) { if (!(Test-Path $p)){ Write-Host " Enrollment key $p is NOT present" -ForegroundColor Red ; $missingKey++} } If ($missingKey -eq 0){ Write-Host " Intune enrollment indicators are present" } # ----------------------------- # IMEActivityCheck # ----------------------------- $stale = 0 $latestImeLog = Get-ChildItem 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs' ` -Filter '*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($latestImeLog) { $lastActivity = $latestImeLog.LastWriteTime $cutoffDate = (Get-Date).AddDays(-28) if ($lastActivity -ge $cutoffDate) { Write-Host " Last Intune activity (IME): $lastActivity" } else { Write-Host " STALE (device activity is older than 28 days)" -ForegroundColor Red ; $stale++ } } else { Write-Host " No Intune Management Extension logs found." -ForegroundColor Red } # ----------------------------- # ComanagementWorkloadCheck # ----------------------------- # Get ConfigMgr co-management state $ccm = Get-CimInstance -Namespace 'root\ccm\InvAgt' -ClassName 'CCM_System' -ErrorAction SilentlyContinue if ($ccm.CoManaged) { Write-Host " Co-management indicators are present, checking required workload ownership" # Autopatch-required workloads $RequiredWorkloads = @{ WindowsUpdatePolicies = 16 # Windows Update policies workload OfficeClickToRunApps = 128 # Office Click-to-Run apps workload DeviceConfiguration = 8 # Device configuration workload } $Missing = @() foreach ($workload in $RequiredWorkloads.GetEnumerator()) { if (-not ($ccm.ComgmtWorkloads -band $workload.Value)) { $Missing += $workload.Key } } if ($Missing.Count -eq 0) { Write-Host " Autopatch-required co-management workloads are owned by Intune " } else { Write-Host " The following workloads are NOT managed by Intune:" -ForegroundColor Red $Missing | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } } } If ($missingKey -eq 0 -and $stale -eq 0 -and $Missing.Count -eq 0 ){ return $true } else { return $false } } Function Test-UpdateEngine{ [CmdletBinding()] param( [Parameter(Mandatory = $true)] [Int]$PolicySource ) $policySources = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\PolicyState' -ErrorAction Stop).PolicySources $srcLabel = switch ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\PolicyState').PolicySources) { 1 { 'GPO (Legacy WSUS)' } 2 { 'Configuration Manager' } 4 { 'Intune/Autopatch' } 5 { 'GPO + MDM (MDM wins)' } 6 { 'SCCM + MDM (MDM wins)' } default { 'Unknown / Not set' } } Write-Host " The active update engine is $($srcLabel)" # Determine if a device is in an Autopatch Group ring $ringErr = 0 $regPath = 'HKLM:\SOFTWARE\Microsoft\WindowsAutopatch\ClientBroker' $valueName = 'Ring' $ringValue = (Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction SilentlyContinue).$valueName if ([string]::IsNullOrWhiteSpace($ringValue)) { Write-Host " This device is not assigned an Autopatch ring" -ForegroundColor Red ; $ringErr++ } else { Write-Host " Device is in an Autopatch ring: $($ringValue)" } if (($policySources -band 4) -and ($ringErr -eq 0)) { $true } else { $false } } function Get-AutopatchServiceStatus { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]] $ServiceNames ) $svcIssue = 0 $results = foreach ($service in $ServiceNames) { $svc = Get-Service -Name $service -ErrorAction SilentlyContinue $cim = Get-CimInstance -ClassName Win32_Service -Filter "Name='$service'" -ErrorAction SilentlyContinue $issueReason = $null if (-not $cim){ $issueReason = 'Service not installed' ; $svcIssue++ } elseif ($cim.StartMode -eq 'Disabled'){ $issueReason = 'Service is disabled' ; $svcIssue++ } elseif ( $svc -and $svc.Status -ne 'Running' -and $cim.Name -in @('UsoSvc','DiagTrack','CryptSvc') ) { $issueReason = 'Service not running' ; $svcIssue++ } [pscustomobject]@{ ServiceName = $service DisplayName = if ($cim) { $cim.DisplayName } else { '<Not Installed>' } Installed = [bool]$cim StartupType = if ($cim) { $cim.StartMode } else { $null } Status = if ($svc) { $svc.Status } else { $null } Issue = $issueReason } } # Output the table first (useful for logs) $results | Sort-Object ServiceName # Then fail if anything was wrong if ($svcIssue -eq 0){ Write-Host "$($script:Symbols.Pass) All required Windows Update–related services are healthy" -ForeGroundColor Green ; $script:summary+= "$($script:Symbols.Pass) All required services are healthy" } else { Write-Host "$($script:Symbols.Fail) $svcIssue Windows Update-related service(s) are missing, disabled, or not running" -ForeGroundColor Red ; $script:exitCode++ ; $script:summary= "$($script:Symbols.Fail) Required services(s) issue found" } } function Test-AutopatchNetworkConnection { [CmdletBinding()] param ( [switch]$IncludeEUDataBoundary, [int[]]$Ports = @(80,443), [int]$TimeoutSeconds = 5 ) $endpoints = @( @{ Name = "MMD Customer"; Host = "mmdcustomer.microsoft.com" } @{ Name = "MDM Download Service"; Host = "mmdls.microsoft.com" } @{ Name = "Azure AD Login"; Host = "login.windows.net" } @{ Name = "Autopatch Device Listener"; Host = "device.autopatch.microsoft.com" } @{ Name = "Autopatch Services"; Host = "services.autopatch.microsoft.com" } # Wildcard representatives @{ Name = "Autopatch Payload Storage"; Host = "payloadprod1.blob.core.windows.net"; Wildcard = "payloadprod*.blob.core.windows.net" } # Deprecated # @{ Name = "Azure Web PubSub"; Host = "autopatch.webpubsub.azure.com"; Wildcard = "*.webpubsub.azure.com" } @{ Name = "Windows Autopatch Device Listener"; Host = "devicelistenerprod.microsoft.com" } ) if ($IncludeEUDataBoundary) { $endpoints += @{ Name = "EU Data Boundary Device Listener" Host = "devicelistenprod.eudb.microsoft.com" } $endpoints = $endpoints | Where-Object { $_.Host -ne "devicelistenerprod.microsoft.com" } } $results = foreach ($ep in $endpoints) { $dnsResolved = $false $portResults = @{} $errorText = $null try { Resolve-DnsName -Name $ep.Host -ErrorAction Stop | Out-Null $dnsResolved = $true foreach ($port in $Ports) { $tcp = Test-NetConnection -ComputerName $ep.Host -Port $port -WarningAction SilentlyContinue $portResults["Port$port"] = $tcp.TcpTestSucceeded } } catch { $errorText = $_.Exception.Message foreach ($port in $Ports) { $portResults["Port$port"] = $false } } [PSCustomObject]@{ Name = $ep.Name Target = $ep.Host Wildcard = $ep.Wildcard DnsResolved = $dnsResolved Tcp80Success = $portResults["Port80"] Tcp443Success = $portResults["Port443"] Reachable = ($dnsResolved -and ($portResults.Values -contains $true)) Error = $errorText } } return $results } function Get-WindowsUpdateTaskStatus { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]] $TaskPaths ) $taskIssue = 0 $results = foreach ($taskPath in $TaskPaths) { # Split full task path into folder + task name $taskName = Split-Path $taskPath -Leaf $taskFolder = Split-Path $taskPath -Parent $task = Get-ScheduledTask ` -TaskName $taskName ` -TaskPath ($taskFolder + '\') ` -ErrorAction SilentlyContinue $issueReason = $null if (-not $task) { $issueReason = 'Task not found' ; $taskIssue++ } elseif ($task.State -eq 'Disabled') { $issueReason = 'Task is disabled' ; $taskIssue++ } [pscustomobject]@{ TaskPath = $taskPath Exists = [bool]$task State = if ($task) { $task.State } else { $null } Issue = $issueReason } } if ($taskIssue -eq 0){ Write-Host "$($script:Symbols.Pass) All required Windows Update scheduled task(s) are healthy" -ForeGroundColor Green ; $script:summary+= "$($script:Symbols.Pass) All required Windows Update tasks are healthy" } else { Write-Host "$($script:Symbols.Fail) $taskIssue required Windows Update scheduled task(s) are missing or disabled" -ForeGroundColor Red ; $script:exitCode++ ; $script:summary+= "$($script:Symbols.Fail) Issue found with required Windows Update tasks" } # Output results for logging $results } #----------------------------------------------------------- Prep ---------------------------------------------------------- # If we are running as a 32-bit process on an x64 system, re-launch as a 64-bit process first if ("$env:PROCESSOR_ARCHITEW6432" -ne "ARM64") { if (Test-Path "$($env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe") { & "$($env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy bypass -NoProfile -File "$PSCommandPath" Exit $lastexitcode } } #**************************************************************************************************************************** # *** READ ME *** #**************************************************************************************************************************** # By default, these are $false unless you specify parameters on the command line or un-comment the lines below to force $true # Run as Remediation or EU Network Data Boundary Switches #**************************************************************************************************************************** # Is this script supposed to run as an Intune remediation? If so, un-comment the line (delete #) to set $Remediation to $true # This will force the script to use NoUniCode symbols and provide Intune Admin center friendly detecion output. #$Remediation = $true # <------ UNCOMMENT THIS LINE TO FORCE THE SCRIPT TO RUN IN REMEDIATION MODE # EU Network Data Endpoint Checks #**************************************************************************************************************************** # Are you running network tests on an EU tenant? If so, un-comment the line to set $EU to $true #$EU = $true # <------ UNCOMMENT THIS LINE TO PERFORM EU TENANT NETWORK ENDPOINT TESTING #**************************************************************************************************************************** # *** READ ME *** #**************************************************************************************************************************** #------------------------------------------------------- Begin Script ------------------------------------------------------- Clear-Host Write-Host "=================================================" -ForegroundColor Cyan Write-Host "| Autopatch Device Configuration Validation |" -ForegroundColor Cyan Write-Host "=================================================" -ForegroundColor Cyan Write-Host " Get-Help Get-AutopatchHealh `n" -Foregroundcolor Green $script:exitCode = 0 # Variable holding the number of failures detected $script:summary = @( ) # Arrary holding test results for summary $script:detectionOutput = @( ) # Arrary holding pre-remediation detection output to display in Intune admin center when this is run as a remediation # Decide which pass/fail markers to use in case we're running as a remediation Use-NoUniCode # If this is running as a remediation, use NoUniCode characters # ---------------------------- # R U Admin? # ---------------------------- RU-Admin # Some checks will fail if this script is not run as admin Write-Host "`nGeneral Autopatch Configuration Checks" -ForegroundColor Cyan Write-Host "================================================" # ---------------------------- # Operating System Release Branch # ---------------------------- Write-Host "Checking Operating System release channel" -ForegroundColor Yellow $releaseBranch = Test-Branch -Branch "CB" If ($releaseBranch -eq $true ){ Write-host "$($script:Symbols.Pass) OS Release channel is supported" -ForegroundColor Green ; $script:summary += " $($script:Symbols.Pass) Operating System is supported" } else { Write-Host "$($script:Symbols.Fail) Operating System release branch is not supported for Autopatch" -ForegroundColor Red ; $script:exitCode++ ;$script:summary += " $($script:Symbols.Fail) Operating System is not supported" } # ---------------------------- # Registry checks # ---------------------------- <# Registry values found at HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate only apply when Windows Update is managed by WSUS / GPO / ConfigMgr. When updates are managed by Intune, WUfB, or Windows Autopatch, Windows instead honors policies under HKLM\SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\PolicyState, and legacy WSUS keys are ignored. #> Write-Host "Checking registry settings " -ForegroundColor Yellow If (Check-Registry -eq $true){ Write-Host "$($script:Symbols.Pass) No blocking or misconfigured WSUS registry values found" -ForegroundColor Green ; $script:summary+= "$($script:Symbols.Pass) No registry issues found" } else { Write-Host "$($script:Symbols.Fail) blocking or misconfigured registry values found" -ForegroundColor Red ; $script:exitCode++ ; $script:summary+= "$($script:Symbols.Fail) Registry checks failed" } # ---------------------------- # Telemetry # ---------------------------- <# Devices registered to Windows Autopatch must be configured to send at least “Required” Windows diagnostic data for Autopatch reports to accurately include device state and update status. #> try { write-host "Checking Telemetry Settings" -ForegroundColor Yellow $telemetryResult = Test-Telemetry -minRequired 1 # Security(0),Required/Basic(1),Enhanced(2),Optional/Full(3) if ( $telemetryResult) { Write-Host "$($script:Symbols.Pass) Telemetry is >= Required/Basic [1] "-ForegroundColor Green ; $script:summary+= "$($script:Symbols.Pass) Telemetry setting at or above miniumum required level" } else{ Write-Host "$($script:Symbols.Fail) Below Required. Minimum is $minRequired" -ForegroundColor Red ; $script:exitCode++ ; $script:summary+= "$($script:Symbols.Fail) Telemetry setting below miniumum required level" } } catch { # Do this if a terminating exception happens Write-Error "An error occurred: $_" } # ---------------------------- # Intune checks # ---------------------------- # Checks device is enrolled and has been active within the past 30 days Write-Host "Checking Intune enrollment and activity" -ForegroundColor Yellow If (intune-checks -eq $true) {Write-Host "$($script:Symbols.Pass) Device is enrolled correctly and active in Intune" -ForegroundColor Green ; $script:summary+= "$($script:Symbols.Pass) Device is enrolled correctly and active in Intune" } else { Write-Host "$($script:Symbols.Fail) Device is missing Intune enrollment or required co-management workloads" -ForegroundColor Red ; $script:exitCode++ ; $script:summary+= "$($script:Symbols.Fail) Device is missing Intune enrollment or required co-management workloads" } # ---------------------------- # Authoritative/active update engine (WSUS vs Microsoft Update) # ---------------------------- <# The authoritative way to determine which Windows Update engine is active is the PolicySources value at HKLM\SOFTWARE\Microsoft\WindowsUpdate\UpdatePolicy\PolicyState. If the value includes 4, the device is controlled by MDM (Intune / Autopatch), and legacy WSUS registry keys do not apply. #> Write-Host "Checking Update policy authority (single source of truth)" -ForegroundColor Yellow $updateEngine = Test-UpdateEngine -PolicySource 4 If ($updateEngine -eq $true ){ Write-host "$($script:Symbols.Pass) Device is configured for Intune/Autopatch updates" -ForegroundColor Green ; $script:summary+= "$($script:Symbols.Pass) Device is configured for Intune/Autopatch updates" } else { Write-Host "$($script:Symbols.Fail) Device is not configured for Intune/Autopatch updates" -ForegroundColor Red ; $script:exitCode++ ; $script:summary+= "$($script:Symbols.Fail) Device is not fully configured for Autopatch" } # ---------------------------- # Windows Update Services Checks # ---------------------------- Write-Host "`nWindows Update service status checks" -ForegroundColor Cyan Write-Host "================================================" Write-Host "Checking Autopatch (Windows Update) related services" -ForegroundColor Yellow $myServicesList = @( "WaaSMedicSvc", "wuauserv", "UsoSvc", "BITS", "DiagTrack", "DoSvc", "CryptSvc" ) try { $result = Get-AutopatchServiceStatus -ServiceNames $myServicesList $result | Format-Table -AutoSize } catch { Write-Error "An error occurred: $_" } # ---------------------------- # Autopatch Network Connectivity Checks # ---------------------------- try { Write-Host "`nAutopatch Network Connectivity Checks" -ForegroundColor Cyan Write-Host "================================================" Write-Host "Checking Autopatch & DO Network Connectivity Checks" -ForegroundColor Yellow # Use this line for non-EU tenants: If ( $EU ){ $results = Test-AutopatchNetworkConnection -IncludeEUDataBoundary } else { $results = Test-AutopatchNetworkConnection } if ($results.Reachable -contains $false) { Write-Host "$($script:Symbols.Fail) Some Autopatch network connectivity checks were unsuccessful" -ForegroundColor Red ; $script:exitCode++ ; $script:summary+= "$($script:Symbols.Fail) Autopatch network connectivity checks unsuccessful" } else{ Write-Host "$($script:Symbols.Pass) Autopatch network connectivity checks successful" -ForegroundColor Green ; $script:summary+= "$($script:Symbols.Pass) Autopatch network connectivity checks successful" } $results | Select Name, Target, Reachable | Format-Table -AutoSize } catch { Write-Error "An error occurred: $_" } # ---------------------------- # Windows Update Task Checks # ---------------------------- Write-Host "`nWindows Update scheduled task checks" -ForegroundColor Cyan Write-Host "================================================" Write-Host "Checking Windows Update related scheduled tasks" -ForegroundColor Yellow $windowsUpdateTasks = @( '\Microsoft\Windows\WindowsUpdate\Scheduled Start', '\Microsoft\Windows\UpdateOrchestrator\Report policies' ) try { $taskResults = Get-WindowsUpdateTaskStatus -TaskPaths $windowsUpdateTasks $taskResults | Format-Table } catch { Write-Error "An error occurred: $_" } # ---------------------------- # Summary # ---------------------------- Write-Host "==================================================" -ForegroundColor Cyan Write-Host "| Autopatch Device Health Summary |" -ForegroundColor Cyan Write-Host "==================================================" -ForegroundColor Cyan Write-Output ($script:summary -join " `n ") Write-Host "==================================================" -ForegroundColor Cyan ; Write-Host " Get-Help Get-AutopatchHealh `n" -Foregroundcolor Green # Remediation exit codes for Intune reporting if ($Remediation -eq $true) { if ($script:exitCode -ge 1) { $failSummary = @() $fails = $summary | Where-Object { $_ -match '\bFAIL\b' } # Get only failed entries if ($fails) { $fails | ForEach-Object { $failSummary += $_ } } # Add fails to detection output summary Write-Host ($failSummary -join " | ") # This is what's displayed in the Intune admin center's detection output Exit 1 # Remediation needed } else { Write-Output "[PASS] Healthy Autopatch configuration" # This is what's displayed in the Intune admin center's detection output Exit 0 # Remediation not needed } } Exit |