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 '<', '<' -replace '>', '>' $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 '<', '<' -replace '>', '>' $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 } |