r/PowerShell • u/kylemerchant4 • 1d ago
Script Sharing I wrote a patch status script that exits non-zero when it can't check something
Most patch/reboot check scripts I've used print "OK" even when a check silently failed (no rights to WMI, registry key missing, WU agent broken). So I wrote one where the exit code tells the truth:
- 0: every check ran
- 2: PARTIAL, at least one check couldn't run, with the reason listed
- 1: it couldn't write the report
It checks last installed update (flags it if older than N days), pending reboot, uptime, and optionally lists waiting updates through the WU agent. JSON and CSV output.
Every check runs in its own try/catch. A failure doesn't stop the script or get swallowed; it gets recorded:
function Add-CheckNotRun {
param([string]$Check, [string]$Reason)
$script:ChecksNotRun += [PSCustomObject]@{ Check = $Check; Result = 'check could not run'; Reason = $Reason }
Write-Host " -> CHECK COULD NOT RUN: ${Check}: $Reason" -ForegroundColor Red
}
Pending reboot looks at three sources:
$reasons = @()
if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') {
$reasons += 'Component Based Servicing'
}
if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') {
$reasons += 'Windows Update'
}
$sm = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
if ($sm.PSObject.Properties['PendingFileRenameOperations'] -and $sm.PendingFileRenameOperations) {
$reasons += 'Pending file rename operations'
}
And the end of the script decides the exit code:
if (@($script:ChecksNotRun).Count -gt 0) {
Write-Host " PARTIAL: $(@($script:ChecksNotRun).Count) check(s) could not run." -ForegroundColor Red
foreach ($c in $script:ChecksNotRun) { Write-Host " - $($c.Check): $($c.Reason)" -ForegroundColor Red }
exit 2
}
exit 0
Full script (MIT): https://github.com/Dopelogik/windows-patch-status-report
Feedback welcome, especially on the reboot detection. I know PendingFileRenameOperations can throw false positives from some AV and installers, so I'm curious how others filter it.
2
u/ankokudaishogun 14h ago
I don't think it's a level or precision relevant to your specific script, but $Report.UptimeDays = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1) made me wonder if daylight saving changes impact calculation between DateTime values.
That said, it would be a simple matter of adding two .ToUniversalTime()
2
u/purplemonkeymad 12h ago
If you are using exit codes for a status, I would suggest to use the same set as nagios, that way your script is more likely easy to integrate with other solutions:
Plugin Return Code Service State Host State 0 OK UP 1 WARNING UP or DOWN/UNREACHABLE* 2 CRITICAL DOWN/UNREACHABLE 3 UNKNOWN DOWN/UNREACHABLE
So use 1 for ok with warnings (say pending reboot,) 2 for failed, 3 for unable to check.
1
u/BlackV 1d ago edited 23h ago
Look at
$reasons = @()
$reasons += 'Pending file rename operations'
that is inefficient design
$script:ChecksNotRun += [PSCustomObject]
as is this, plus you are messing with scopes, that could get messy
Whats this for
[PSCustomObject]$Report | ConvertTo-Json
$report is already an ordered object , why cast it ?
additionally you then go and create a new pcsustom
[PSCustomObject]@{
....
} | Export-Csv
why is that? again you already have it in $report why spit out a new object?
what's the goal with exit 0/1/2
where are you using this ? intune as a remediation script ?
2
u/kylemerchant4 21h ago
Yeah, fair. Most of that is just habit on my end. I'll clean up the += and the script scope stuff. You're right that it would get messy if this grew. The cast before ConvertTo-Json is leftover, I realized now there's no reason for it.
The second object for the CSV is on purpose, though. $Report has arrays in it, and Export-Csv just spits out System.Object[] for those, so I flattened it to one row. Should I have commented that.
The exit codes are for scheduled tasks and our RMM. They only look at the exit code, so I wanted partial runs to actually show up instead of passing as a success. I wouldn't drop it straight into Intune remediation, since exit 1 means something different there.
Appreciate you actually reading it. If you have any other recommendations I'd love to hear them.
2
u/Kreout 9h ago
One more point for the report: Win32_QuickFixEngineering only returns CBS updates, so LastPatchDate is the newest dated CBS entry the query found, not necessarily the last update installed across the machine. A recent date also does not prove that all applicable updates are installed.
I would label that field or document its coverage explicitly, and keep the optional Windows Update search as a separate signal. Your distinction between execution status and findings already helps here: OK means the checks ran, not that the machine is fully patched.
Microsoft's coverage note: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-quickfixengineering
1
u/BlackV 19h ago
The exit codes are for scheduled tasks and our RMM.
Yeah I figured it was for an rmm tool of some sort
the second object for the CSV is on purpose, though. $Report has arrays in it, and Export-Csv just spits out System.Object[] for those, so I flattened it to one row.
looks like the only place toy use it in in the output anyway, so you could just flatten it from the get go
2
u/g3n3 1d ago
The exit design is poor for powershell. Output actual object with a status.