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) &nbsp;|&nbsp;
      $($os.Caption) ($($os.Version)) &nbsp;|&nbsp;
      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
    }
}