Vulnerability/Test-PatchCompliance.ps1

function Test-PatchCompliance {
    <#
    .SYNOPSIS
        Tests Windows patch compliance and identifies missing security updates
    .DESCRIPTION
        Scans for missing Windows updates, security patches, and critical updates.
        Identifies compliance status and provides patch installation recommendations.
    .PARAMETER IncludeOptional
        Include optional updates in the scan
    .PARAMETER DaysOld
        Flag updates older than specified days as critical (default: 30)
    .PARAMETER OutputPath
        Path to save patch compliance report
    .PARAMETER Format
        Report format: HTML, JSON, CSV, or All
    .EXAMPLE
        Test-PatchCompliance
        Test-PatchCompliance -IncludeOptional -DaysOld 60 -OutputPath "C:\Reports"
    .OUTPUTS
        PSCustomObject with patch compliance results
    #>

    
    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$IncludeOptional,
        
        [Parameter()]
        [int]$DaysOld = 30,
        
        [Parameter()]
        [string]$OutputPath,
        
        [Parameter()]
        [ValidateSet('HTML', 'JSON', 'CSV', 'All')]
        [string]$Format = 'HTML'
    )
    
    begin {
        Write-Host "=== PATCH COMPLIANCE TEST ===" -ForegroundColor Cyan
        Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow
        Write-Host "Include Optional Updates: $(if($IncludeOptional){'Yes'}else{'No'})" -ForegroundColor Yellow
        Write-Host "Critical Age Threshold: $DaysOld days" -ForegroundColor Yellow
        
        $osInfo = $null
        try { $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop } catch { $osInfo = $null }

        $compliance = [PSCustomObject]@{
            ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            ComputerName = $env:COMPUTERNAME
            OSVersion = $(if ($null -ne $osInfo) { $osInfo.Caption } else { $null })
            LastBootTime = $(if ($null -ne $osInfo) { $osInfo.LastBootUpTime } else { $null })
            WindowsUpdateEnabled = $false
            AutoUpdateEnabled = $false
            InstalledUpdates = @()
            MissingUpdates = @()
            Summary = @{
                TotalInstalled = 0
                TotalMissing = 0
                CriticalMissing = 0
                ImportantMissing = 0
                ModerateMissing = 0
                LowMissing = 0
                OldestMissingDays = 0
                ComplianceScore = 0
            }
            Recommendations = @()
        }
    }
    
    process {
        try {
            # 1. CHECK WINDOWS UPDATE SERVICE
            Write-Host "`n[1/5] Checking Windows Update service..." -ForegroundColor Cyan
            
            $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
            if ($wuService) {
                $compliance.WindowsUpdateEnabled = ($wuService.Status -eq 'Running')
                Write-Host " Windows Update Service: $($wuService.Status)" -ForegroundColor $(
                    if ($wuService.Status -eq 'Running') { 'Green' } else { 'Red' }
                )
            }
            
            # Check auto-update settings
            try {
                $auSettings = (New-Object -ComObject Microsoft.Update.AutoUpdate).Settings
                $compliance.AutoUpdateEnabled = ($auSettings.NotificationLevel -ge 3)
                Write-Host " Auto-Update Level: $($auSettings.NotificationLevel)" -ForegroundColor $(
                    if ($auSettings.NotificationLevel -ge 3) { 'Green' } else { 'Yellow' }
                )
            }
            catch {
                Write-Warning "Could not determine auto-update settings"
            }
            
            # 2. GET INSTALLED UPDATES
            Write-Host "`n[2/5] Retrieving installed updates..." -ForegroundColor Cyan
            
            try {
                $session = New-Object -ComObject Microsoft.Update.Session
                $searcher = $session.CreateUpdateSearcher()
                
                Write-Host " Querying update history (this may take a moment)..." -ForegroundColor Gray
                
                $historyCount = $searcher.GetTotalHistoryCount()
                if ($historyCount -gt 0) {
                    $history = $searcher.QueryHistory(0, [Math]::Min($historyCount, 500))
                    
                    foreach ($update in $history) {
                        if ($update.ResultCode -eq 2) {  # Succeeded
                            $compliance.InstalledUpdates += [PSCustomObject]@{
                                Title = $update.Title
                                Date = $update.Date
                                KB = if ($update.Title -match 'KB(\d+)') { $matches[1] } else { 'N/A' }
                            }
                        }
                    }
                    
                    $compliance.Summary.TotalInstalled = $compliance.InstalledUpdates.Count
                    Write-Host " Found $($compliance.Summary.TotalInstalled) installed updates" -ForegroundColor Green
                }
            }
            catch {
                Write-Warning "Error retrieving installed updates: $_"
            }
            
            # 3. SCAN FOR MISSING UPDATES
            Write-Host "`n[3/5] Scanning for missing updates..." -ForegroundColor Cyan
            Write-Host " This may take several minutes..." -ForegroundColor Gray
            
            try {
                $session = New-Object -ComObject Microsoft.Update.Session
                $searcher = $session.CreateUpdateSearcher()
                
                # Search criteria
                $criteria = "IsInstalled=0"
                if (-not $IncludeOptional) {
                    $criteria += " and Type='Software'"
                }
                
                Write-Host " Searching with criteria: $criteria" -ForegroundColor Gray
                $searchResult = $searcher.Search($criteria)
                
                $updateCount = $searchResult.Updates.Count
                Write-Host " Found $updateCount missing update(s)" -ForegroundColor $(
                    if ($updateCount -eq 0) { 'Green' } else { 'Yellow' }
                )
                
                # Process missing updates
                foreach ($update in $searchResult.Updates) {
                    # Determine severity - FIXED: Safe property access
                    $severity = 'Low'
                    if ($null -ne $update.PSObject.Properties['MsrcSeverity'] -and $update.MsrcSeverity) {
                        $severity = $update.MsrcSeverity
                    }
                    elseif ($update.Title -match 'Critical|Security') {
                        $severity = 'Critical'
                    }
                    elseif ($update.Title -match 'Important') {
                        $severity = 'Important'
                    }
                    
                    # Extract KB number
                    $kbNumber = 'N/A'
                    if ($update.Title -match 'KB(\d+)') {
                        $kbNumber = "KB$($matches[1])"
                    }
                    
                    # Calculate age - FIXED: Safe property access
                    $releaseDate = if ($null -ne $update.PSObject.Properties['LastDeploymentChangeTime']) {
                        $update.LastDeploymentChangeTime
                    } else {
                        $null
                    }
                    
                    $ageInDays = if ($releaseDate) {
                        [Math]::Round(((Get-Date) - $releaseDate).TotalDays)
                    } else { 0 }
                    
                    if ($ageInDays -gt $compliance.Summary.OldestMissingDays) {
                        $compliance.Summary.OldestMissingDays = $ageInDays
                    }
                    
                    # FIXED: Safe property access for all optional properties
                    $description = if ($null -ne $update.PSObject.Properties['Description']) {
                        $update.Description
                    } else {
                        'No description available'
                    }
                    
                    $maxDownloadSize = if ($null -ne $update.PSObject.Properties['MaxDownloadSize']) {
                        [Math]::Round($update.MaxDownloadSize / 1MB, 2)
                    } else {
                        0
                    }
                    
                    $isSuperseded = if ($null -ne $update.PSObject.Properties['IsSuperseded']) {
                        $update.IsSuperseded
                    } else {
                        $false
                    }
                    
                    $rebootRequired = if ($null -ne $update.PSObject.Properties['RebootRequired']) {
                        $update.RebootRequired
                    } else {
                        $false
                    }
                    
                    $categories = if ($null -ne $update.PSObject.Properties['Categories']) {
                        ($update.Categories | ForEach-Object { $_.Name }) -join ', '
                    } else {
                        'Unknown'
                    }
                    
                    $missingUpdate = [PSCustomObject]@{
                        Title = $update.Title
                        KB = $kbNumber
                        Severity = $severity
                        Description = $description
                        ReleaseDate = $releaseDate
                        AgeInDays = $ageInDays
                        Size = $maxDownloadSize
                        IsSuperseded = $isSuperseded
                        RebootRequired = $rebootRequired
                        Categories = $categories
                    }
                    
                    $compliance.MissingUpdates += $missingUpdate
                    
                    # Update counters
                    switch ($severity) {
                        'Critical' { $compliance.Summary.CriticalMissing++ }
                        'Important' { $compliance.Summary.ImportantMissing++ }
                        'Moderate' { $compliance.Summary.ModerateMissing++ }
                        'Low' { $compliance.Summary.LowMissing++ }
                    }
                    
                    # Display critical/important updates
                    if ($severity -in @('Critical', 'Important')) {
                        $color = if ($severity -eq 'Critical') { 'Red' } else { 'Yellow' }
                        Write-Host " [!] $severity - $kbNumber - $($update.Title.Substring(0, [Math]::Min(60, $update.Title.Length)))..." -ForegroundColor $color
                    }
                }
                
                $compliance.Summary.TotalMissing = $compliance.MissingUpdates.Count
            }
            catch {
                Write-Error "Error scanning for missing updates: $_"
                $compliance.Summary.TotalMissing = -1  # Indicates scan failure
            }
            
            # 4. CALCULATE COMPLIANCE SCORE
            Write-Host "`n[4/5] Calculating compliance score..." -ForegroundColor Cyan
            
            $score = 100
            
            # Deduct points for missing updates
            $score -= ($compliance.Summary.CriticalMissing * 15)
            $score -= ($compliance.Summary.ImportantMissing * 10)
            $score -= ($compliance.Summary.ModerateMissing * 5)
            $score -= ($compliance.Summary.LowMissing * 2)
            
            # Deduct points for old missing updates
            if ($compliance.Summary.OldestMissingDays -gt $DaysOld) {
                $score -= 10
            }
            
            # Deduct points if auto-update disabled
            if (-not $compliance.AutoUpdateEnabled) {
                $score -= 10
            }
            
            # Ensure score is between 0-100
            $compliance.Summary.ComplianceScore = [Math]::Max(0, [Math]::Min(100, $score))
            
            Write-Host " Compliance Score: $($compliance.Summary.ComplianceScore)%" -ForegroundColor $(
                if ($compliance.Summary.ComplianceScore -ge 80) { 'Green' }
                elseif ($compliance.Summary.ComplianceScore -ge 60) { 'Yellow' }
                else { 'Red' }
            )
            
            # 5. GENERATE RECOMMENDATIONS
            Write-Host "`n[5/5] Generating recommendations..." -ForegroundColor Cyan
            
            if ($compliance.Summary.CriticalMissing -gt 0) {
                $compliance.Recommendations += "CRITICAL: Install $($compliance.Summary.CriticalMissing) critical security update(s) immediately"
            }
            
            if ($compliance.Summary.ImportantMissing -gt 0) {
                $compliance.Recommendations += "IMPORTANT: Install $($compliance.Summary.ImportantMissing) important update(s) within 7 days"
            }
            
            if ($compliance.Summary.OldestMissingDays -gt $DaysOld) {
                $compliance.Recommendations += "WARNING: Oldest missing update is $($compliance.Summary.OldestMissingDays) days old"
            }
            
            if (-not $compliance.AutoUpdateEnabled) {
                $compliance.Recommendations += "Enable automatic updates for security patches"
            }
            
            if (-not $compliance.WindowsUpdateEnabled) {
                $compliance.Recommendations += "Start Windows Update service: Start-Service wuauserv"
            }
            
            if ($compliance.Summary.TotalMissing -eq 0) {
                $compliance.Recommendations += "System is fully patched - maintain regular update schedule"
            }
            
            $compliance.Recommendations += "Schedule monthly patch maintenance window"
            $compliance.Recommendations += "Test updates in non-production environment first"
            $compliance.Recommendations += "Maintain patch deployment documentation"
            
            # 6. EXPORT REPORTS
            if ($OutputPath) {
                Write-Host "`n[*] Exporting patch compliance reports..." -ForegroundColor Cyan
                
                if (-not (Test-Path $OutputPath)) {
                    New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
                }
                
                $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
                $reportName = "PatchCompliance_$timestamp"
                
                # Export JSON
                if ($Format -in @('JSON', 'All')) {
                    $jsonPath = Join-Path $OutputPath "$reportName.json"
                    $compliance | ConvertTo-Json -Depth 10 | Out-File $jsonPath -Encoding UTF8
                    Write-Host " JSON report saved: $jsonPath" -ForegroundColor Green
                }
                
                # Export CSV (Missing Updates)
                if ($Format -in @('CSV', 'All')) {
                    $csvPath = Join-Path $OutputPath "$reportName.csv"
                    $compliance.MissingUpdates | Export-Csv $csvPath -NoTypeInformation
                    Write-Host " CSV report saved: $csvPath" -ForegroundColor Green
                }
                
                # Export HTML
                if ($Format -in @('HTML', 'All')) {
                    $htmlPath = Join-Path $OutputPath "$reportName.html"
                    $html = Generate-PatchComplianceHTML -Compliance $compliance
                    $html | Out-File $htmlPath -Encoding UTF8
                    Write-Host " HTML report saved: $htmlPath" -ForegroundColor Green
                }
            }
            
        }
        catch {
            Write-Error "Error during patch compliance test: $_"
            throw
        }
    }
    
    end {
        Write-Host "`n=== PATCH COMPLIANCE TEST COMPLETE ===" -ForegroundColor $(
            if ($compliance.Summary.ComplianceScore -ge 80) { 'Green' }
            elseif ($compliance.Summary.ComplianceScore -ge 60) { 'Yellow' }
            else { 'Red' }
        )
        
        Write-Host "`nCompliance Summary:" -ForegroundColor Cyan
        Write-Host " OS Version: $($compliance.OSVersion)" -ForegroundColor White
        Write-Host " Last Boot: $($compliance.LastBootTime)" -ForegroundColor White
        Write-Host " Installed Updates: $($compliance.Summary.TotalInstalled)" -ForegroundColor Green
        Write-Host " Missing Updates: $($compliance.Summary.TotalMissing)" -ForegroundColor $(
            if ($compliance.Summary.TotalMissing -eq 0) { 'Green' } else { 'Yellow' }
        )
        Write-Host " Critical Missing: $($compliance.Summary.CriticalMissing)" -ForegroundColor $(
            if ($compliance.Summary.CriticalMissing -gt 0) { 'Red' } else { 'Gray' }
        )
        Write-Host " Important Missing: $($compliance.Summary.ImportantMissing)" -ForegroundColor $(
            if ($compliance.Summary.ImportantMissing -gt 0) { 'Yellow' } else { 'Gray' }
        )
        Write-Host " Compliance Score: $($compliance.Summary.ComplianceScore)%" -ForegroundColor $(
            if ($compliance.Summary.ComplianceScore -ge 80) { 'Green' }
            elseif ($compliance.Summary.ComplianceScore -ge 60) { 'Yellow' }
            else { 'Red' }
        )
        
        if ($compliance.MissingUpdates.Count -gt 0) {
            Write-Host "`nTop Missing Updates:" -ForegroundColor Cyan
            $compliance.MissingUpdates | 
                Where-Object { $_.Severity -in @('Critical', 'Important') } |
                Sort-Object { 
                    switch ($_.Severity) {
                        'Critical' { 0 }
                        'Important' { 1 }
                        default { 2 }
                    }
                }, AgeInDays -Descending |
                Select-Object -First 5 |
                ForEach-Object {
                    $color = if ($_.Severity -eq 'Critical') { 'Red' } else { 'Yellow' }
                    Write-Host " [$($_.Severity)] $($_.KB) - $($_.AgeInDays) days old" -ForegroundColor $color
                }
        }
        
        if ($compliance.Recommendations.Count -gt 0) {
            Write-Host "`nRecommendations:" -ForegroundColor Cyan
            $compliance.Recommendations | ForEach-Object {
                Write-Host " ! $_" -ForegroundColor Yellow
            }
        }
        
        return $compliance
    }
}

