mqsft.windowspatching.psm1
|
# ============================================================================== # mqsft.windowspatching.psm1 - AUTO-GENERATED by Create_Module_0_0_7.ps1 v0.0.7 # ------------------------------------------------------------------------------ # Module Version : 1.0.12 # Script Version : 0.0.7 # Built : 2026-02-24 21:51:37 # Source : C:\Modules\functions\mqsft.windowspatching # # DO NOT EDIT THIS FILE DIRECTLY. # Edit source files in C:\Modules\functions\mqsft.windowspatching and re-run Create_Module_0_0_7.ps1. # ============================================================================== Set-StrictMode -Version Latest # --- Source: Clear-WindowsUpdateCache.ps1 -------------------------------- function Clear-WindowsUpdateCache { <# .SYNOPSIS Clears the Windows Update cache by stopping services, deleting SoftwareDistribution contents, and restarting services. .DESCRIPTION This function stops the Windows Update (wuauserv) and BITS services, clears all files in the C:\Windows\SoftwareDistribution folder, and then restarts the services. This is a common troubleshooting step for Windows Update issues. .EXAMPLE Clear-WindowsUpdateCache Clears the Windows Update cache folder. .NOTES Requires Administrator privileges to run. #> [CmdletBinding()] param() # Check if running as Administrator $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Error "This function requires Administrator privileges. Please run PowerShell as Administrator." return } try { Write-Host "Stopping Windows Update services..." -ForegroundColor Yellow # Stop Windows Update service Write-Host " Stopping Windows Update service (wuauserv)..." -ForegroundColor Cyan Stop-Service -Name wuauserv -Force -ErrorAction Stop # Stop BITS service Write-Host " Stopping BITS service..." -ForegroundColor Cyan Stop-Service -Name bits -Force -ErrorAction Stop Write-Host "Services stopped successfully." -ForegroundColor Green # Clear SoftwareDistribution folder $softwareDistPath = "C:\Windows\SoftwareDistribution" Write-Host "`nClearing SoftwareDistribution folder..." -ForegroundColor Yellow if (Test-Path $softwareDistPath) { $items = Get-ChildItem -Path $softwareDistPath -Recurse -Force $itemCount = $items.Count Write-Host " Found $itemCount items to delete..." -ForegroundColor Cyan Remove-Item -Path "$softwareDistPath\*" -Recurse -Force -ErrorAction Stop Write-Host "SoftwareDistribution folder cleared successfully." -ForegroundColor Green } else { Write-Warning "SoftwareDistribution folder not found at $softwareDistPath" } # Restart services Write-Host "`nRestarting Windows Update services..." -ForegroundColor Yellow Write-Host " Starting Windows Update service (wuauserv)..." -ForegroundColor Cyan Start-Service -Name wuauserv -ErrorAction Stop Write-Host " Starting BITS service..." -ForegroundColor Cyan Start-Service -Name bits -ErrorAction Stop Write-Host "Services restarted successfully." -ForegroundColor Green Write-Host "`n✓ Windows Update cache cleared successfully!" -ForegroundColor Green Write-Host "You can now try running Windows Update again." -ForegroundColor White } catch { Write-Error "An error occurred: $_" # Attempt to restart services if they were stopped Write-Host "`nAttempting to restart services..." -ForegroundColor Yellow try { Start-Service -Name wuauserv -ErrorAction SilentlyContinue Start-Service -Name bits -ErrorAction SilentlyContinue } catch { Write-Warning "Could not restart services. You may need to restart them manually." } } } # Export the function (if this script is used as a module) # Export-ModuleMember -Function Clear-WindowsUpdateCache # --- Source: Export-PatchReport.ps1 -------------------------------------- #Requires -Version 5.1 <# .SYNOPSIS Generates a self-contained HTML patch report for the local machine. .DESCRIPTION Export-PatchReport collects patch status, compliance score, hotfix history, and pending update data, then renders it as a professional standalone HTML file that can be opened in any browser, emailed, or stored as a record. The report includes: - Machine and OS summary header - Compliance score and grade (colour coded) - Pending updates table - Installed hotfix history (last N patches) - Failed/aborted update callout section - WU service and configuration status .PARAMETER OutputPath File path for the HTML report. Defaults to Desktop\PatchReport_<hostname>_<date>.html .PARAMETER Last Number of historical hotfixes to include. Default: 50. .PARAMETER OpenInBrowser Automatically open the report in the default browser after generation. .OUTPUTS [string] Path to the generated HTML file. .EXAMPLE Export-PatchReport Generates report on the Desktop and prints the path. .EXAMPLE Export-PatchReport -OutputPath "C:\Reports\Server01.html" -OpenInBrowser Saves to a specific path and opens it in the browser immediately. .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS #> function Export-PatchReport { [CmdletBinding()] [OutputType([string])] param( [string]$OutputPath = "$([System.Environment]::GetFolderPath('Desktop'))\PatchReport_$($env:COMPUTERNAME)_$(Get-Date -Format 'yyyyMMdd_HHmm').html", [ValidateRange(1,500)] [int]$Last = 50, [switch]$OpenInBrowser ) Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' EXPORT PATCH REPORT ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' Collecting data...' -ForegroundColor DarkGray $now = Get-Date $os = Get-CimInstance Win32_OperatingSystem $cs = Get-CimInstance Win32_ComputerSystem $reboot = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' $wuSvc = (Get-Service wuauserv -ErrorAction SilentlyContinue).Status # Hotfix history Write-Host ' Loading hotfix history...' -ForegroundColor DarkGray $hotfixes = @() try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $total = $searcher.GetTotalHistoryCount() if ($total -gt 0) { $history = $searcher.QueryHistory(0, [Math]::Min($total, $Last * 2)) $hotfixes = $history | Where-Object { $_.Title -match 'KB' } | Select-Object -First $Last | ForEach-Object { $rc = switch ($_.ResultCode) { 2 { 'Succeeded' } 3 { 'Succeeded*' } 4 { 'Failed' } 5 { 'Aborted' } default { 'Unknown' } } [PSCustomObject]@{ Date = $_.Date.ToString('yyyy-MM-dd HH:mm') KB = if ($_.Title -match '(KB\d+)') { $Matches[1] } else { '' } Title = $_.Title Result = $rc Categories = ($_.Categories | ForEach-Object { $_.Name }) -join ', ' } } } } catch {} # Pending updates Write-Host ' Checking pending updates...' -ForegroundColor DarkGray $pending = @() try { $pendResult = $searcher.Search('IsInstalled=0 and IsHidden=0') $pending = $pendResult.Updates | ForEach-Object { $kb = if ($_.KBArticleIDs.Count -gt 0) { "KB$($_.KBArticleIDs[0])" } else { 'N/A' } $sev = if ($_.MsrcSeverity) { $_.MsrcSeverity } else { 'N/A' } [PSCustomObject]@{ KB = $kb Title = $_.Title Severity = $sev SizeMB = [math]::Round($_.MaxDownloadSize / 1MB, 1) } } } catch {} # Compliance score (quick inline version) $score = 100 if ($null -eq ($hotfixes | Where-Object { $_.Result -eq 'Succeeded' } | Select-Object -First 1)) { $score -= 30 } if ($pending.Count -gt 5) { $score -= [Math]::Min(25, $pending.Count * 3) } if ($reboot) { $score -= 10 } if ($wuSvc -ne 'Running') { $score -= 15 } $score = [Math]::Max(0, $score) $grade = switch ($score) { { $_ -ge 90 } {'A'} { $_ -ge 80 } {'B'} { $_ -ge 70 } {'C'} { $_ -ge 60 } {'D'} default {'F'} } $gradeHex = switch ($grade) { 'A' {'#22c55e'} 'B' {'#86efac'} 'C' {'#facc15'} 'D' {'#f97316'} 'F' {'#ef4444'} } $failed = $hotfixes | Where-Object { $_.Result -in 'Failed','Aborted' } # ── Build HTML rows ────────────────────────────────────────────────────── function Get-PendingRows { if (-not $pending) { return '<tr><td colspan="4" style="text-align:center;color:#6b7280">No pending updates</td></tr>' } $pending | ForEach-Object { $sc = switch ($_.Severity) { 'Critical' {'#ef4444'} 'Important' {'#f97316'} default {'#6b7280'} } "<tr> <td><code>$($_.KB)</code></td> <td style='color:$sc;font-weight:600'>$($_.Severity)</td> <td style='max-width:500px'>$($_.Title)</td> <td>$($_.SizeMB) MB</td> </tr>" } } function Get-HistoryRows { if (-not $hotfixes) { return '<tr><td colspan="4" style="text-align:center;color:#6b7280">No history found</td></tr>' } $hotfixes | ForEach-Object { $rc = $_.Result $bc = switch ($rc) { 'Succeeded' {'#dcfce7'} 'Succeeded*' {'#fef9c3'} 'Failed' {'#fee2e2'} 'Aborted' {'#fee2e2'} default {'#f3f4f6'} } $tc = switch ($rc) { 'Succeeded' {'#15803d'} 'Succeeded*' {'#854d0e'} 'Failed' {'#991b1b'} 'Aborted' {'#991b1b'} default {'#374151'} } "<tr> <td>$($_.Date)</td> <td><code>$($_.KB)</code></td> <td style='max-width:480px;overflow:hidden'>$($_.Title)</td> <td><span style='background:$bc;color:$tc;padding:2px 8px;border-radius:4px;font-size:0.8em;font-weight:600'>$rc</span></td> </tr>" } } function Get-FailedRows { if (-not $failed -or $failed.Count -eq 0) { return '<p style="color:#22c55e">✔ No failed or aborted updates in history.</p>' } $rows = $failed | ForEach-Object { "<tr style='background:#fee2e2'> <td>$($_.Date)</td> <td><code>$($_.KB)</code></td> <td>$($_.Title)</td> <td style='color:#991b1b;font-weight:600'>$($_.Result)</td> </tr>" } return "<table class='data-table'> <thead><tr><th>Date</th><th>KB</th><th>Title</th><th>Result</th></tr></thead> <tbody>$($rows -join '')</tbody></table>" } # ── Compose HTML ───────────────────────────────────────────────────────── $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Patch Report — $($env:COMPUTERNAME)</title> <style> *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f1f5f9; color: #1e293b; padding: 24px; } h1 { font-size: 1.6rem; font-weight: 700; margin-bottom: 4px; } h2 { font-size: 1.1rem; font-weight: 600; margin: 0 0 12px; color: #334155; } .card { background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.08); padding: 20px 24px; margin-bottom: 20px; } .header-bar { background: #1e3a5f; color: #fff; border-radius: 10px; padding: 20px 28px; margin-bottom: 20px; display:flex; justify-content:space-between; align-items:center; } .header-meta { font-size: 0.85rem; opacity: 0.8; margin-top: 6px; } .score-circle { width: 90px; height: 90px; border-radius: 50%; display:flex; flex-direction:column; align-items:center; justify-content:center; background: $gradeHex; } .score-num { font-size: 1.8rem; font-weight: 800; color: #fff; line-height:1; } .score-lbl { font-size: 0.7rem; color: rgba(255,255,255,.8); } .kv-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; } .kv { background:#f8fafc; border-radius:8px; padding: 12px 16px; } .kv-label { font-size: 0.72rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; } .kv-value { font-size: 1rem; font-weight: 700; color: #0f172a; } .badge { display:inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 700; } .badge-ok { background:#dcfce7; color:#15803d; } .badge-warn{ background:#fef9c3; color:#854d0e; } .badge-fail{ background:#fee2e2; color:#991b1b; } .data-table { width:100%; border-collapse:collapse; font-size:0.85rem; } .data-table th { background:#f1f5f9; padding:8px 12px; text-align:left; font-weight:600; font-size:0.75rem; text-transform:uppercase; letter-spacing:.05em; color:#475569; border-bottom:2px solid #e2e8f0; } .data-table td { padding:8px 12px; border-bottom:1px solid #f1f5f9; vertical-align:top; } .data-table tbody tr:hover { background:#f8fafc; } code { background:#f1f5f9; padding:1px 5px; border-radius:4px; font-size:0.85em; } .section-warn { background:#fff7ed; border-left: 4px solid #f97316; padding: 16px; border-radius: 0 8px 8px 0; } footer { text-align:center; color:#94a3b8; font-size:0.8rem; margin-top:8px; } </style> </head> <body> <div class="header-bar"> <div> <h1>🛡 Windows Patch Report</h1> <div class="header-meta"> $($env:COMPUTERNAME) | $($os.Caption) ($($os.Version)) | Generated: $($now.ToString('yyyy-MM-dd HH:mm:ss')) </div> </div> <div class="score-circle"> <span class="score-num">$score</span> <span class="score-lbl">SCORE</span> </div> </div> <div class="card"> <h2>System Overview</h2> <div class="kv-grid"> <div class="kv"><div class="kv-label">Computer</div><div class="kv-value">$($env:COMPUTERNAME)</div></div> <div class="kv"><div class="kv-label">Domain / Workgroup</div><div class="kv-value">$($cs.Domain)</div></div> <div class="kv"><div class="kv-label">OS</div><div class="kv-value" style="font-size:.85rem">$($os.Caption)</div></div> <div class="kv"><div class="kv-label">OS Version</div><div class="kv-value">$($os.Version)</div></div> <div class="kv"><div class="kv-label">WU Service</div><div class="kv-value"><span class="badge $(if ($wuSvc -eq 'Running') {'badge-ok'} else {'badge-fail'})">$wuSvc</span></div></div> <div class="kv"><div class="kv-label">Reboot Required</div><div class="kv-value"><span class="badge $(if ($reboot) {'badge-warn'} else {'badge-ok'})">$(if ($reboot) {'Yes'} else {'No'})</span></div></div> <div class="kv"><div class="kv-label">Pending Updates</div><div class="kv-value"><span class="badge $(if ($pending.Count -gt 0) {'badge-warn'} else {'badge-ok'})">$($pending.Count)</span></div></div> <div class="kv"><div class="kv-label">Compliance Grade</div><div class="kv-value" style="color:$gradeHex;font-size:1.5rem">$grade</div></div> </div> </div> $(if ($pending.Count -gt 0) { "<div class='card'> <h2>⬇ Pending Updates ($($pending.Count))</h2> <table class='data-table'> <thead><tr><th>KB</th><th>Severity</th><th>Title</th><th>Size</th></tr></thead> <tbody>$(Get-PendingRows)</tbody> </table> </div>" } else { "<div class='card'><h2>⬇ Pending Updates</h2><p style='color:#22c55e'>✔ No pending updates.</p></div>" }) $(if ($failed -and $failed.Count -gt 0) { "<div class='card section-warn'> <h2>âš Failed / Aborted Updates ($($failed.Count))</h2> $(Get-FailedRows) </div>" }) <div class="card"> <h2>📋 Installed Update History (last $Last)</h2> <table class="data-table"> <thead><tr><th>Date</th><th>KB</th><th>Title</th><th>Result</th></tr></thead> <tbody>$(Get-HistoryRows)</tbody> </table> </div> <footer>Generated by PSWinAdmin · Get-PatchReport · $($now.ToString('yyyy-MM-dd HH:mm:ss'))</footer> </body> </html> "@ # Write file $html | Set-Content -Path $OutputPath -Encoding UTF8 Write-Host " Report saved to: $OutputPath" -ForegroundColor Green if ($OpenInBrowser) { Start-Process $OutputPath } Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return $OutputPath } # --- Source: Find-Update.ps1 --------------------------------------------- #Requires -Version 5.1 <# .SYNOPSIS Searches available Windows Updates without installing them. .DESCRIPTION Find-Update queries the Windows Update COM API (or WSUS if configured) to list available updates matching your criteria — without downloading or installing anything. Useful for: - Auditing what is pending before a maintenance window - Checking whether a specific KB is available - Filtering by category (Security, Critical, Driver, etc.) - Piping results into Invoke-WindowsUpdate or Install-SpecificKB .PARAMETER KB Search for a specific KB article. Example: -KB KB5034441 .PARAMETER Title Search by title substring. Example: -Title ".NET" .PARAMETER Category Filter by update category. Example: -Category Security .PARAMETER IncludeHidden Include updates that have been hidden/declined. .OUTPUTS [PSCustomObject[]] — one object per matching update. .EXAMPLE Find-Update Lists all pending updates. .EXAMPLE Find-Update -Category Security Lists pending Security updates only. .EXAMPLE Find-Update -KB KB5034441 Checks whether a specific KB is available and not yet installed. .EXAMPLE Find-Update | Where-Object { $_.SizeMB -gt 100 } Find large updates (over 100 MB). .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS #> function Find-Update { [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter()] [string]$KB, [Parameter()] [string]$Title, [Parameter()] [string]$Category, [switch]$IncludeHidden ) Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' FIND WINDOWS UPDATES ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan Write-Host "`n Searching Windows Update..." -ForegroundColor DarkGray $query = if ($IncludeHidden) { 'IsInstalled=0' } else { 'IsInstalled=0 and IsHidden=0' } try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $searchResult = $searcher.Search($query) } catch { Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red return } $updates = @($searchResult.Updates) # Apply filters if ($KB) { $kbNum = $KB -replace 'KB', '' $updates = $updates | Where-Object { $_.KBArticleIDs -contains $kbNum } } if ($Title) { $updates = $updates | Where-Object { $_.Title -match [regex]::Escape($Title) } } if ($Category) { $updates = $updates | Where-Object { ($_.Categories | ForEach-Object { $_.Name }) -match $Category } } if ($updates.Count -eq 0) { Write-Host ' No matching updates found.' -ForegroundColor Green return } Write-Host " Found $($updates.Count) update(s):`n" -ForegroundColor White $header = " {0,-12} {1,-10} {2,-12} {3,-8} {4,-12} {5}" Write-Host ($header -f 'KB','Severity','Category','Size MB','Downloaded','Title') -ForegroundColor Cyan Write-Host ($header -f '--','--------','--------','-------','-----------','-----') -ForegroundColor DarkGray $results = $updates | ForEach-Object { $kb = if ($_.KBArticleIDs.Count -gt 0) { "KB$($_.KBArticleIDs[0])" } else { 'N/A' } $cats = ($_.Categories | ForEach-Object { $_.Name }) -join ', ' $sizeMB = [math]::Round($_.MaxDownloadSize / 1MB, 1) $sev = if ($_.MsrcSeverity) { $_.MsrcSeverity } else { 'N/A' } $dlStatus = if ($_.IsDownloaded) { 'Yes' } else { 'No' } $title = if ($_.Title.Length -gt 55) { $_.Title.Substring(0,52) + '...' } else { $_.Title } $sevColor = switch ($sev) { 'Critical' { 'Red' } 'Important' { 'Yellow' } 'Moderate' { 'White' } default { 'DarkGray' } } Write-Host ($header -f $kb, $sev, ($cats.Split(',')[0].Trim()), $sizeMB, $dlStatus, $title) -ForegroundColor $sevColor [PSCustomObject]@{ KB = $kb Title = $_.Title Severity = $sev Categories = $cats SizeMB = $sizeMB IsDownloaded = $_.IsDownloaded IsHidden = $_.IsHidden RebootRequired = $_.InstallationBehavior.RebootBehavior -ne 0 Description = $_.Description SupportURL = $_.SupportUrl Identity = $_.Identity } } $totalMB = [math]::Round(($results | Measure-Object SizeMB -Sum).Sum, 1) Write-Host '' Write-Host " Total download size: $totalMB MB" -ForegroundColor White Write-Host " Run Invoke-WindowsUpdate to install all, or Install-SpecificKB -KB <id> for one." -ForegroundColor DarkGray Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return $results } # --- Source: Get-HotFixReport.ps1 ---------------------------------------- function Get-HotFixReport { param( [int]$Last = 50, [string]$ExportCSV = "" ) # OS install date - exact datetime $osInstallDate = (Get-CimInstance Win32_OperatingSystem).InstallDate Write-Host "`n Detecting OS install date: $($osInstallDate.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor DarkGray # Pull full WU history via COM API $updateSession = New-Object -ComObject Microsoft.Update.Session $updateSearcher = $updateSession.CreateUpdateSearcher() $totalHistory = $updateSearcher.GetTotalHistoryCount() $updateHistory = $updateSearcher.QueryHistory(0, $totalHistory) # Build KB lookup from WU history (exact datetimes) $wuLookup = @{} foreach ($entry in $updateHistory) { if ($entry.Title -match '(KB\d+)') { $kb = $matches[1] if (-not $wuLookup.ContainsKey($kb)) { $wuLookup[$kb] = [PSCustomObject]@{ Title = $entry.Title Date = $entry.Date # exact datetime from WU ResultCode = switch ($entry.ResultCode) { 1 { "In Progress" } 2 { "Succeeded" } 3 { "Succeeded (Reboot Req)" } 4 { "Failed" } 5 { "Aborted" } default { "Unknown" } } Categories = ($entry.Categories | ForEach-Object { $_.Name }) -join ", " SupportURL = $entry.SupportUrl Description = $entry.Description ClientAppID = $entry.ClientApplicationID } } } } # Build hotfix list, using WU exact datetime for PreImage decision $hotfixes = Get-HotFix | ForEach-Object { $kb = $_.HotFixID $wu = $wuLookup[$kb] # Use WU exact datetime if available - this is the source of truth # Get-HotFix only returns date with no time, causing same-day collisions $exactDate = if ($wu -and $wu.Date) { $wu.Date } elseif ($_.InstalledOn) { [datetime]$_.InstalledOn } else { $null } # PreImage = installed BEFORE OS was deployed # If we only have a date (no time), and it's the same day as install, # we treat it as POST-image to avoid false positives $preImage = if ($null -eq $exactDate) { $false # unknown date = assume post-image } elseif ($wu -and $wu.Date) { # We have exact time from WU - use precise comparison $wu.Date -lt $osInstallDate } else { # Only have a date from Get-HotFix, no time # If same day as install, benefit of doubt = post-image $exactDate.Date -lt $osInstallDate.Date } [PSCustomObject]@{ InstalledOn = $exactDate InstalledDate = if ($exactDate) { $exactDate.ToString("yyyy-MM-dd") } else { "Unknown" } InstalledTime = if ($exactDate -and $wu -and $wu.Date) { $exactDate.ToString("HH:mm:ss") } else { "Unknown" } HotFixID = $kb Title = if ($wu) { $wu.Title } else { "$($_.Description) $kb" } Description = $_.Description InstalledBy = if ($_.InstalledBy -and $_.InstalledBy -ne "") { $_.InstalledBy } else { "Unknown" } ClientAppID = if ($wu -and $wu.ClientAppID) { $wu.ClientAppID -replace "Microsoft\.Windows\.", "" -replace "AutomaticUpdates","AutoUpdate" } else { "Unknown" } Result = if ($wu) { $wu.ResultCode } else { "Unknown" } Categories = if ($wu) { $wu.Categories } else { $_.Description } SupportURL = if ($wu) { $wu.SupportURL } else { "https://support.microsoft.com/en-us/search?query=$kb" } ComputerName = $_.CSName PreImage = $preImage } } | Sort-Object InstalledOn -Descending | Select-Object -First $Last $postImage = $hotfixes | Where-Object { -not $_.PreImage } $preImage = $hotfixes | Where-Object { $_.PreImage } $dated = $postImage | Where-Object { $_.InstalledOn } $oldest = ($dated | Sort-Object InstalledOn | Select-Object -First 1).InstalledOn $newest = ($dated | Sort-Object InstalledOn -Descending | Select-Object -First 1).InstalledOn $last7 = ($dated | Where-Object { $_.InstalledOn -ge (Get-Date).AddDays(-7) }).Count $last30 = ($dated | Where-Object { $_.InstalledOn -ge (Get-Date).AddDays(-30) }).Count $byType = $postImage | Group-Object Description | Sort-Object Count -Descending $failed = $hotfixes | Where-Object { $_.Result -eq "Failed" -or $_.Result -eq "Aborted" } function Write-Section ($text) { Write-Host "`n[$text]" -ForegroundColor Yellow } function Get-AgeColor ($date, $preImage, $result) { if ($preImage) { return "DarkGray" } if ($result -in "Failed","Aborted") { return "Red" } if (-not $date) { return "DarkGray" } $days = ((Get-Date) - $date).Days if ($days -le 7) { return "Green" } if ($days -le 30) { return "Yellow" } if ($days -le 90) { return "White" } return "DarkGray" } # ── HEADER ────────────────────────────────────────── Write-Host "" Write-Host "=====================================" -ForegroundColor Cyan Write-Host " HOTFIX / PATCH REPORT " -ForegroundColor Cyan Write-Host "=====================================" -ForegroundColor Cyan Write-Host " Computer : " -NoNewline; Write-Host $env:COMPUTERNAME -ForegroundColor White Write-Host " Generated : " -NoNewline; Write-Host (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ForegroundColor White Write-Host " OS Deployed : " -NoNewline; Write-Host ($osInstallDate.ToString("yyyy-MM-dd HH:mm:ss")) -ForegroundColor Magenta Write-Host " Showing : Last $Last updates" Write-Host "-------------------------------------" -ForegroundColor DarkGray # ── SUMMARY ───────────────────────────────────────── Write-Section "SUMMARY" Write-Host " Total found : " -NoNewline; Write-Host $hotfixes.Count -ForegroundColor White Write-Host " Post-deploy : " -NoNewline; Write-Host $postImage.Count -ForegroundColor $(if ($postImage.Count -gt 0) { "Green" } else { "White" }) Write-Host " Baked in image : " -NoNewline; Write-Host $preImage.Count -ForegroundColor DarkGray Write-Host " Last 7 days : " -NoNewline; Write-Host $last7 -ForegroundColor $(if ($last7 -gt 0) { "Green" } else { "DarkGray" }) Write-Host " Last 30 days : " -NoNewline; Write-Host $last30 -ForegroundColor $(if ($last30 -gt 0) { "Yellow" } else { "DarkGray" }) Write-Host " Failed/Aborted : " -NoNewline; Write-Host $failed.Count -ForegroundColor $(if ($failed.Count -gt 0) { "Red" } else { "Green" }) if ($newest) { Write-Host " Newest (post) : " -NoNewline; Write-Host ($newest.ToString("yyyy-MM-dd HH:mm:ss")) -ForegroundColor Green } if ($oldest) { Write-Host " Oldest (post) : " -NoNewline; Write-Host ($oldest.ToString("yyyy-MM-dd HH:mm:ss")) -ForegroundColor White } # ── FAILED CALLOUT ─────────────────────────────────── if ($failed.Count -gt 0) { Write-Section "FAILED / ABORTED ⚠" $failed | ForEach-Object { Write-Host (" {0,-15} {1,-22} {2}" -f $_.HotFixID, $_.Result, $_.Title) -ForegroundColor Red } } # ── BY TYPE ───────────────────────────────────────── Write-Section "BY TYPE (Post-Deploy)" if ($byType) { $byType | ForEach-Object { Write-Host (" {0,-30} : " -f $_.Name) -NoNewline Write-Host $_.Count -ForegroundColor White } } else { Write-Host " None post-deploy." -ForegroundColor DarkGray } # ── PATCH LIST ────────────────────────────────────── Write-Section "PATCH LIST" Write-Host (" {0,-12} {1,-8} {2,-15} {3,-22} {4,-10} {5,-25} {6,-20} {7,-22} {8}" -f ` "Date","Time","KB","Type","Age","Installed By","Installer App","Result","Source") -ForegroundColor Cyan Write-Host (" {0,-12} {1,-8} {2,-15} {3,-22} {4,-10} {5,-25} {6,-20} {7,-22} {8}" -f ` "----","----","--","----","---","------------","-------------","------","------") -ForegroundColor DarkGray $hotfixes | ForEach-Object { $daysAgo = if ($_.InstalledOn) { "$( ((Get-Date) - $_.InstalledOn).Days ) days" } else { "?" } $source = if ($_.PreImage) { "[BASE IMAGE]" } else { "Post-Deploy" } $color = Get-AgeColor $_.InstalledOn $_.PreImage $_.Result Write-Host (" {0,-12} {1,-8} {2,-15} {3,-22} {4,-10} {5,-25} {6,-20} {7,-22} {8}" -f $_.InstalledDate, $_.InstalledTime, $_.HotFixID, $_.Description, $daysAgo, $_.InstalledBy, $_.ClientAppID, $_.Result, $source ) -ForegroundColor $color } # ── LEGEND ────────────────────────────────────────── Write-Section "LEGEND" Write-Host " ● Post-deploy 0-7d " -ForegroundColor Green -NoNewline; Write-Host "(Green)" -ForegroundColor DarkGray Write-Host " ● Post-deploy 8-30d " -ForegroundColor Yellow -NoNewline; Write-Host "(Yellow)" -ForegroundColor DarkGray Write-Host " ● Post-deploy 31-90d" -ForegroundColor White -NoNewline; Write-Host "(White)" -ForegroundColor DarkGray Write-Host " ● Baked in image " -ForegroundColor DarkGray -NoNewline; Write-Host "(DarkGray)" -ForegroundColor DarkGray Write-Host " ● Failed/Aborted " -ForegroundColor Red -NoNewline; Write-Host "(Red)" -ForegroundColor DarkGray # ── GAP WARNING ───────────────────────────────────── if ($oldest -and $newest) { $gap = ($newest - $oldest).Days if ($gap -gt 30) { Write-Host "`n ⚠ WARNING: $gap day gap in post-deploy patches." -ForegroundColor Red } else { Write-Host "`n ✔ Patch gap is within acceptable range ($gap days)." -ForegroundColor Green } } else { Write-Host "`n ✔ No post-deploy patch gap to report." -ForegroundColor Green } # ── EXPORT ────────────────────────────────────────── if ($ExportCSV -ne "") { $hotfixes | Export-Csv -Path $ExportCSV -NoTypeInformation Write-Host "`n[EXPORT] Saved to: $ExportCSV" -ForegroundColor Cyan } Write-Host "`n=====================================" -ForegroundColor Cyan # ── AUTO-RUN UPDATES ──────────────────────────────── Write-Host "`n[AUTO-UPDATE CHECK]" -ForegroundColor Yellow Write-Host " Checking for pending updates..." -ForegroundColor DarkGray try { Import-Module PSWindowsUpdate -ErrorAction Stop $pending = Get-WindowsUpdate -AcceptAll -IgnoreReboot 2>$null if ($pending -and $pending.Count -gt 0) { Write-Host " Found $($pending.Count) pending update(s) — installing now..." -ForegroundColor Yellow $pending | ForEach-Object { Write-Host " → $($_.KB) $($_.Title)" -ForegroundColor DarkGray } Get-WindowsUpdate -Install -AcceptAll -AutoReboot -IgnoreReboot 2>$null Write-Host " ✔ Updates installed. A reboot may be pending." -ForegroundColor Green } else { Write-Host " ✔ System is fully up to date." -ForegroundColor Green } } catch { Write-Host " ⚠ PSWindowsUpdate module not found. Run: Install-Module PSWindowsUpdate -Force" -ForegroundColor Red } Write-Host "`n=====================================" -ForegroundColor Cyan } # --- Source: Get-PatchStatus.ps1 ----------------------------------------- #Requires -Version 5.1 <# .SYNOPSIS Quick patch health snapshot for the local machine. .DESCRIPTION Get-PatchStatus returns a single object summarising the machine's current Windows Update state: - Date and result of last successful update scan - Date and result of last successful installation - Count of pending (not yet installed) updates - Whether a reboot is currently required - Windows Update service state - WSUS server (if configured) Designed as a fast "at a glance" check. Use Get-HotFixReport for full historical detail, or Invoke-WindowsUpdate to action pending updates. .PARAMETER ComputerName Target computer. Defaults to the local machine. Requires WinRM / PSRemoting to be enabled on the remote host. .OUTPUTS [PSCustomObject] with properties: ComputerName, LastScanTime, LastInstallTime, PendingCount, RebootRequired, WUServiceState, WSUSServer, GeneratedAt .EXAMPLE Get-PatchStatus Returns patch status for the local machine. .EXAMPLE Get-PatchStatus -ComputerName SERVER01 Returns patch status for SERVER01 over PSRemoting. .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS Admin : Recommended (required for WU COM API on some builds) #> function Get-PatchStatus { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [string]$ComputerName = $env:COMPUTERNAME ) process { $scriptBlock = { $result = [ordered]@{ ComputerName = $env:COMPUTERNAME LastScanTime = $null LastScanResult = 'Unknown' LastInstallTime = $null LastInstallResult= 'Unknown' PendingCount = 0 RebootRequired = $false WUServiceState = 'Unknown' WSUSServer = 'Not configured (using Microsoft Update)' GeneratedAt = (Get-Date) } # WU service state $svc = Get-Service -Name wuauserv -ErrorAction SilentlyContinue $result.WUServiceState = if ($svc) { $svc.Status.ToString() } else { 'Not found' } # WSUS server from registry $wuKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' if (Test-Path $wuKey) { $wsus = (Get-ItemProperty $wuKey -ErrorAction SilentlyContinue).WUServer if ($wsus) { $result.WSUSServer = $wsus } } # Reboot required flag $rebootKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' $result.RebootRequired = Test-Path $rebootKey # WU COM API — last scan / install times and pending count try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() # Pending updates (not installed) $pending = $searcher.Search('IsInstalled=0 and IsHidden=0') $result.PendingCount = $pending.Updates.Count # History for last scan / install dates $total = $searcher.GetTotalHistoryCount() if ($total -gt 0) { $history = $searcher.QueryHistory(0, [Math]::Min($total, 50)) $installs = $history | Where-Object { $_.ResultCode -eq 2 } | Sort-Object Date -Descending if ($installs) { $result.LastInstallTime = $installs[0].Date $result.LastInstallResult = 'Succeeded' } } # Last scan time from AutoUpdate $au = New-Object -ComObject Microsoft.Update.AutoUpdate $results = $au.Results if ($results.LastSearchSuccessDate) { $result.LastScanTime = $results.LastSearchSuccessDate $result.LastScanResult = 'Succeeded' } } catch { Write-Warning "WU COM API error: $($_.Exception.Message)" } [PSCustomObject]$result } # Run locally or remotely if ($ComputerName -eq $env:COMPUTERNAME -or $ComputerName -eq 'localhost') { $data = & $scriptBlock } else { $data = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -ErrorAction Stop } # Pretty console output $pendingColor = if ($data.PendingCount -gt 0) { 'Yellow' } else { 'Green' } $rebootColor = if ($data.RebootRequired) { 'Red' } else { 'Green' } $svcColor = if ($data.WUServiceState -eq 'Running') { 'Green' } else { 'Red' } Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' PATCH STATUS SNAPSHOT ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan Write-Host " Computer : $($data.ComputerName)" Write-Host " Generated : $($data.GeneratedAt.ToString('yyyy-MM-dd HH:mm:ss'))" Write-Host '-------------------------------------' -ForegroundColor DarkGray Write-Host ' Last Scan : ' -NoNewline Write-Host $(if ($data.LastScanTime) { $data.LastScanTime.ToString('yyyy-MM-dd HH:mm') } else { 'Unknown' }) -ForegroundColor White Write-Host ' Last Install : ' -NoNewline Write-Host $(if ($data.LastInstallTime) { $data.LastInstallTime.ToString('yyyy-MM-dd HH:mm') } else { 'Unknown' }) -ForegroundColor White Write-Host ' Pending Updates: ' -NoNewline; Write-Host $data.PendingCount -ForegroundColor $pendingColor Write-Host ' Reboot Needed : ' -NoNewline; Write-Host $data.RebootRequired -ForegroundColor $rebootColor Write-Host ' WU Service : ' -NoNewline; Write-Host $data.WUServiceState -ForegroundColor $svcColor Write-Host " WSUS Server : $($data.WSUSServer)" Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return $data } } # --- Source: Get-WindowsUpdateLog.ps1 ------------------------------------ #Requires -Version 5.1 <# .SYNOPSIS Parses the Windows Update log into human-readable events. .DESCRIPTION Get-WindowsUpdateLog converts the binary ETL (Event Tracing for Windows) logs used by Windows Update into readable text, then filters and structures the output into useful events. On Windows 10/11, WU logs are stored as ETL files and require the Get-WindowsUpdateLog cmdlet (included in Windows) to decode them. On older systems, WindowsUpdate.log is plain text. The function surfaces: - Failed update installs with error codes - Successful installations with KB and timestamp - Service start/stop events - Scan completion events - Download errors .PARAMETER Last Number of most-recent log events to return. Default: 100. .PARAMETER Level Filter by event level: All, Error, Warning, Info. Default: All. .PARAMETER KB Filter events mentioning a specific KB article. .PARAMETER LogPath Optional custom path to a plain-text WindowsUpdate.log file. If not supplied, the function auto-decodes the ETL logs. .PARAMETER ExportPath Save filtered output to this text file path. .OUTPUTS [PSCustomObject[]] — structured log events. .EXAMPLE Get-WindowsUpdateLog Returns the last 100 WU log events. .EXAMPLE Get-WindowsUpdateLog -Level Error -Last 50 Returns the last 50 error events. .EXAMPLE Get-WindowsUpdateLog -KB KB5034441 Shows all log events related to KB5034441. .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS Note : ETL decode requires the Windows SDK or built-in Get-WindowsUpdateLog (available on Windows 10 1507+). Plain-text log fallback works on all. #> function Get-WindowsUpdateLog { [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [ValidateRange(1,5000)] [int]$Last = 100, [ValidateSet('All','Error','Warning','Info')] [string]$Level = 'All', [string]$KB, [string]$LogPath, [string]$ExportPath ) Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' WINDOWS UPDATE LOG ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan $rawLines = @() # ── Source: Custom log path ────────────────────────────────────────────── if ($LogPath) { if (-not (Test-Path $LogPath)) { Write-Host " ERROR: Log file not found: $LogPath" -ForegroundColor Red return } Write-Host " Reading: $LogPath" -ForegroundColor DarkGray $rawLines = Get-Content $LogPath } # ── Source: Plain-text legacy log ─────────────────────────────────────── elseif (Test-Path "$env:SystemRoot\WindowsUpdate.log") { $legacyPath = "$env:SystemRoot\WindowsUpdate.log" Write-Host " Reading legacy log: $legacyPath" -ForegroundColor DarkGray $rawLines = Get-Content $legacyPath } # ── Source: ETL decode via Get-WindowsUpdateLog ───────────────────────── else { $tempLog = "$env:TEMP\PSWinAdmin_WULog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" Write-Host " Decoding ETL logs (this takes ~30 seconds)..." -ForegroundColor DarkGray try { $null = Get-WindowsUpdateLog -LogPath $tempLog -ErrorAction Stop 2>&1 if (Test-Path $tempLog) { $rawLines = Get-Content $tempLog Write-Host " ETL decoded: $tempLog" -ForegroundColor DarkGray } } catch { Write-Host " Could not decode ETL log: $($_.Exception.Message)" -ForegroundColor Yellow Write-Host ' Falling back to Windows Event Log...' -ForegroundColor DarkGray # Fallback: query Windows Event Log for WU-related events $events = Get-WinEvent -LogName 'System' -ErrorAction SilentlyContinue | Where-Object { $_.ProviderName -match 'WindowsUpdate|Windows Update|wuauserv' } | Select-Object -Last 500 if (-not $events) { Write-Host ' No Windows Update events found in Event Log.' -ForegroundColor Yellow return } $results = $events | ForEach-Object { [PSCustomObject]@{ Timestamp = $_.TimeCreated Level = $_.LevelDisplayName Source = $_.ProviderName EventId = $_.Id Message = ($_.Message -split "`n")[0].Trim() KB = if ($_.Message -match '(KB\d+)') { $Matches[1] } else { '' } Raw = $_.Message } } return Format-WULogResults -Results $results -Level $Level -KB $KB -Last $Last -ExportPath $ExportPath } } # ── Parse raw log lines ────────────────────────────────────────────────── Write-Host " Parsing $($rawLines.Count) log lines..." -ForegroundColor DarkGray $results = $rawLines | ForEach-Object { $line = $_ # WU log format: YYYY/MM/DD HH:MM:SS.mmm ComponentName Level Message if ($line -match '^(\d{4}/\d{2}/\d{2}\s+\d{2}:\d{2}:\d{2}).*?\s+(ERROR|WARNING|INFO|FATAL)\s+(.+)$') { $ts = try { [datetime]::ParseExact($Matches[1],'yyyy/MM/dd HH:mm:ss',$null) } catch { $null } $lvl = $Matches[2] $msg = $Matches[3].Trim() $kbRef = if ($msg -match '(KB\d+)') { $Matches[1] } else { '' } [PSCustomObject]@{ Timestamp = $ts Level = $lvl Source = 'WULog' EventId = '' Message = if ($msg.Length -gt 200) { $msg.Substring(0,197) + '...' } else { $msg } KB = $kbRef Raw = $line } } # Also catch lines with 'FAILED' or 'error' even if format differs elseif ($line -match '(FAILED|error 0x[0-9a-fA-F]+|Install failed)') { [PSCustomObject]@{ Timestamp = $null Level = 'ERROR' Source = 'WULog' EventId = '' Message = $line.Trim() KB = if ($line -match '(KB\d+)') { $Matches[1] } else { '' } Raw = $line } } } | Where-Object { $_ -ne $null } return Format-WULogResults -Results $results -Level $Level -KB $KB -Last $Last -ExportPath $ExportPath } function Format-WULogResults { param($Results, $Level, $KB, $Last, $ExportPath) # Apply filters if ($Level -ne 'All') { $Results = $Results | Where-Object { $_.Level -eq $Level.ToUpper() } } if ($KB) { $kbNum = $KB -replace 'KB','' $Results = $Results | Where-Object { $_.KB -match $kbNum -or $_.Message -match $kbNum } } $Results = $Results | Where-Object { $_ -ne $null } | Select-Object -Last $Last if (-not $Results -or $Results.Count -eq 0) { Write-Host ' No matching log entries found.' -ForegroundColor DarkGray return } Write-Host " Showing $($Results.Count) event(s):`n" -ForegroundColor White $Results | ForEach-Object { $color = switch ($_.Level) { 'ERROR' { 'Red' } 'FATAL' { 'Red' } 'WARNING' { 'Yellow' } default { 'DarkGray' } } $ts = if ($_.Timestamp) { $_.Timestamp.ToString('yyyy-MM-dd HH:mm:ss') } else { '(no timestamp) ' } $kb = if ($_.KB) { " [$($_.KB)]" } else { '' } Write-Host " $ts [$($_.Level,-7)]$kb" -ForegroundColor $color Write-Host " $($_.Message)" -ForegroundColor $(if ($color -eq 'DarkGray') { 'White' } else { $color }) Write-Host '' } if ($ExportPath) { $Results | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Host " Exported to: $ExportPath" -ForegroundColor Cyan } Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return $Results } # --- Source: Install-SpecificKB.ps1 -------------------------------------- #Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS Downloads and installs a specific KB directly from Microsoft Update Catalog. .DESCRIPTION Install-SpecificKB pulls a named KB article from the Microsoft Update Catalog and installs it on the local machine, bypassing the normal WU pending-queue. Useful for: - Emergency security patches that haven't reached WU yet - Applying a specific KB on a machine with restricted WU access - Testing a particular update in isolation - Re-installing a KB that was rolled back Process: 1. Query the WU COM API to see if the KB is available via WU 2. If found via WU — install directly through WU API (cleanest method) 3. If not found — scrape Microsoft Update Catalog for the .msu/.cab URL, download to TEMP, and install via wusa.exe / DISM .PARAMETER KB The KB article ID to install. Example: KB5034441 or 5034441 .PARAMETER AutoReboot Automatically reboot after install if required. .PARAMETER DownloadOnly Download the update but do not install it. .PARAMETER DownloadPath Folder to save the downloaded update file. Defaults to $env:TEMP\PSWinAdmin. .OUTPUTS [PSCustomObject] — result with KB, Method, Success, RebootRequired, FilePath .EXAMPLE Install-SpecificKB -KB KB5034441 Installs KB5034441 via the cleanest available method. .EXAMPLE Install-SpecificKB -KB KB5034441 -DownloadOnly -DownloadPath "C:\Patches" Downloads KB5034441 to C:\Patches without installing. .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS, Run as Administrator Note : Catalog scraping may break if Microsoft changes their HTML. WU API method is always preferred when available. #> function Install-SpecificKB { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string]$KB, [switch]$AutoReboot, [switch]$DownloadOnly, [string]$DownloadPath = "$env:TEMP\PSWinAdmin" ) # Normalise KB format $kbNum = $KB -replace 'KB', '' $kbFull = "KB$kbNum" $arch = if ([System.Environment]::Is64BitOperatingSystem) { 'x64' } else { 'x86' } Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' INSTALL SPECIFIC KB ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan Write-Host " KB : $kbFull" Write-Host " Arch : $arch" Write-Host " Mode : $(if ($DownloadOnly) { 'Download only' } else { 'Download and install' })" $result = [PSCustomObject]@{ KB = $kbFull Method = 'Unknown' Success = $false RebootRequired = $false FilePath = '' Message = '' } # ── Method 1: WU COM API ──────────────────────────────────────────────── Write-Host "`n [1/2] Checking Windows Update API..." -ForegroundColor Yellow try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $search = $searcher.Search("IsInstalled=0 and IsHidden=0") $match = $search.Updates | Where-Object { $_.KBArticleIDs -contains $kbNum } if ($match) { Write-Host " Found via WU API — installing via Windows Update..." -ForegroundColor Green $result.Method = 'WU API' if (-not $DownloadOnly) { $coll = New-Object -ComObject Microsoft.Update.UpdateColl $match | ForEach-Object { $coll.Add($_) | Out-Null } # Download $dl = $session.CreateUpdateDownloader() $dl.Updates = $coll $dl.Download() | Out-Null # Install $inst = $session.CreateUpdateInstaller() $inst.Updates = $coll $instResult = $inst.Install() $codeText = switch ($instResult.ResultCode) { 2 { 'Succeeded' } 3 { 'Succeeded (Reboot Required)' } 4 { 'Failed' } 5 { 'Aborted' } default { "Code $($instResult.ResultCode)" } } $result.Success = ($instResult.ResultCode -in 2,3) $result.RebootRequired = $instResult.RebootRequired $result.Message = $codeText $color = if ($result.Success) { 'Green' } else { 'Red' } Write-Host " Result: $codeText" -ForegroundColor $color } else { Write-Host ' -DownloadOnly: Skipping install.' -ForegroundColor DarkGray $result.Success = $true $result.Message = 'Downloaded via WU API (not installed)' } goto_done # PowerShell doesn't have goto — we use a flag below } else { Write-Host " $kbFull not found in WU queue. Trying Update Catalog..." -ForegroundColor DarkGray } } catch { Write-Host " WU API error: $($_.Exception.Message)" -ForegroundColor DarkGray } # ── Method 2: Update Catalog scrape ───────────────────────────────────── if (-not $result.Success -or $DownloadOnly) { Write-Host " [2/2] Querying Microsoft Update Catalog for $kbFull..." -ForegroundColor Yellow try { $searchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$kbFull" $response = Invoke-WebRequest -Uri $searchUrl -UseBasicParsing -TimeoutSec 15 -ErrorAction Stop $result.Method = 'Update Catalog' # Parse update GUIDs from the catalog response $guids = [regex]::Matches($response.Content, "goToDetails\('([a-f0-9\-]{36})'\)") | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique if (-not $guids) { Write-Host " $kbFull not found in Update Catalog." -ForegroundColor Red $result.Message = 'Not found in Update Catalog' return $result } Write-Host " Found $($guids.Count) package(s) in catalog. Selecting best match for $arch..." -ForegroundColor DarkGray # Get download link for the right architecture $downloadUrl = $null foreach ($guid in $guids) { $detailUrl = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx" $body = "updateIDs=[{'size':0,'languages':'','uidInfo':'$guid','updateID':'$guid'}]&updateIDsBlockedForImport=&wsusApiPresent=&contentImport=&sku=&serverName=&ssl=&portNumber=&version=" try { $dlPage = Invoke-WebRequest -Uri $detailUrl -Method Post -Body $body -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop $urls = [regex]::Matches($dlPage.Content, 'https://[^"'']+\.(msu|cab)') | ForEach-Object { $_.Value } | Select-Object -Unique $archUrl = $urls | Where-Object { $_ -match $arch } | Select-Object -First 1 if (-not $archUrl) { $archUrl = $urls | Select-Object -First 1 } if ($archUrl) { $downloadUrl = $archUrl; break } } catch {} } if (-not $downloadUrl) { Write-Host ' Could not resolve a download URL from the catalog.' -ForegroundColor Red $result.Message = 'No download URL found in catalog' return $result } # Download $null = New-Item -ItemType Directory -Path $DownloadPath -Force $fileName = Split-Path $downloadUrl -Leaf $filePath = Join-Path $DownloadPath $fileName $result.FilePath = $filePath Write-Host " Downloading: $fileName" -ForegroundColor DarkGray Write-Host " From : $downloadUrl" -ForegroundColor DarkGray $wc = New-Object System.Net.WebClient $wc.DownloadFile($downloadUrl, $filePath) Write-Host " Saved to : $filePath" -ForegroundColor Green if ($DownloadOnly) { $result.Success = $true $result.Message = "Downloaded to $filePath" Write-Host ' -DownloadOnly: Not installing.' -ForegroundColor DarkGray } else { Write-Host ' Installing...' -ForegroundColor Yellow if ($filePath -match '\.msu$') { $wusaArgs = @("/quiet", "/norestart", $filePath) $proc = Start-Process -FilePath 'wusa.exe' -ArgumentList $wusaArgs -Wait -PassThru $result.Success = ($proc.ExitCode -in 0, 3010) $result.RebootRequired = ($proc.ExitCode -eq 3010) $result.Message = "wusa.exe exit code: $($proc.ExitCode)" } elseif ($filePath -match '\.cab$') { $dismArgs = @("/Online", "/Add-Package", "/PackagePath:$filePath", "/Quiet", "/NoRestart") $proc = Start-Process -FilePath 'DISM.exe' -ArgumentList $dismArgs -Wait -PassThru $result.Success = ($proc.ExitCode -in 0, 3010) $result.RebootRequired = ($proc.ExitCode -eq 3010) $result.Message = "DISM exit code: $($proc.ExitCode)" } $color = if ($result.Success) { 'Green' } else { 'Red' } Write-Host " $($result.Message)" -ForegroundColor $color } } catch { Write-Host " Catalog error: $($_.Exception.Message)" -ForegroundColor Red $result.Message = $_.Exception.Message return $result } } # Reboot handling if ($result.RebootRequired -and $AutoReboot -and -not $DownloadOnly) { Write-Host "`n Reboot required. Rebooting in 60 seconds..." -ForegroundColor Yellow Start-Sleep -Seconds 60 Restart-Computer -Force } elseif ($result.RebootRequired) { Write-Host "`n REBOOT REQUIRED to complete the update." -ForegroundColor Yellow } Write-Host '' Write-Host " KB : $($result.KB)" Write-Host " Method : $($result.Method)" Write-Host " Success : $($result.Success)" -ForegroundColor $(if ($result.Success) { 'Green' } else { 'Red' }) Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return $result } # --- Source: Invoke-WindowsUpdate.ps1 ------------------------------------ #Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS Installs pending Windows Updates — all, by category, or specific KBs. .DESCRIPTION Invoke-WindowsUpdate uses the Windows Update COM API to search for, download, and install updates without requiring PSWindowsUpdate. Modes of operation: - Default : Install all available, non-hidden updates - -KBArticle : Install one or more specific KBs (e.g. KB5034441) - -Category : Filter by category (Security, Critical, etc.) - -WhatIf : List what would be installed without installing The function handles the full WU pipeline: Search -> Filter -> Download -> Install -> Report Reboot behaviour is controlled by -AutoReboot and -RebootDelay. .PARAMETER KBArticle One or more KB IDs to install. Example: -KBArticle KB5034441,KB5031445 .PARAMETER Category Filter updates by category name substring. Examples: 'Security', 'Critical', 'Definition', 'Driver' .PARAMETER AutoReboot Automatically reboot after install if required. Default: does NOT reboot (reports reboot needed instead). .PARAMETER RebootDelay Seconds to wait before rebooting when -AutoReboot is set. Default: 60. .PARAMETER NoConfirm Skip confirmation prompt before installing. .OUTPUTS [PSCustomObject[]] — one result object per update attempted. .EXAMPLE Invoke-WindowsUpdate Shows pending updates and installs all of them after confirmation. .EXAMPLE Invoke-WindowsUpdate -KBArticle KB5034441 -NoConfirm Silently installs a specific KB. .EXAMPLE Invoke-WindowsUpdate -Category Security -AutoReboot -RebootDelay 120 Installs all Security updates and reboots after 120 seconds if required. .EXAMPLE Invoke-WindowsUpdate -WhatIf Lists what would be installed without taking any action. .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS, Run as Administrator Note : Does not require PSWindowsUpdate module #> function Invoke-WindowsUpdate { [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject[]])] param( [Parameter()] [string[]]$KBArticle, [Parameter()] [string]$Category, [switch]$AutoReboot, [ValidateRange(10, 3600)] [int]$RebootDelay = 60, [switch]$NoConfirm ) Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' INVOKE WINDOWS UPDATE ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan # Build search query $query = 'IsInstalled=0 and IsHidden=0' Write-Host "`n[1/4] Searching for available updates..." -ForegroundColor Yellow $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() try { $searchResult = $searcher.Search($query) } catch { Write-Host " ERROR: Update search failed — $($_.Exception.Message)" -ForegroundColor Red Write-Host ' Try running Invoke-WURepair first.' -ForegroundColor DarkGray return } $updates = $searchResult.Updates # Apply KB filter if ($KBArticle) { $kbNormalised = $KBArticle | ForEach-Object { $_ -replace 'KB','',1; "KB$($_ -replace 'KB','')" } | Select-Object -Unique $updates = $updates | Where-Object { $u = $_ $kbNormalised | Where-Object { $u.KBArticleIDs -contains ($_ -replace 'KB','') } } } # Apply category filter if ($Category) { $updates = $updates | Where-Object { ($_.Categories | ForEach-Object { $_.Name }) -match $Category } } if ($updates.Count -eq 0) { Write-Host ' No matching updates found. System may already be up to date.' -ForegroundColor Green return } # List what we found Write-Host " Found $($updates.Count) update(s):" -ForegroundColor White $updates | ForEach-Object { $kb = if ($_.KBArticleIDs.Count -gt 0) { "KB$($_.KBArticleIDs[0])" } else { 'N/A' } $size = [math]::Round($_.MaxDownloadSize / 1MB, 1) Write-Host (" {0,-12} {1,-65} {2} MB" -f $kb, ($_.Title.Substring(0, [Math]::Min($_.Title.Length, 64))), $size) -ForegroundColor DarkGray } if ($WhatIfPreference) { Write-Host "`n -WhatIf specified — no changes made." -ForegroundColor Yellow return } # Confirm if (-not $NoConfirm) { Write-Host '' $answer = Read-Host " Install $($updates.Count) update(s)? (Y/N)" if ($answer -notmatch '^[Yy]') { Write-Host ' Aborted.' -ForegroundColor DarkGray return } } $results = [System.Collections.Generic.List[PSCustomObject]]::new() # Download Write-Host "`n[2/4] Downloading..." -ForegroundColor Yellow $toDownload = New-Object -ComObject Microsoft.Update.UpdateColl $updates | Where-Object { -not $_.IsDownloaded } | ForEach-Object { $toDownload.Add($_) | Out-Null } if ($toDownload.Count -gt 0) { $downloader = $session.CreateUpdateDownloader() $downloader.Updates = $toDownload try { $dlResult = $downloader.Download() Write-Host " Download result code: $($dlResult.ResultCode)" -ForegroundColor DarkGray } catch { Write-Host " Download error: $($_.Exception.Message)" -ForegroundColor Red } } else { Write-Host ' All updates already downloaded.' -ForegroundColor DarkGray } # Install Write-Host "`n[3/4] Installing..." -ForegroundColor Yellow $toInstall = New-Object -ComObject Microsoft.Update.UpdateColl $updates | ForEach-Object { $toInstall.Add($_) | Out-Null } $installer = $session.CreateUpdateInstaller() $installer.Updates = $toInstall try { $installResult = $installer.Install() } catch { Write-Host " Install error: $($_.Exception.Message)" -ForegroundColor Red return } # Collect results Write-Host "`n[4/4] Results:" -ForegroundColor Yellow $rebootNeeded = $false for ($i = 0; $i -lt $updates.Count; $i++) { $u = $updates.Item($i) $res = $installResult.GetUpdateResult($i) $kb = if ($u.KBArticleIDs.Count -gt 0) { "KB$($u.KBArticleIDs[0])" } else { 'N/A' } $codeText = switch ($res.ResultCode) { 0 { 'Not Started' } 1 { 'In Progress' } 2 { 'Succeeded' } 3 { 'Succeeded*' } # reboot required 4 { 'Failed' } 5 { 'Aborted' } default { "Code $($res.ResultCode)" } } $needsReboot = ($res.RebootRequired -or $res.ResultCode -eq 3) if ($needsReboot) { $rebootNeeded = $true } $color = switch ($res.ResultCode) { 2 { 'Green' } 3 { 'Yellow' } 4 { 'Red' } 5 { 'Red' } default { 'White' } } $rebootText = if ($needsReboot) { ' [REBOOT REQ]' } else { '' } Write-Host (" {0,-12} {1,-55} {2}{3}" -f $kb, ($u.Title.Substring(0,[Math]::Min($u.Title.Length,54))), $codeText, $rebootText) -ForegroundColor $color $results.Add([PSCustomObject]@{ KB = $kb Title = $u.Title ResultCode = $res.ResultCode Result = $codeText RebootNeeded = $needsReboot HResult = $res.HResult }) } # Reboot handling Write-Host '' if ($rebootNeeded) { if ($AutoReboot) { Write-Host " Reboot required. Rebooting in $RebootDelay seconds..." -ForegroundColor Yellow Write-Host ' Press Ctrl+C to cancel.' -ForegroundColor DarkGray Start-Sleep -Seconds $RebootDelay Restart-Computer -Force } else { Write-Host ' REBOOT REQUIRED to complete installation.' -ForegroundColor Yellow Write-Host ' Run: Restart-Computer -Force or use -AutoReboot next time.' -ForegroundColor DarkGray } } else { Write-Host ' No reboot required.' -ForegroundColor Green } Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return $results.ToArray() } # --- Source: Invoke-WURepair.ps1 ----------------------------------------- #Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS Full Windows Update repair — resets components, runs SFC and DISM. .DESCRIPTION Invoke-WURepair performs a comprehensive Windows Update component repair, working through a layered set of fixes in order of least to most invasive: 1. Stop WU-related services 2. Re-register Windows Update COM DLLs 3. Reset BITS transfer queue 4. Clear SoftwareDistribution and catroot2 caches 5. Repair WinSock and WinHTTP proxy settings 6. Restart all services 7. Run System File Checker (SFC /scannow) 8. Run DISM CheckHealth 9. Run DISM ScanHealth 10. Run DISM RestoreHealth (downloads and replaces corrupt files) Each stage is logged and timed. Results are returned as a structured object so the output can be used in automation pipelines. Use this when: - Windows Update throws persistent errors (0x80070002, 0x8024402F, etc.) - Updates download but fail to install - WU is stuck at "Checking for updates..." - SFC or DISM previously flagged corruption .PARAMETER SkipSFC Skip the SFC /scannow step (saves ~10 minutes on large installs). .PARAMETER SkipDISM Skip all DISM health checks (saves significant time; use when offline). .PARAMETER NoConfirm Skip the confirmation prompt for automated use. .OUTPUTS [PSCustomObject] with per-stage Success/Output detail. .EXAMPLE Invoke-WURepair Full repair with confirmation prompt. .EXAMPLE Invoke-WURepair -SkipDISM -NoConfirm Fast repair (no DISM), no prompt. Good for quick fixes. .NOTES Author : PSWinAdmin Version : 1.0.0 Requires : PowerShell 5.1+, Windows OS, Run as Administrator Duration : 5–30 minutes depending on options and system state #> function Invoke-WURepair { [CmdletBinding()] [OutputType([PSCustomObject])] param( [switch]$SkipSFC, [switch]$SkipDISM, [switch]$NoConfirm ) if (-not $NoConfirm) { Write-Host '' Write-Host ' Invoke-WURepair will stop WU services, clear caches, re-register DLLs,' -ForegroundColor Yellow Write-Host ' and optionally run SFC and DISM. This may take 5-30 minutes.' -ForegroundColor Yellow $answer = Read-Host ' Continue? (Y/N)' if ($answer -notmatch '^[Yy]') { Write-Host ' Aborted.' -ForegroundColor DarkGray return } } $stages = [System.Collections.Generic.List[PSCustomObject]]::new() $overall = $true $start = Get-Date function Invoke-Stage { param([string]$Name, [scriptblock]$Action) Write-Host "`n [$Name]" -ForegroundColor Yellow $stageStart = Get-Date try { $output = & $Action $elapsed = ((Get-Date) - $stageStart).TotalSeconds Write-Host " Done ($( [math]::Round($elapsed,1) )s)" -ForegroundColor Green return [PSCustomObject]@{ Stage=$Name; Success=$true; Output=$output; ElapsedSec=[math]::Round($elapsed,1) } } catch { $elapsed = ((Get-Date) - $stageStart).TotalSeconds Write-Host " WARNING: $($_.Exception.Message)" -ForegroundColor Red return [PSCustomObject]@{ Stage=$Name; Success=$false; Output=$_.Exception.Message; ElapsedSec=[math]::Round($elapsed,1) } } } Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' WINDOWS UPDATE REPAIR ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan Write-Host " Started: $($start.ToString('yyyy-MM-dd HH:mm:ss'))" $services = @('wuauserv','cryptsvc','bits','msiserver','appidsvc','trustedinstaller') # Stage 1 — Stop services $stages.Add((Invoke-Stage -Name 'Stop WU Services' -Action { foreach ($svc in $services) { $s = Get-Service $svc -ErrorAction SilentlyContinue if ($s -and $s.Status -eq 'Running') { Stop-Service $svc -Force -ErrorAction SilentlyContinue Write-Host " Stopped: $svc" -ForegroundColor DarkGray } } Start-Sleep -Seconds 3 })) # Stage 2 — Re-register DLLs $stages.Add((Invoke-Stage -Name 'Re-register DLLs' -Action { $dlls = @( 'wuapi.dll','wuaueng.dll','wuaueng1.dll','wucltui.dll', 'wups.dll','wups2.dll','wuweb.dll','wucltux.dll', 'muweb.dll','wuwebv.dll','qmgr.dll','qmgrprxy.dll', 'msxml.dll','msxml3.dll','msxml6.dll', 'softpub.dll','wintrust.dll','initpki.dll', 'oleaut32.dll','ole32.dll','shell32.dll','atl.dll' ) $count = 0 foreach ($dll in $dlls) { $p = "$env:SystemRoot\System32\$dll" if (Test-Path $p) { regsvr32.exe /s $p; $count++ } } Write-Host " Registered $count DLLs" -ForegroundColor DarkGray })) # Stage 3 — Reset BITS $stages.Add((Invoke-Stage -Name 'Reset BITS Queue' -Action { $null = bitsadmin /reset /allusers 2>&1 Write-Host ' BITS queue cleared' -ForegroundColor DarkGray })) # Stage 4 — Clear caches $stages.Add((Invoke-Stage -Name 'Clear WU Cache' -Action { $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' foreach ($path in @("$env:SystemRoot\SoftwareDistribution","$env:SystemRoot\System32\catroot2")) { if (Test-Path $path) { $bak = "$path.bak_$timestamp" try { Rename-Item $path $bak -ErrorAction Stop Write-Host " Renamed: $path" -ForegroundColor DarkGray } catch { Remove-Item $path -Recurse -Force -ErrorAction SilentlyContinue Write-Host " Removed: $path" -ForegroundColor DarkGray } } } })) # Stage 5 — Network reset $stages.Add((Invoke-Stage -Name 'Reset WinSock / WinHTTP' -Action { netsh winsock reset | Out-Null netsh winhttp reset proxy | Out-Null Write-Host ' WinSock and proxy reset' -ForegroundColor DarkGray })) # Stage 6 — Restart services $stages.Add((Invoke-Stage -Name 'Restart Services' -Action { Start-Sleep -Seconds 2 $startOrder = @('cryptsvc','bits','msiserver','wuauserv') foreach ($svc in $startOrder) { try { Start-Service $svc -ErrorAction SilentlyContinue Write-Host " Started: $svc" -ForegroundColor DarkGray } catch {} } })) # Stage 7 — SFC if (-not $SkipSFC) { $stages.Add((Invoke-Stage -Name 'System File Checker (SFC)' -Action { Write-Host ' Running sfc /scannow — this may take several minutes...' -ForegroundColor DarkGray $sfcOutput = & "$env:SystemRoot\System32\sfc.exe" /scannow 2>&1 | Out-String if ($sfcOutput -match 'did not find any integrity violations') { Write-Host ' SFC: No integrity violations found.' -ForegroundColor Green } elseif ($sfcOutput -match 'successfully repaired') { Write-Host ' SFC: Corruption found and repaired.' -ForegroundColor Yellow } elseif ($sfcOutput -match 'unable to fix') { Write-Host ' SFC: Corruption found but could not repair — run DISM RestoreHealth.' -ForegroundColor Red } $sfcOutput })) } # Stage 8, 9, 10 — DISM if (-not $SkipDISM) { $stages.Add((Invoke-Stage -Name 'DISM CheckHealth' -Action { Write-Host ' Running DISM /CheckHealth...' -ForegroundColor DarkGray $out = & DISM /Online /Cleanup-Image /CheckHealth 2>&1 | Out-String Write-Host ($out | Select-String 'The component store' | Select-Object -First 1) -ForegroundColor DarkGray $out })) $stages.Add((Invoke-Stage -Name 'DISM ScanHealth' -Action { Write-Host ' Running DISM /ScanHealth (may take 5-10 minutes)...' -ForegroundColor DarkGray $out = & DISM /Online /Cleanup-Image /ScanHealth 2>&1 | Out-String Write-Host ($out | Select-String 'No component store corruption detected|corruption was detected' | Select-Object -First 1) -ForegroundColor DarkGray $out })) $stages.Add((Invoke-Stage -Name 'DISM RestoreHealth' -Action { Write-Host ' Running DISM /RestoreHealth (downloads from Windows Update — may take 10-20 min)...' -ForegroundColor DarkGray $out = & DISM /Online /Cleanup-Image /RestoreHealth 2>&1 | Out-String Write-Host ($out | Select-String 'The restore operation completed|operation completed successfully|errors' | Select-Object -First 1) -ForegroundColor DarkGray $out })) } $elapsed = [math]::Round(((Get-Date) - $start).TotalMinutes, 1) $failed = $stages | Where-Object { -not $_.Success } $overall = ($failed.Count -eq 0) Write-Host '' Write-Host '=====================================' -ForegroundColor Cyan Write-Host ' SUMMARY ' -ForegroundColor Cyan Write-Host '=====================================' -ForegroundColor Cyan Write-Host " Total time : $elapsed minutes" Write-Host " Stages run : $($stages.Count)" Write-Host " Passed : $($stages.Count - $failed.Count)" -ForegroundColor Green if ($failed.Count -gt 0) { Write-Host " Failed : $($failed.Count)" -ForegroundColor Red $failed | ForEach-Object { Write-Host " - $($_.Stage)" -ForegroundColor Red } } Write-Host '' if ($overall) { Write-Host ' Repair complete. Try Windows Update now.' -ForegroundColor Green Write-Host ' Run Get-PatchStatus to check for pending updates.' -ForegroundColor DarkGray } else { Write-Host ' Repair completed with warnings. Review output above.' -ForegroundColor Yellow } Write-Host '=====================================' -ForegroundColor Cyan Write-Host '' return [PSCustomObject]@{ Success = $overall ElapsedMin = $elapsed StagesRun = $stages.Count StageResults = $stages } } |