<# .SYNOPSIS ShopDB agent utility: reports pending Dell Command | Update items. .DESCRIPTION Discovered and executed by PowerShellUtilityRunner on the telemetry cadence. Emits a single JSON object on stdout, which the agent attaches to the report as `dellupdates`. Never throws and never writes anything but JSON: a failure here must not break a telemetry run, so every error path still emits a valid object with a `status` explaining why the list is empty. THIS SCRIPT DOES NOT SCAN. It only reads the log a previous scan wrote. The scan itself is fired by the \ShopDB-DellCommandScan scheduled task, which is what pins dcu-cli.exe's -outputLog to the path below. Keeping the two apart is deliberate: a telemetry collector that shelled out to dcu-cli would block the cadence for minutes and hammer Dell's servers every 15 minutes. OUTPUT CONTRACT (keep in sync with plugins/endpoints/plugin.py): status unknown | ok | error - "unknown" means no scan has ever run, which is NOT the same as "ok" with an empty list (known clean). scannedat ISO-8601 UTC timestamp of the scan log's last write, or null. count total pending updates found. updates up to $MaxUpdates entries of { name, severity, version }. truncated true when count exceeded $MaxUpdates. Dell's own long description text is deliberately NOT carried: it is sentence-length marketing prose per update, which would add roughly an order of magnitude to every report for no operational value. Name plus severity is what an operator decides on. #> $ErrorActionPreference = 'Stop' # Cap the list rather than the payload. A machine that is years behind can have # dozens of pending updates, and an unbounded list would bloat every report on # the cadence. Mirrors the MaxReportedConnections precedent in CommandExecutor: # a truncated answer is more useful than a rejected payload. $MaxUpdates = 25 # Must match the -outputLog path set in the \ShopDB-DellCommandScan task # definition. ShopDB-initiated scans write HERE rather than to Dell's default # location, which is what keeps them distinguishable from the corporate-managed # DCU tasks that also run on these machines. $ScanLog = Join-Path $env:ProgramData 'ShopDB\EndpointAgent\dellcommand\scan.xml' function Write-Result { param($Status, $ScannedAt, $Count, $Updates, $Truncated) # Compress so the payload carries no pretty-printing whitespace. Depth 3 # covers the nested updates array. [pscustomobject]@{ status = $Status scannedat = $ScannedAt count = $Count updates = @($Updates) truncated = [bool]$Truncated } | ConvertTo-Json -Depth 3 -Compress } try { if (-not (Test-Path -LiteralPath $ScanLog)) { # No scan has run yet. This is the normal state for a freshly adopted # endpoint, not an error, but it must stay distinct from "scanned and # found nothing" or the UI would claim a machine is up to date when # nobody has ever checked. Write-Result -Status 'unknown' -ScannedAt $null -Count 0 -Updates @() -Truncated $false return } $logFile = Get-Item -LiteralPath $ScanLog $scannedAt = $logFile.LastWriteTimeUtc.ToString('o') [xml]$report = Get-Content -LiteralPath $ScanLog -Raw # dcu-cli's scan report shape has varied across DCU versions, so select the # update nodes by name anywhere in the tree rather than by a fixed path. $nodes = @($report.SelectNodes('//update')) if ($nodes.Count -eq 0) { $nodes = @($report.SelectNodes('//Update')) } $updates = foreach ($node in $nodes) { # Attribute casing also varies by version; take whichever is present and # fall back rather than emitting a null-filled row. $name = $node.name if (-not $name) { $name = $node.Name } if (-not $name) { $name = $node.display } if (-not $name) { continue } $severity = $node.urgency if (-not $severity) { $severity = $node.Urgency } if (-not $severity) { $severity = $node.criticality } $version = $node.version if (-not $version) { $version = $node.Version } [pscustomobject]@{ name = [string]$name severity = if ($severity) { [string]$severity } else { 'unknown' } version = if ($version) { [string]$version } else { $null } } } $updates = @($updates) $count = $updates.Count $truncated = $count -gt $MaxUpdates if ($truncated) { $updates = $updates[0..($MaxUpdates - 1)] } Write-Result -Status 'ok' -ScannedAt $scannedAt -Count $count ` -Updates $updates -Truncated $truncated } catch { # Emit a valid object rather than failing: the runner would log a non-JSON # utility and drop it, which looks identical to "never ran" on the server. # An explicit error status tells an operator the scan log exists but could # not be read, which is a different problem from no scan at all. Write-Result -Status 'error' -ScannedAt $null -Count 0 -Updates @() -Truncated $false }