# Helper function to generate HTML report
function Generate-PatchComplianceHTML {
    param($Compliance)
    
    $scoreColor = if($Compliance.Summary.ComplianceScore -ge 80){'safe'}
                  elseif($Compliance.Summary.ComplianceScore -ge 60){'medium'}
                  else{'critical'}
    
    $html = @"
<!DOCTYPE html>
<html>
<head>
    <title>Patch Compliance Report - $($Compliance.ComputerName)</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
        h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
        h2 { color: #34495e; margin-top: 30px; }
        .summary { background: #ecf0f1; padding: 20px; border-radius: 5px; margin: 20px 0; }
        .metric { display: inline-block; margin: 10px 20px 10px 0; }
        .metric-label { font-weight: bold; color: #7f8c8d; }
        .metric-value { font-size: 24px; font-weight: bold; }
        .critical { color: #e74c3c; }
        .high { color: #e67e22; }
        .important { color: #e67e22; }
        .medium { color: #f39c12; }
        .moderate { color: #f39c12; }
        .low { color: #95a5a6; }
        .safe { color: #27ae60; }
        table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        th { background: #3498db; color: white; padding: 12px; text-align: left; }
        td { padding: 10px; border-bottom: 1px solid #ddd; }
        tr:hover { background: #f8f9fa; }
        .severity-badge { padding: 4px 8px; border-radius: 3px; color: white; font-weight: bold; }
        .severity-critical { background: #e74c3c; }
        .severity-important { background: #e67e22; }
        .severity-moderate { background: #f39c12; }
        .severity-low { background: #95a5a6; }
        .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #7f8c8d; font-size: 12px; }
        .compliant { color: #27ae60; font-weight: bold; font-size: 16px; padding: 20px; text-align: center; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Windows Patch Compliance Report</h1>
        <p><strong>Computer:</strong> $($Compliance.ComputerName) | <strong>Scan Date:</strong> $($Compliance.ScanDate)</p>
        <p><strong>OS:</strong> $($Compliance.OSVersion) | <strong>Last Boot:</strong> $($Compliance.LastBootTime)</p>
         
        <div class="summary">
            <h2>Compliance Summary</h2>
            <div class="metric">
                <div class="metric-label">Compliance Score</div>
                <div class="metric-value $scoreColor">$($Compliance.Summary.ComplianceScore)%</div>
            </div>
            <div class="metric">
                <div class="metric-label">Installed Updates</div>
                <div class="metric-value safe">$($Compliance.Summary.TotalInstalled)</div>
            </div>
            <div class="metric">
                <div class="metric-label">Missing Updates</div>
                <div class="metric-value $(if($Compliance.Summary.TotalMissing -gt 0){'critical'}else{'safe'})">
                    $($Compliance.Summary.TotalMissing)
                </div>
            </div>
            <div class="metric">
                <div class="metric-label">Critical Missing</div>
                <div class="metric-value critical">$($Compliance.Summary.CriticalMissing)</div>
            </div>
            <div class="metric">
                <div class="metric-label">Important Missing</div>
                <div class="metric-value important">$($Compliance.Summary.ImportantMissing)</div>
            </div>
        </div>
         
        <h2>Missing Updates</h2>
"@

    
    if ($Compliance.MissingUpdates.Count -gt 0) {
        $html += @"
        <table>
            <tr>
                <th>Severity</th>
                <th>KB</th>
                <th>Title</th>
                <th>Age (Days)</th>
                <th>Size (MB)</th>
                <th>Reboot Required</th>
            </tr>
"@

        
        foreach ($update in ($Compliance.MissingUpdates | Sort-Object { 
            switch ($_.Severity) {
                'Critical' { 0 }
                'Important' { 1 }
                'Moderate' { 2 }
                'Low' { 3 }
            }
        }, AgeInDays -Descending)) {
            $severityClass = "severity-$($update.Severity.ToLower())"
            $title = $update.Title -replace '<', '&lt;' -replace '>', '&gt;'
            $reboot = if ($update.RebootRequired) { 'Yes' } else { 'No' }
            
            $html += @"
            <tr>
                <td><span class="severity-badge $severityClass">$($update.Severity)</span></td>
                <td><strong>$($update.KB)</strong></td>
                <td>$title</td>
                <td>$($update.AgeInDays)</td>
                <td>$($update.Size)</td>
                <td>$reboot</td>
            </tr>
"@

        }
        
        $html += " </table>`n"
    }
    else {
        $html += " <p class=`"compliant`">System is fully patched - no missing updates detected</p>`n"
    }
    
    $html += " `n <h2>Recommendations</h2>`n <ul>`n"
    
    foreach ($rec in $Compliance.Recommendations) {
        $recEscaped = $rec -replace '<', '&lt;' -replace '>', '&gt;'
        $html += " <li>$recEscaped</li>`n"
    }
    
    $currentDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    
    $html += @"
        </ul>
         
        <div class="footer">
            Generated by WindowsSecurityAudit Module | Patch Compliance | $currentDate
        </div>
    </div>
</body>
</html>
"@

    
    return $html
}