functions/System/Drift/Get-UpdateStatusDrift.ps1
|
function Get-UpdateStatusDrift { <# .SYNOPSIS Detects configuration drift in Windows Update settings. .DESCRIPTION Comprehensive Windows Update drift detection supporting multiple security profiles (Basis, Recommended, Strict). Checks automatic update enablement, scheduled install times, reboot behavior, notification levels, and update recency. Supports both local and remote computer analysis with optional detailed output. Logging via Write-Log from Core module. Returns PSCustomObject array with drift findings. .PARAMETER ComputerName Target computer name for drift analysis (default: 'localhost' for local computer). .PARAMETER Profile Security baseline profile: Basis (minimum), Recommended (default), or Strict (maximum hardening). Different profiles enforce different update policies and reboot behaviors. .PARAMETER Detailed Include additional detailed checks (update recency, specific update types). Automatically enabled for Recommended+ profiles in most contexts. .PARAMETER ReportDriftOnly Return only items with DRIFT status, filtering out COMPLIANT items. Useful for compliance reports focusing on deviations. .PARAMETER Credential PowerShell credential object for authenticating remote computer access. Only used when ComputerName is not 'localhost'. .PARAMETER AutoUpdateEnabled Whether automatic updates should be enabled (default: $true for Recommended, varies by profile). .PARAMETER RequireScheduledRestart Enforce scheduled restarts for updates (default: $true for Strict). .PARAMETER MaxDaysSinceLastUpdate Maximum days allowed since last successful update check (default: 7). .EXAMPLE $drifts = Get-UpdateStatusDrift if ($drifts.Count -gt 0) { $drifts | Write-Output } .EXAMPLE $drifts = Get-UpdateStatusDrift -ComputerName SERVER01 -Profile Strict -Credential $cred -Detailed .EXAMPLE Get-UpdateStatusDrift -Profile Basis -ReportDriftOnly | Export-Csv -Path drifts.csv .NOTES Depends on Write-Log (Core) for logging. Applies to Windows Server 2016+ and Windows 10+. Supports three profiles: Basis (3 checks), Recommended (5+ checks), Strict (7+ checks with detailed). Returns PSCustomObject array with drift findings including Category, Setting, Expected, Actual, Status, and Severity. #> [CmdletBinding(SupportsShouldProcess)] param( [string]$ComputerName = 'localhost', [ValidateSet('Basis', 'Recommended', 'Strict')] [string]$Profile = 'Recommended', [switch]$Detailed, [switch]$ReportDriftOnly, [pscredential]$Credential, [bool]$AutoUpdateEnabled = $true, [bool]$RequireScheduledRestart = $false, [ValidateRange(1, 365)] [int]$MaxDaysSinceLastUpdate = 7 ) $findings = @() try { if (-not $PSCmdlet.ShouldProcess($ComputerName, "Check Windows Update Drift")) { return $findings } # Build remote execution parameters $remoteParams = @{ ErrorAction = 'SilentlyContinue' } if ($ComputerName -ne 'localhost') { $remoteParams['ComputerName'] = $ComputerName if ($Credential) { $remoteParams['Credential'] = $Credential } } # Profile-based configuration switch ($Profile) { 'Basis' { $checkAutoUpdate = $true $checkScheduledInstall = $false $checkNotificationLevel = $false $checkUpdateRecency = $false $checkRebootBehavior = $false } 'Recommended' { $checkAutoUpdate = $true $checkScheduledInstall = $true $checkNotificationLevel = $true $checkUpdateRecency = $Detailed $checkRebootBehavior = $Detailed } 'Strict' { $checkAutoUpdate = $true $checkScheduledInstall = $true $checkNotificationLevel = $true $checkUpdateRecency = $true $checkRebootBehavior = $true } } $auPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" # [CHECK 1] Automatic Updates Enabled if ($checkAutoUpdate) { $auProperty = Get-ItemProperty -Path $auPath -Name NoAutoUpdate -ErrorAction SilentlyContinue $noAutoUpdate = $auProperty.NoAutoUpdate if ($null -eq $noAutoUpdate) { $noAutoUpdate = 0 } $autoUpdateEnabled = $noAutoUpdate -eq 0 if ($autoUpdateEnabled -eq $AutoUpdateEnabled) { if ($autoUpdateEnabled) { $actualStatus = 'Enabled' $expectedStatus = 'Enabled' } else { $actualStatus = 'Disabled' $expectedStatus = 'Disabled' } $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Automatic Updates' Expected = $expectedStatus Actual = $actualStatus Status = 'COMPLIANT' Severity = 'INFO' ComputerName = $ComputerName } } else { if ($autoUpdateEnabled) { $actualStatus = 'Enabled' } else { $actualStatus = 'Disabled' } if ($AutoUpdateEnabled) { $expectedStatus = 'Enabled' } else { $expectedStatus = 'Disabled' } $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Automatic Updates' Expected = $expectedStatus Actual = $actualStatus Status = 'DRIFT' Severity = 'HIGH' ComputerName = $ComputerName } Write-Log -Message "Update drift: Auto-updates is $actualStatus (expected $expectedStatus)" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } } # [CHECK 2] Scheduled Install Day/Time (Recommended+) if ($checkScheduledInstall) { $scheduleDay = (Get-ItemProperty -Path $auPath -Name ScheduledInstallDay -ErrorAction SilentlyContinue).ScheduledInstallDay $scheduleTime = (Get-ItemProperty -Path $auPath -Name ScheduledInstallTime -ErrorAction SilentlyContinue).ScheduledInstallTime if ($null -eq $scheduleDay -or $null -eq $scheduleTime) { $actualStatus = 'Not Configured' $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Scheduled Install' Expected = 'Day and Time Configured' Actual = $actualStatus Status = 'DRIFT' Severity = 'MEDIUM' ComputerName = $ComputerName } Write-Log -Message "Update drift: Scheduled install day/time not configured" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } else { $dayNames = @{ 0 = 'Every Day'; 1 = 'Sunday'; 2 = 'Monday'; 3 = 'Tuesday'; 4 = 'Wednesday'; 5 = 'Thursday'; 6 = 'Friday'; 7 = 'Saturday' } $dayName = $dayNames[$scheduleDay] $actualStatus = "$dayName at $scheduleTime hours" $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Scheduled Install' Expected = 'Day and Time Configured' Actual = $actualStatus Status = 'COMPLIANT' Severity = 'INFO' ComputerName = $ComputerName } } } # [CHECK 3] Notification Level (Recommended+) if ($checkNotificationLevel) { $notifyLevel = (Get-ItemProperty -Path $auPath -Name AUOptions -ErrorAction SilentlyContinue).AUOptions if ($null -eq $notifyLevel) { $notifyLevel = 0 } $notifyLevelNames = @{ 0 = 'Not Configured' 2 = 'Notify for Download/Auto-Install' 3 = 'Auto Download/Notify for Install' 4 = 'Auto Download and Install' } $notifyStatus = $notifyLevelNames[$notifyLevel] if ($null -eq $notifyStatus) { $notifyStatus = "Unknown ($notifyLevel)" } # Recommend level 4 (auto download and install) for Strict, level 3 for Recommended if ($Profile -eq 'Strict') { $expectedNotifyLevel = 4 } else { $expectedNotifyLevel = 3 } $expectedStatus = $notifyLevelNames[$expectedNotifyLevel] if ($notifyLevel -eq $expectedNotifyLevel) { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Update Notification Level' Expected = $expectedStatus Actual = $notifyStatus Status = 'COMPLIANT' Severity = 'INFO' ComputerName = $ComputerName } } else { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Update Notification Level' Expected = $expectedStatus Actual = $notifyStatus Status = 'DRIFT' Severity = 'MEDIUM' ComputerName = $ComputerName } Write-Log -Message "Update drift: Notification level is $notifyStatus (expected $expectedStatus)" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } } # [CHECK 4] Reboot Behavior (Strict) if ($checkRebootBehavior) { $noAutoReboot = (Get-ItemProperty -Path $auPath -Name NoAutoRebootWithLoggedOnUsers -ErrorAction SilentlyContinue).NoAutoRebootWithLoggedOnUsers if ($null -eq $noAutoReboot) { $noAutoReboot = 0 } $rebootDisabled = $noAutoReboot -eq 1 if ($RequireScheduledRestart) { $expectedStatus = 'Auto Reboot Enabled' } else { $expectedStatus = 'Respects User Sessions' } if ($rebootDisabled) { $actualStatus = 'Respects User Sessions' } else { $actualStatus = 'Auto Reboot Enabled' } if ($rebootDisabled -ne $RequireScheduledRestart) { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Reboot Behavior' Expected = $expectedStatus Actual = $actualStatus Status = 'COMPLIANT' Severity = 'INFO' ComputerName = $ComputerName } } else { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Reboot Behavior' Expected = $expectedStatus Actual = $actualStatus Status = 'DRIFT' Severity = 'MEDIUM' ComputerName = $ComputerName } Write-Log -Message "Update drift: Reboot behavior is '$actualStatus' (expected '$expectedStatus')" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } } # [CHECK 5] Update Recency (Strict/Detailed) if ($checkUpdateRecency) { try { $updateSession = New-Object -ComObject Microsoft.Update.Session $updateSearcher = $updateSession.CreateUpdateSearcher() $searchResult = $updateSearcher.Search("IsInstalled=1") $recentUpdate = $searchResult.Updates | Sort-Object -Property InstallationDate -Descending | Select-Object -First 1 if ($recentUpdate) { $lastUpdateDate = $recentUpdate.InstallationDate $daysSinceUpdate = [math]::Floor(((Get-Date) - $lastUpdateDate).TotalDays) $expectedStatus = "Within $MaxDaysSinceLastUpdate days" if ($daysSinceUpdate -le $MaxDaysSinceLastUpdate) { $actualStatus = "$daysSinceUpdate days ago" $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Last Update Recency' Expected = $expectedStatus Actual = $actualStatus Status = 'COMPLIANT' Severity = 'INFO' ComputerName = $ComputerName } } else { $actualStatus = "$daysSinceUpdate days ago" $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Last Update Recency' Expected = $expectedStatus Actual = $actualStatus Status = 'DRIFT' Severity = 'HIGH' ComputerName = $ComputerName } Write-Log -Message "Update drift: Last update was $daysSinceUpdate days ago (expected within $MaxDaysSinceLastUpdate)" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } } else { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Last Update Recency' Expected = "At least one update installed" Actual = 'No updates found' Status = 'DRIFT' Severity = 'HIGH' ComputerName = $ComputerName } Write-Log -Message "Update drift: No installed updates found" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } } catch { Write-Log -Message "Error checking update recency: $_" -Level Warning -Caller $MyInvocation.MyCommand.Name } } # [CHECK 6] Pending Updates try { $updateSession = New-Object -ComObject Microsoft.Update.Session $updateSearcher = $updateSession.CreateUpdateSearcher() $searchResult = $updateSearcher.Search("IsInstalled=0") $pendingUpdates = $searchResult.Updates.Count if ($pendingUpdates -gt 0) { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Pending Updates' Expected = 'All Updates Installed' Actual = "$pendingUpdates pending" Status = 'DRIFT' Severity = 'MEDIUM' ComputerName = $ComputerName } Write-Log -Message "Update drift: $pendingUpdates updates pending installation" ` -Level Warning -Caller $MyInvocation.MyCommand.Name } else { $findings += [PSCustomObject]@{ Category = 'Windows Updates' Setting = 'Pending Updates' Expected = 'All Updates Installed' Actual = '0 pending' Status = 'COMPLIANT' Severity = 'INFO' ComputerName = $ComputerName } } } catch { Write-Log -Message "Error checking pending updates: $_" -Level Warning -Caller $MyInvocation.MyCommand.Name } } catch { Write-Log -Message "Error checking update status: $_" -Level Error -Caller $MyInvocation.MyCommand.Name throw } # Filter to drift-only if requested if ($ReportDriftOnly) { $findings = $findings | Where-Object { $_.Status -eq 'DRIFT' } } return $findings } |