|
| 1 | +# ============================================================================== |
| 2 | +# ShellKnight - Health Check (read-only) |
| 3 | +# ============================================================================== |
| 4 | +# Confirms a box is actually self-sufficient. Run it from Datto RMM (or a GPO / |
| 5 | +# any remote-exec) across the fleet and eyeball the output: it tells you, per |
| 6 | +# machine, whether the agentless loop is intact BEFORE the dashboard would |
| 7 | +# otherwise show it drifting stale days later. |
| 8 | +# |
| 9 | +# Checks: PASS/WARN/FAIL |
| 10 | +# * Scheduled task 'ShellKnight' present + enabled (+ next run time) |
| 11 | +# * config.json present + valid (has Battlefield URL + key; shows site/interval) |
| 12 | +# * Last run recency (newest JSON report vs the configured interval) |
| 13 | +# * Installed version (from the newest JSON report) |
| 14 | +# |
| 15 | +# Exit code: 0 = healthy 1 = warning(s) 2 = failure(s) |
| 16 | +# (so Datto can flag non-zero results automatically) |
| 17 | +# |
| 18 | +# This script only READS. It changes nothing and sends nothing. |
| 19 | +# ============================================================================== |
| 20 | + |
| 21 | +$Root = 'C:\ProgramData\ShellKnight' |
| 22 | +$ConfigPath = Join-Path $Root 'config.json' |
| 23 | +$JsonDir = Join-Path $Root 'JSON' |
| 24 | +$TaskName = 'ShellKnight' |
| 25 | + |
| 26 | +$fails = 0; $warns = 0 |
| 27 | +$lines = @() |
| 28 | +function Add-Line([string]$state, [string]$label, [string]$detail) { |
| 29 | + $script:lines += (' [{0}] {1,-22}: {2}' -f $state, $label, $detail) |
| 30 | + if ($state -eq 'FAIL') { $script:fails++ } |
| 31 | + elseif ($state -eq 'WARN') { $script:warns++ } |
| 32 | +} |
| 33 | + |
| 34 | +# --- config.json --------------------------------------------------------------- |
| 35 | +$cfg = $null |
| 36 | +$scheduleHours = 8 |
| 37 | +if (Test-Path $ConfigPath) { |
| 38 | + try { |
| 39 | + $cfg = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json |
| 40 | + $hasUrl = [bool]$cfg.BattlefieldURL |
| 41 | + $hasKey = [bool]$cfg.BattlefieldApiKey |
| 42 | + if ($cfg.ScheduleHours) { $scheduleHours = [int]$cfg.ScheduleHours } |
| 43 | + if ($hasUrl -and $hasKey) { |
| 44 | + $site = if ($cfg.SiteName) { $cfg.SiteName } else { '(none - falls back to domain)' } |
| 45 | + Add-Line 'PASS' 'config.json' "valid | site '$site' | every ${scheduleHours}h" |
| 46 | + } else { |
| 47 | + Add-Line 'FAIL' 'config.json' 'present but missing Battlefield URL or key -> re-bootstrap' |
| 48 | + } |
| 49 | + } catch { |
| 50 | + Add-Line 'FAIL' 'config.json' "present but unreadable/corrupt -> re-bootstrap ($($_.Exception.Message))" |
| 51 | + } |
| 52 | +} else { |
| 53 | + Add-Line 'FAIL' 'config.json' 'MISSING -> never onboarded here; run the install command' |
| 54 | +} |
| 55 | + |
| 56 | +# --- scheduled task ------------------------------------------------------------ |
| 57 | +$taskState = $null; $nextRun = $null |
| 58 | +try { |
| 59 | + $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop |
| 60 | + $taskState = "$($t.State)" |
| 61 | + try { $nextRun = (Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction Stop).NextRunTime } catch {} |
| 62 | +} catch { |
| 63 | + # Fallback for older hosts without the ScheduledTasks module. |
| 64 | + $raw = & schtasks.exe /Query /TN $TaskName /FO LIST /V 2>$null |
| 65 | + if ($LASTEXITCODE -eq 0 -and $raw) { |
| 66 | + $taskState = 'Present' |
| 67 | + $nr = ($raw | Select-String 'Next Run Time:') -replace '.*Next Run Time:\s*', '' |
| 68 | + if ($nr) { $nextRun = "$nr".Trim() } |
| 69 | + } |
| 70 | +} |
| 71 | +if (-not $taskState) { |
| 72 | + Add-Line 'FAIL' "task '$TaskName'" 'MISSING -> not self-scheduling; re-bootstrap to restore it' |
| 73 | +} elseif ($taskState -eq 'Disabled') { |
| 74 | + Add-Line 'FAIL' "task '$TaskName'" 'present but DISABLED -> enable or re-bootstrap' |
| 75 | +} else { |
| 76 | + $nr = if ($nextRun) { "next run $nextRun" } else { 'next run unknown' } |
| 77 | + Add-Line 'PASS' "task '$TaskName'" "$taskState, $nr" |
| 78 | +} |
| 79 | + |
| 80 | +# --- last run recency + version (from newest JSON report) ---------------------- |
| 81 | +$newest = $null |
| 82 | +if (Test-Path $JsonDir) { |
| 83 | + $newest = Get-ChildItem -LiteralPath $JsonDir -Filter '*.json' -ErrorAction SilentlyContinue | |
| 84 | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 |
| 85 | +} |
| 86 | +if ($newest) { |
| 87 | + $ageH = [math]::Round(((Get-Date) - $newest.LastWriteTime).TotalHours, 1) |
| 88 | + $when = $newest.LastWriteTime.ToString('yyyy-MM-dd HH:mm') |
| 89 | + # Allow up to 2x the interval + a little slack before calling it late. |
| 90 | + $threshold = ($scheduleHours * 2) + 2 |
| 91 | + if ($ageH -gt $threshold) { |
| 92 | + Add-Line 'WARN' 'last run' "$when (${ageH}h ago) -> exceeds ${threshold}h; box may be offline or task not firing" |
| 93 | + } else { |
| 94 | + Add-Line 'PASS' 'last run' "$when (${ageH}h ago)" |
| 95 | + } |
| 96 | + $ver = $null |
| 97 | + try { $ver = (Get-Content -LiteralPath $newest.FullName -Raw | ConvertFrom-Json).version } catch {} |
| 98 | + if ($ver) { Add-Line 'PASS' 'installed version' "$ver" } |
| 99 | + else { Add-Line 'WARN' 'installed version' 'unknown (report had no version field)' } |
| 100 | +} else { |
| 101 | + Add-Line 'WARN' 'last run' 'no run reports found yet (has it completed a run?)' |
| 102 | +} |
| 103 | + |
| 104 | +# --- verdict ------------------------------------------------------------------- |
| 105 | +$result = if ($fails) { 'UNHEALTHY' } elseif ($warns) { 'DEGRADED' } else { 'HEALTHY' } |
| 106 | +$stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm') |
| 107 | +Write-Host '' |
| 108 | +Write-Host " ShellKnight Health Check | $env:COMPUTERNAME | $stamp" |
| 109 | +Write-Host ' ------------------------------------------------------------------------' |
| 110 | +$lines | ForEach-Object { Write-Host $_ } |
| 111 | +Write-Host ' ------------------------------------------------------------------------' |
| 112 | +Write-Host " RESULT: $result ($fails fail, $warns warn)" |
| 113 | +Write-Host '' |
| 114 | + |
| 115 | +if ($fails) { exit 2 } elseif ($warns) { exit 1 } else { exit 0 } |
0 commit comments