Private/Kinds/Updates.ps1
|
# The Updates Kind: whether Windows is still maintaining itself. # # A machine that stopped updating months ago does not announce it. The symptom a Customer # reports is that it got slow, and the cause is a servicing stack that has been failing # quietly since a fixed point that this Check is what finds. # Windows Update result codes, as the agent reports them. Numeric because the strings # beside them are localised and the estate is German. $script:UpdateResultSucceeded = 2 $script:UpdateResultFailed = 4 # How much history to ask for. The agent keeps far more than a Technician needs and # fetching all of it is slow on a machine that has been running for years. $script:UpdateHistoryLimit = 100 # Where Windows records that it wants a restart. Read as three independent facts, because # a pending file rename is ordinary and a pending servicing operation is not. $script:UpdateRebootKeys = @( @{ Name = 'CBS'; Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' } @{ Name = 'WindowsUpdate'; Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' } ) function Get-UpdatesData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $days = [int](Get-Parameter $Parameters 'Days' 30) $since = (Get-Date).AddDays(-$days) $history = @() $reachable = $true try { $session = New-Object -ComObject Microsoft.Update.Session $searcher = $session.CreateUpdateSearcher() $total = $searcher.GetTotalHistoryCount() if ($total -gt 0) { $history = @($searcher.QueryHistory(0, [Math]::Min($script:UpdateHistoryLimit, $total)) | ForEach-Object { [pscustomobject]@{ Title = "$($_.Title)" Date = $_.Date ResultCode = $_.ResultCode } }) } } catch { # The update service is stoppable, and is stopped on plenty of managed machines. # That is a different fact from a machine that has simply never updated. $reachable = $false } $pending = @() foreach ($key in $script:UpdateRebootKeys) { if (Test-Path $key.Path) { $pending += $key.Name } } $renames = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' ` -ErrorAction SilentlyContinue).PendingFileRenameOperations if ($renames) { $pending += 'FileRename' } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Updates' # When the Gatherer looked. The age of the last update is a Severity input, so a # Judge computing it from the clock would not be a pure function of what it was # handed - the same data would grade differently tomorrow, and no fixture could # pin a boundary. The System Kind records the same thing for the same reason. GatheredAt = Get-Date Days = $days Since = $since ServiceReachable = $reachable History = $history PendingReboot = $pending } } function ConvertTo-UpdatesFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) New-LastUpdateFinding -Data $Data -Parameters $Parameters New-FailedUpdateFinding -Data $Data -Parameters $Parameters New-PendingRebootFinding -Data $Data -Parameters $Parameters } function New-LastUpdateFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warnDays = Get-Parameter $Parameters 'LastUpdateWarnDays' 35 $failDays = Get-Parameter $Parameters 'LastUpdateFailDays' 60 if (-not (Get-DataProperty $Data 'ServiceReachable')) { return New-UnavailableFinding -Category Updates -Check (Get-Text 'Check.Updates.LastSuccessfulUpdate') ` -Hint (Get-Text 'Hint.Updates.TheWindowsUpdateAgentDid') } $succeeded = @((Get-DataCollection $Data 'History') | Where-Object { (ConvertTo-Number $_.ResultCode) -eq $script:UpdateResultSucceeded -and $_.Date }) if (-not $succeeded.Count) { # An empty history is not an old update; it is no evidence either way, and saying # so is the difference between a Technician checking and a Technician assuming. return New-UnavailableFinding -Category Updates -Check (Get-Text 'Check.Updates.LastSuccessfulUpdate') ` -Hint (Get-Text 'Hint.Updates.ThisMachineHasNoSuccessful') } $gatheredAt = Get-DataProperty $Data 'GatheredAt' if (-not $gatheredAt) { return New-UnavailableFinding -Category Updates -Check (Get-Text 'Check.Updates.LastSuccessfulUpdate') ` -Hint (Get-Text 'Hint.Updates.TheCheckDidNotRecord') } $last = @($succeeded | Sort-Object { [datetime]$_.Date } -Descending)[0] $age = ([datetime]$gatheredAt - [datetime]$last.Date).TotalDays New-Finding -Category Updates -Check (Get-Text 'Check.Updates.LastSuccessfulUpdate') ` -Severity (Get-Severity $age $warnDays $failDays) ` -Value ((Get-Text 'Value.Updates.LastWithAge') -f [datetime]$last.Date, $age) ` -Hint (Get-Text 'Hint.Updates.WindowsNotUpdating') } function New-FailedUpdateFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warnAbove = Get-Parameter $Parameters 'FailedUpdateWarnAbove' 0 if (-not (Get-DataProperty $Data 'ServiceReachable')) { return } $since = Get-DataProperty $Data 'Since' $failed = @((Get-DataCollection $Data 'History') | Where-Object { (ConvertTo-Number $_.ResultCode) -eq $script:UpdateResultFailed -and $_.Date -and (-not $since -or [datetime]$_.Date -ge [datetime]$since) }) New-Finding -Category Updates -Check (Get-Text 'Check.Updates.FailedUpdatesPeriod') ` -Severity (Get-Severity $failed.Count $warnAbove ([double]::MaxValue)) -Value $failed.Count ` -Hint (Get-Text 'Hint.Updates.RepeatedFailedUpdatesServicingProblem') } function New-PendingRebootFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $pending = Get-DataCollection $Data 'PendingReboot' # A pending file rename on its own is ordinary - an installer tidying up at next boot. # A pending servicing or update operation means the machine is mid-change and some of # what Gutcheck measured is in an in-between state. $serious = @($pending | Where-Object { $_ -in 'CBS', 'WindowsUpdate' }) $severity = 'OK' if ($serious.Count) { $severity = 'WARN' } New-Finding -Category Updates -Check (Get-Text 'Check.Updates.PendingReboot') -Severity $severity ` -Value $(if ($pending.Count) { $pending -join ', ' } else { 'no' }) ` -Hint (Get-Text 'Hint.Updates.RestartTheLaptop') } |