Private/Get-WUPendingUpdates.ps1

function Get-WUPendingUpdates {
    <#
    .SYNOPSIS
        Retrieves policy-scoped pending Windows Updates with a bounded WUA search.

    .DESCRIPTION
        Runs the synchronous Windows Update Agent search in an isolated child process so
        an unhealthy WUA client cannot block the calling diagnostic indefinitely.

        On Intune, Windows Update for Business, and Windows Autopatch devices, the result
        contains only updates visible under the effective update policy. It does not
        bypass deployment approvals, rollout timing, safeguard holds, or scan-source
        configuration.
    #>


    [CmdletBinding()]
    param(
        [string]$LogPath,

        [ValidateRange(5, 1800)]
        [int]$SearchTimeoutSeconds = 120
    )

    Write-WULog -Message 'Checking for policy-scoped pending Windows Updates' -LogPath $LogPath
    Write-WULog -Message "WUA search timeout: $SearchTimeoutSeconds seconds" -LogPath $LogPath

    $searchEnvelope = Invoke-WUSearch `
        -Criteria 'IsInstalled=0' `
        -TimeoutSeconds $SearchTimeoutSeconds `
        -HistoryLimit 200 `
        -LogPath $LogPath

    $updateHistory = @{}
    foreach ($entry in @($searchEnvelope.History)) {
        if ($entry.KBNumber) {
            if (-not $updateHistory.ContainsKey($entry.KBNumber)) {
                $updateHistory[$entry.KBNumber] = @()
            }
            $updateHistory[$entry.KBNumber] += $entry
        }
    }

    $pendingUpdates = foreach ($update in @($searchEnvelope.Updates)) {
        $effectiveSize = if ($update.ActualDownloadSize -gt 0) {
            [int64]$update.ActualDownloadSize
        }
        else {
            [int64]$update.MaxDownloadSize
        }

        $sizeKB = if ($effectiveSize -gt 0) {
            [math]::Round($effectiveSize / 1KB, 1)
        }
        else {
            0
        }

        $importance = if ($update.MsrcSeverity) {
            $update.MsrcSeverity
        }
        elseif ($update.AutoSelectOnWebSites) {
            'Important'
        }
        else {
            'Optional'
        }

        $kbNumber = $null
        if ($update.KBArticleIDs -and @($update.KBArticleIDs).Count -gt 0) {
            $kbNumber = 'KB{0}' -f @($update.KBArticleIDs)[0]
        }
        elseif ($update.Title -match 'KB(\d+)') {
            $kbNumber = "KB$($matches[1])"
        }

        $previousAttempts = 0
        $lastAttemptDate = $null
        $lastAttemptResult = $null
        if ($kbNumber -and $updateHistory.ContainsKey($kbNumber)) {
            $attempts = @($updateHistory[$kbNumber])
            $previousAttempts = $attempts.Count
            $lastAttempt = $attempts | Sort-Object Date -Descending | Select-Object -First 1
            if ($lastAttempt) {
                $lastAttemptDate = $lastAttempt.Date
                $lastAttemptResult = switch ([int]$lastAttempt.ResultCode) {
                    0 { 'Not Started' }
                    1 { 'In Progress' }
                    2 { 'Succeeded' }
                    3 { 'Succeeded with Errors' }
                    4 { 'Failed (0x{0:X8})' -f ([uint32]$lastAttempt.HResult) }
                    5 { 'Aborted' }
                    default { "Unknown ($($lastAttempt.ResultCode))" }
                }
            }
        }

        $downloadStatus = if ($update.IsDownloaded) {
            'Downloaded (100%)'
        }
        elseif ($update.DownloadContentsCount -gt 0) {
            'Partially Downloaded'
        }
        else {
            'Not Downloaded (0%)'
        }

        $installationState = if ($update.IsDownloaded) {
            'Pending Install'
        }
        elseif ($update.DownloadContentsCount -gt 0) {
            'Downloading'
        }
        else {
            'Pending Download'
        }

        if ($previousAttempts -gt 0 -and $lastAttemptResult -match 'Failed|Aborted') {
            $installationState = "Retry Pending ($previousAttempts previous attempts)"
        }

        [PSCustomObject]@{
            Title = $update.Title
            KBNumber = $kbNumber
            KBArticleIDs = @($update.KBArticleIDs)
            SecurityBulletinIDs = @($update.SecurityBulletinIDs)
            Description = $update.Description
            Categories = @($update.Categories) -join ', '
            Type = $update.Type
            Importance = $importance
            SizeKB = $sizeKB
            SizeMB = [math]::Round($sizeKB / 1024, 1)
            MaxDownloadSizeMB = [math]::Round(([int64]$update.MaxDownloadSize) / 1MB, 1)
            IsEstimatedSize = $update.ActualDownloadSize -le 0
            DownloadContentsCount = $update.DownloadContentsCount
            DownloadStatus = $downloadStatus
            IsDownloaded = [bool]$update.IsDownloaded
            InstallationState = $installationState
            PreviousAttempts = $previousAttempts
            LastAttemptDate = $lastAttemptDate
            LastAttemptResult = $lastAttemptResult
            RebootRequired = [bool]$update.RebootRequired
            IsHidden = [bool]$update.IsHidden
            IsMandatory = [bool]$update.IsMandatory
            ReleaseDate = $update.ReleaseDate
            SupportUrl = $update.SupportUrl
            UpdateID = $update.UpdateID
            RevisionNumber = $update.RevisionNumber
            ScanScope = 'Windows Update Agent policy-scoped catalog'
        }
    }

    $pendingUpdates = @($pendingUpdates)
    Write-WULog -Message "Policy-scoped WUA search completed; pending updates: $($pendingUpdates.Count)" -LogPath $LogPath

    if ($pendingUpdates.Count -gt 0) {
        $totalSizeMB = ($pendingUpdates | Measure-Object -Property SizeMB -Sum).Sum
        $retryCount = @(
            $pendingUpdates | Where-Object {
                $_.PreviousAttempts -gt 0 -and $_.LastAttemptResult -match 'Failed|Aborted'
            }
        ).Count

        Write-WULog -Message " Estimated total size: $([math]::Round($totalSizeMB, 1)) MB" -LogPath $LogPath
        Write-WULog -Message " Previously failed and pending retry: $retryCount" -LogPath $LogPath
    }

    return $pendingUpdates
}