diff --git a/.claude/commands/rmm.md b/.claude/commands/rmm.md index 3abd176..1dd8d41 100644 --- a/.claude/commands/rmm.md +++ b/.claude/commands/rmm.md @@ -24,6 +24,7 @@ Interact with the GuruRMM agent fleet: list agents, run remote commands (PowerSh /rmm cancel Cancel a pending or running command /rmm history [N] Recent command history (default 10, max 500) /rmm onboard [site] Create a new client + site, vault the one-time enrollment key +/rmm diagnose [client] Run onboarding health/security diagnostic + baseline ``` --- @@ -679,6 +680,84 @@ git -C "$VR" add "clients/$SLUG/gururmm-site-main.sops.yaml" && git -C "$VR" com --- +## Onboarding diagnostic (`/rmm diagnose`) + +Run a one-shot security + health + inventory probe against a newly onboarded +Windows agent and produce a prioritized "take this seriously" report plus an +immutable before/after baseline. This is the Phase 1 tooling implementation; +a native GuruRMM feature (DB-backed storage, scheduled re-baselines) is Phase 3. + +**Runner:** `.claude/scripts/run-onboarding-diagnostic.sh [client-slug]` +**Probe:** `.claude/scripts/onboarding-diagnostic.ps1` (Windows PowerShell 5.1, ASCII, runs as SYSTEM) + +```bash +bash "$REPO_ROOT/.claude/scripts/run-onboarding-diagnostic.sh" FrontDeskReception rednour +``` + +If no client-slug is given, it is derived by slugifying the agent's `client_name`. + +### Workflow + +1. Authenticate to RMM (vault creds, same as the rest of this skill). +2. Resolve the agent (exact UUID, exact hostname, then partial). Windows-only. +3. Upload the probe to the endpoint **base64-encoded, in <24 KB chunks** (the + agent caps an inline command body at ~32-40 KB; the probe is ~60 KB), then a + final small command decodes it to a `.ps1`, runs it, and deletes both temp + files. Every check in the probe is wrapped in try/catch, so one failing check + becomes an `unknown`-severity finding instead of aborting the probe. +4. The probe emits a single JSON object fenced by `===DIAG-JSON-START===` / + `===DIAG-JSON-END===`; the runner extracts it from between the markers. +5. Grade, write two baseline files, diff against any prior baseline, alert. + +### Grade model + +| Grade | Meaning | +|-------|---------| +| **RED** | At least one `critical` finding | +| **AMBER** | At least one `warning`, no `critical` | +| **GREEN** | No `critical` and no `warning` | + +`unknown`-severity findings (a check that failed to run) do not change the grade +but are listed in the report for manual follow-up. + +### What it checks + +- **Security:** Defender state (RTP/service/signature age/tamper), 3rd-party AV + conflicts, leftover competitor RMM / remote-access agents (ScreenConnect, + NinjaRMM, Datto, Atera, Kaseya, TeamViewer, AnyDesk, Splashtop, N-able, + Syncro, Action1, Automate, LogMeIn), firewall profiles, BitLocker (laptop-aware), + local admins / built-in Administrator / non-expiring passwords, patch posture + + OS EOL, RDP/NLA, SMBv1, UAC, LAPS. +- **Health:** disk free %, SMART/physical-disk health, 14-day stability + (unexpected shutdown / BSOD / disk errors), pending reboot, uptime, failed + auto-start services, domain secure channel, time source, battery (laptops), + backup-agent presence. +- **Inventory baseline (info):** model/serial, CPU/RAM, BIOS, TPM, Secure Boot, + OS edition/build/activation, full installed-software list, local users/groups, + network, scheduled tasks + Run-key autoruns. + +### Where baselines are stored + +`clients//onboarding-baselines/` — two files per run, both timestamped +`-`: + +- `*.json` — the raw immutable snapshot (do not edit; it is the source of truth + for diffs). +- `*.md` — the human report: grade, findings grouped critical -> warning -> + info -> unknown, inventory summary, and a diff section vs the most recent prior + baseline (new / resolved / regressed findings, software added/removed). + +Baselines are immutable and append-only. GuruRMM-DB storage of baselines arrives +with the Phase 3 native feature. + +### Alerting + +Each **CRITICAL** finding and a **RED** overall grade auto-post a one-line +`[RMM]` alert to **#dev-alerts** via `post-bot-alert.sh` (ASCII only, soft-fail). +Example: `[RMM] Onboarding diag () = RED: critical - `. + +--- + ## Known enrolled agents (verify with GET /api/agents — UUIDs change on re-enroll) Do not use this table as authoritative — always resolve live. Treat as a starting hint only. diff --git a/.claude/scripts/onboarding-diagnostic.ps1 b/.claude/scripts/onboarding-diagnostic.ps1 new file mode 100644 index 0000000..9b0d7e2 --- /dev/null +++ b/.claude/scripts/onboarding-diagnostic.ps1 @@ -0,0 +1,1275 @@ +<# +.SYNOPSIS + GuruRMM onboarding diagnostic probe (Phase 1). + +.DESCRIPTION + Security + health + inventory probe for a newly onboarded Windows agent. + Dispatched via the GuruRMM RMM API and run as SYSTEM by the agent. + + Design constraints (do not relax): + - Windows PowerShell 5.1 compatible (no PS7-only syntax, no ternary, no ??). + - ASCII only. No unicode, em-dashes, smart quotes. They break the + Discord alert path and can break PS5.1 parsing of this file. + - Non-interactive. No Read-Host / pause / prompts. + - EVERY check wrapped in try/catch so one failure cannot abort the probe. + A failed check records a finding with severity "unknown". + - Emits a SINGLE JSON object to stdout, fenced by literal marker lines: + ===DIAG-JSON-START=== + { ... } + ===DIAG-JSON-END=== + so the runner can extract it cleanly from surrounding noise. + + Finding schema: + { id, category, severity, title, detail, evidence } + severity in: critical | warning | info | unknown + + Top-level schema: + { host, collected_at_utc, os{}, facts{}, findings[] } +#> + +# Be permissive: non-terminating errors should not kill the probe. Each check +# uses its own try/catch with -ErrorAction Stop where it needs hard failures. +$ErrorActionPreference = 'Continue' +$ProgressPreference = 'SilentlyContinue' +Set-StrictMode -Off + +# --------------------------------------------------------------------------- +# Collectors +# --------------------------------------------------------------------------- +$Findings = New-Object System.Collections.ArrayList +$Facts = @{} + +function Add-Finding { + param( + [string]$Id, + [string]$Category, + [string]$Severity, # critical | warning | info | unknown + [string]$Title, + [string]$Detail, + $Evidence + ) + $ev = '' + try { + if ($null -ne $Evidence) { + if ($Evidence -is [string]) { + $ev = $Evidence + } else { + $ev = ($Evidence | Out-String).Trim() + } + } + } catch { $ev = '' } + # Keep evidence bounded so a single finding can't bloat the JSON. + if ($ev.Length -gt 4000) { $ev = $ev.Substring(0, 4000) + ' ...[truncated]' } + + $null = $Findings.Add([ordered]@{ + id = $Id + category = $Category + severity = $Severity + title = $Title + detail = $Detail + evidence = $ev + }) +} + +# Wrap a check. If the scriptblock throws, record an "unknown" finding instead +# of letting the exception propagate and abort the whole probe. +function Invoke-Check { + param( + [string]$Id, + [string]$Category, + [string]$Title, + [scriptblock]$Body + ) + try { + & $Body + } catch { + $msg = '' + try { $msg = $_.Exception.Message } catch { $msg = 'unknown error' } + Add-Finding -Id "$Id.error" -Category $Category -Severity 'unknown' ` + -Title "Check failed: $Title" ` + -Detail "The probe could not complete this check. Manual review recommended." ` + -Evidence $msg + } +} + +# Safe fact setter (never throws). +function Set-Fact { + param([string]$Key, $Value) + try { $Facts[$Key] = $Value } catch { } +} + +# --------------------------------------------------------------------------- +# Basic host / OS facts (best-effort; never aborts) +# --------------------------------------------------------------------------- +$HostName = $env:COMPUTERNAME +try { if ([string]::IsNullOrEmpty($HostName)) { $HostName = [System.Net.Dns]::GetHostName() } } catch { } + +$CollectedAtUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + +$OsInfo = [ordered]@{ + caption = '' + version = '' + build = '' + install_date = '' + last_boot_utc = '' + architecture = '' +} + +# Chassis / laptop detection result is needed by multiple checks. +$IsLaptop = $false + +Invoke-Check -Id 'os.info' -Category 'inventory' -Title 'OS information' -Body { + $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + $script:OsInfo.caption = [string]$os.Caption + $script:OsInfo.version = [string]$os.Version + $script:OsInfo.build = [string]$os.BuildNumber + $script:OsInfo.architecture = [string]$os.OSArchitecture + try { $script:OsInfo.install_date = $os.InstallDate.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } catch { } + try { $script:OsInfo.last_boot_utc = $os.LastBootUpTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } catch { } +} + +Invoke-Check -Id 'chassis.detect' -Category 'inventory' -Title 'Chassis type (laptop detection)' -Body { + $enc = Get-CimInstance -ClassName Win32_SystemEnclosure -ErrorAction Stop + $types = @() + foreach ($e in $enc) { if ($e.ChassisTypes) { $types += $e.ChassisTypes } } + Set-Fact 'chassis_types' $types + # 8=Portable 9=Laptop 10=Notebook 11=Hand Held 12=Docking 14=Sub Notebook + foreach ($t in $types) { + if ($t -eq 8 -or $t -eq 9 -or $t -eq 10 -or $t -eq 11 -or $t -eq 14) { $script:IsLaptop = $true } + } + Set-Fact 'is_laptop' $script:IsLaptop +} + +# =========================================================================== +# SECURITY CHECKS +# =========================================================================== + +# --- Microsoft Defender --- +Invoke-Check -Id 'sec.defender' -Category 'security' -Title 'Microsoft Defender status' -Body { + $mp = $null + try { $mp = Get-MpComputerStatus -ErrorAction Stop } catch { $mp = $null } + if ($null -eq $mp) { + Add-Finding -Id 'sec.defender.unavailable' -Category 'security' -Severity 'warning' ` + -Title 'Defender status unavailable' ` + -Detail 'Get-MpComputerStatus returned nothing. Defender may be disabled, replaced by a 3rd-party AV, or the cmdlet is unavailable. Confirm an active AV exists (see security-center check).' ` + -Evidence 'Get-MpComputerStatus returned null' + Set-Fact 'defender' @{ available = $false } + return + } + + $rtp = [bool]$mp.RealTimeProtectionEnabled + $amsvc = [bool]$mp.AMServiceEnabled + $sigAge = 0 + try { $sigAge = [int]$mp.AntispywareSignatureAge } catch { $sigAge = -1 } + $tamper = $false + try { $tamper = [bool]$mp.IsTamperProtected } catch { $tamper = $false } + + Set-Fact 'defender' @{ + available = $true + real_time_protection = $rtp + am_service_enabled = $amsvc + antispyware_signature_age = $sigAge + tamper_protected = $tamper + antivirus_enabled = [bool]$mp.AntivirusEnabled + nis_enabled = [bool]$mp.NISEnabled + } + + $ev = "RealTimeProtectionEnabled=$rtp; AMServiceEnabled=$amsvc; AntispywareSignatureAge=$sigAge days; IsTamperProtected=$tamper" + + if (-not $rtp) { + Add-Finding -Id 'sec.defender.rtp_off' -Category 'security' -Severity 'critical' ` + -Title 'Defender real-time protection is OFF' ` + -Detail 'Real-time protection is disabled. The endpoint is unprotected against active threats. Re-enable immediately or confirm a managed 3rd-party AV is providing real-time protection.' ` + -Evidence $ev + } + if (-not $amsvc) { + Add-Finding -Id 'sec.defender.amservice_off' -Category 'security' -Severity 'critical' ` + -Title 'Defender antimalware service is not running' ` + -Detail 'The Defender antimalware service is not active. If no 3rd-party AV is present, this endpoint has no antivirus protection.' ` + -Evidence $ev + } + if ($sigAge -lt 0) { + # Sentinel: AntispywareSignatureAge could not be read. Never let this + # vanish silently - emit an explicit unknown so currency is flagged for + # manual review instead of being treated as "fine". + Add-Finding -Id 'sec.defender.sig_unknown' -Category 'security' -Severity 'unknown' ` + -Title 'Defender signature currency could not be determined' ` + -Detail 'AntispywareSignatureAge could not be read from Get-MpComputerStatus. Signature freshness is unknown. Verify manually (Get-MpComputerStatus, or Update-MpSignature to force a refresh).' ` + -Evidence $ev + } elseif ($sigAge -gt 7) { + Add-Finding -Id 'sec.defender.sig_stale' -Category 'security' -Severity 'warning' ` + -Title "Defender signatures are $sigAge days old" ` + -Detail 'Antispyware signatures are more than 7 days old. The endpoint may not detect recent threats. Force a signature update (Update-MpSignature).' ` + -Evidence $ev + } + if (-not $tamper) { + Add-Finding -Id 'sec.defender.tamper_off' -Category 'security' -Severity 'warning' ` + -Title 'Defender tamper protection is OFF' ` + -Detail 'Tamper protection is disabled, so malware or a local admin can silently disable Defender. Enable tamper protection (typically via Intune / Security Center).' ` + -Evidence $ev + } + if ($rtp -and $amsvc -and $sigAge -ge 0 -and $sigAge -le 7) { + Add-Finding -Id 'sec.defender.ok' -Category 'security' -Severity 'info' ` + -Title 'Defender active and current' ` + -Detail 'Real-time protection on, service running, signatures current.' ` + -Evidence $ev + } +} + +# --- 3rd-party AV via SecurityCenter2 --- +Invoke-Check -Id 'sec.av_products' -Category 'security' -Title 'Registered antivirus products' -Body { + $avList = @() + try { + $av = Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName AntiVirusProduct -ErrorAction Stop + } catch { + # SecurityCenter2 does not exist on Server SKUs; that is expected, not an error. + $av = $null + } + if ($null -eq $av) { + Set-Fact 'antivirus_products' @() + Add-Finding -Id 'sec.av_products.none_registered' -Category 'security' -Severity 'info' ` + -Title 'No AV products registered in Security Center' ` + -Detail 'SecurityCenter2 returned no AntiVirusProduct entries. This is normal on Windows Server SKUs (Security Center is a client feature). On a workstation, confirm Defender or a managed AV is active.' ` + -Evidence 'root\SecurityCenter2 AntiVirusProduct: none' + return + } + + $nonDefender = @() + foreach ($p in $av) { + $name = [string]$p.displayName + $avList += $name + if ($name -notmatch 'Windows Defender' -and $name -notmatch 'Microsoft Defender') { + $nonDefender += $name + } + } + Set-Fact 'antivirus_products' $avList + + if ($nonDefender.Count -gt 0) { + $names = ($nonDefender -join ', ') + Add-Finding -Id 'sec.av_products.third_party' -Category 'security' -Severity 'warning' ` + -Title "Third-party AV present: $names" ` + -Detail 'A non-Defender antivirus is registered. Running two real-time AV engines causes conflicts, performance loss, and detection gaps. Confirm the intended AV and ensure only one provides real-time protection.' ` + -Evidence ("Registered AV: " + ($avList -join ', ')) + } else { + Add-Finding -Id 'sec.av_products.defender_only' -Category 'security' -Severity 'info' ` + -Title 'Defender is the only registered AV' ` + -Detail 'Only Microsoft/Windows Defender is registered in Security Center.' ` + -Evidence ($avList -join ', ') + } +} + +# --- Leftover / competitor management agents (CRITICAL at onboarding) --- +Invoke-Check -Id 'sec.foreign_agents' -Category 'security' -Title 'Competitor / leftover management agents' -Body { + # name regex -> friendly label. + # IMPORTANT: anchor short/ambiguous tokens with \b word boundaries. Bare + # substrings cause false positives (e.g. 'cove' matches 'Recovery'/'Discovery', + # 'ltsvc' matches 'VaultSvc'). \b requires a word boundary on each side. + $patterns = @( + @{ rx = 'screenconnect|connectwise control|connectwisecontrol'; label = 'ScreenConnect / ConnectWise Control' }, + @{ rx = 'ninjarmm|ninja rmm'; label = 'NinjaRMM' }, + @{ rx = 'datto rmm|\bcagservice\b|\baemagent\b|centrastage'; label = 'Datto RMM' }, + @{ rx = '\batera\b|ateraagent'; label = 'Atera' }, + @{ rx = '\bkaseya\b|\bagentmon\b'; label = 'Kaseya' }, + @{ rx = '\bteamviewer\b'; label = 'TeamViewer' }, + @{ rx = '\blogmein\b'; label = 'LogMeIn' }, + @{ rx = '\banydesk\b'; label = 'AnyDesk' }, + @{ rx = '\bsplashtop\b'; label = 'Splashtop (SOS/Streamer)' }, + @{ rx = '\bn-able\b|\bnableagent\b|take control|\bbasupport\b'; label = 'N-able / Take Control' }, + @{ rx = '\bsyncro\b|\bkabuto\b'; label = 'Syncro / Kabuto' }, + @{ rx = '\baction1\b'; label = 'Action1' }, + @{ rx = '\blabtech\b|connectwise automate|\bltservice\b|\bltsvcmon\b'; label = 'ConnectWise Automate / LabTech' }, + @{ rx = '\bbeyondtrust\b|\bbomgar\b'; label = 'BeyondTrust / Bomgar' }, + @{ rx = '\bgotoassist\b|goto assist|goto resolve|\bgotoresolve\b'; label = 'GoToAssist / GoTo Resolve' }, + @{ rx = '\brustdesk\b'; label = 'RustDesk' }, + @{ rx = '\bpulseway\b'; label = 'Pulseway' }, + @{ rx = '\blevel\.io\b|\blevelio\b|level rmm'; label = 'Level.io' }, + @{ rx = '\bmanageengine\b|zoho assist|\bzohoassist\b'; label = 'ManageEngine / Zoho Assist' } + ) + + # Collect installed programs from both 64-bit and 32-bit uninstall hives. + $installed = @() + $uninstallPaths = @( + 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', + 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' + ) + foreach ($up in $uninstallPaths) { + try { + $items = Get-ItemProperty $up -ErrorAction SilentlyContinue + foreach ($it in $items) { + if ($it.DisplayName) { + $installed += [pscustomobject]@{ + Name = [string]$it.DisplayName + Version = [string]$it.DisplayVersion + Publisher = [string]$it.Publisher + } + } + } + } catch { } + } + + # Collect services (name + display name). + $svcRows = @() + try { + $svcRows = Get-CimInstance -ClassName Win32_Service -ErrorAction Stop | + Select-Object Name, DisplayName, State, StartMode + } catch { + try { $svcRows = Get-Service -ErrorAction SilentlyContinue | Select-Object Name, DisplayName } catch { $svcRows = @() } + } + + $hits = @() + foreach ($pat in $patterns) { + $rx = $pat.rx + $matchEvidence = @() + + foreach ($prog in $installed) { + if ($prog.Name -match $rx) { + $matchEvidence += ("program: " + $prog.Name + " " + $prog.Version) + } + } + foreach ($s in $svcRows) { + $sn = [string]$s.Name + $sd = [string]$s.DisplayName + if ($sn -match $rx -or $sd -match $rx) { + $st = '' + try { $st = [string]$s.State } catch { } + $matchEvidence += ("service: " + $sn + " (" + $sd + ") " + $st) + } + } + + if ($matchEvidence.Count -gt 0) { + $hits += [pscustomobject]@{ Label = $pat.label; Evidence = ($matchEvidence | Select-Object -Unique) } + } + } + + Set-Fact 'foreign_agents' ($hits | ForEach-Object { $_.Label }) + + if ($hits.Count -gt 0) { + foreach ($h in $hits) { + Add-Finding -Id ('sec.foreign_agents.' + ($h.Label -replace '[^A-Za-z0-9]+','_').ToLower()) ` + -Category 'security' -Severity 'critical' ` + -Title ('Foreign management/remote-access agent: ' + $h.Label) ` + -Detail 'A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.' ` + -Evidence ($h.Evidence -join "`n") + } + } else { + Add-Finding -Id 'sec.foreign_agents.none' -Category 'security' -Severity 'info' ` + -Title 'No competitor/leftover management agents detected' ` + -Detail 'No known competitor RMM or unmanaged remote-access agents found in installed programs or services.' ` + -Evidence 'Scanned uninstall hives (HKLM + WOW6432Node) and Win32_Service' + } +} + +# --- Firewall profiles --- +Invoke-Check -Id 'sec.firewall' -Category 'security' -Title 'Windows Firewall profiles' -Body { + $profiles = Get-NetFirewallProfile -ErrorAction Stop + $state = @{} + $disabled = @() + foreach ($p in $profiles) { + $en = [bool]$p.Enabled + $state[[string]$p.Name] = $en + if (-not $en) { $disabled += [string]$p.Name } + } + Set-Fact 'firewall_profiles' $state + + if ($disabled.Count -gt 0) { + Add-Finding -Id 'sec.firewall.disabled' -Category 'security' -Severity 'critical' ` + -Title ('Firewall disabled on profile(s): ' + ($disabled -join ', ')) ` + -Detail 'One or more firewall profiles are OFF. The endpoint is exposed to lateral movement and inbound attacks on those networks. Re-enable all profiles.' ` + -Evidence ("Profile states: " + (($state.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '; ')) + } else { + Add-Finding -Id 'sec.firewall.ok' -Category 'security' -Severity 'info' ` + -Title 'All firewall profiles enabled' ` + -Detail 'Domain, Private, and Public firewall profiles are all enabled.' ` + -Evidence (($state.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '; ') + } +} + +# --- BitLocker --- +Invoke-Check -Id 'sec.bitlocker' -Category 'security' -Title 'BitLocker on OS volume' -Body { + $sysDrive = $env:SystemDrive + if ([string]::IsNullOrEmpty($sysDrive)) { $sysDrive = 'C:' } + + $vol = $null + try { $vol = Get-BitLockerVolume -MountPoint $sysDrive -ErrorAction Stop } catch { $vol = $null } + if ($null -eq $vol) { + Add-Finding -Id 'sec.bitlocker.unavailable' -Category 'security' -Severity 'unknown' ` + -Title 'BitLocker status unavailable' ` + -Detail 'Get-BitLockerVolume failed for the OS volume. BitLocker may not be installed (Home edition) or the cmdlet is unavailable. Verify encryption manually (manage-bde -status).' ` + -Evidence "MountPoint=$sysDrive, Get-BitLockerVolume returned null" + Set-Fact 'bitlocker' @{ os_volume = $sysDrive; available = $false } + return + } + + $protection = [string]$vol.ProtectionStatus # On / Off + $pct = 0 + try { $pct = [int]$vol.EncryptionPercentage } catch { $pct = -1 } + $hasRecovery = $false + $protTypes = @() + try { + foreach ($kp in $vol.KeyProtector) { + $protTypes += [string]$kp.KeyProtectorType + if ([string]$kp.KeyProtectorType -match 'RecoveryPassword') { $hasRecovery = $true } + } + } catch { } + + Set-Fact 'bitlocker' @{ + os_volume = $sysDrive + available = $true + protection_status = $protection + encryption_percent = $pct + recovery_key_present = $hasRecovery + key_protectors = $protTypes + } + + $ev = "Volume=$sysDrive; ProtectionStatus=$protection; EncryptionPercentage=$pct; KeyProtectors=$($protTypes -join ',')" + + $encrypted = ($protection -eq 'On') + if (-not $encrypted) { + $sev = 'warning' + $extra = '' + if ($script:IsLaptop) { $sev = 'critical'; $extra = ' This is a laptop (portable chassis), so the data-at-rest risk if lost or stolen is high.' } + Add-Finding -Id 'sec.bitlocker.unencrypted' -Category 'security' -Severity $sev ` + -Title 'OS volume is NOT encrypted with BitLocker' ` + -Detail ('The operating system volume is unencrypted. Data is exposed if the disk is removed or the device is lost.' + $extra + ' Enable BitLocker and escrow the recovery key.') ` + -Evidence $ev + } else { + if (-not $hasRecovery) { + Add-Finding -Id 'sec.bitlocker.no_recovery' -Category 'security' -Severity 'warning' ` + -Title 'BitLocker on, but no recovery password protector' ` + -Detail 'The OS volume is encrypted but has no RecoveryPassword key protector. Without an escrowed recovery key, a TPM/boot change can permanently lock out the data. Add a recovery password and escrow it.' ` + -Evidence $ev + } else { + Add-Finding -Id 'sec.bitlocker.ok' -Category 'security' -Severity 'info' ` + -Title 'OS volume encrypted with recovery protector present' ` + -Detail 'BitLocker is on for the OS volume and a recovery password protector exists.' ` + -Evidence $ev + } + } +} + +# --- Local administrators / privileged accounts --- +Invoke-Check -Id 'sec.local_admins' -Category 'security' -Title 'Local administrators and privileged accounts' -Body { + $adminMembers = @() + try { + $members = Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop + foreach ($m in $members) { $adminMembers += [string]$m.Name } + } catch { + # Fallback via net localgroup if the cmdlet is unavailable. + try { + $raw = (net localgroup Administrators) 2>$null + $capture = $false + foreach ($line in $raw) { + if ($line -match '^----') { $capture = $true; continue } + if ($line -match 'completed successfully') { $capture = $false; continue } + if ($capture -and $line.Trim().Length -gt 0) { $adminMembers += $line.Trim() } + } + } catch { } + } + Set-Fact 'local_administrators' $adminMembers + + # Built-in Administrator (RID 500) enabled? + $builtinAdminEnabled = $null + $neverExpire = @() + try { + $users = Get-LocalUser -ErrorAction Stop + foreach ($u in $users) { + try { + if ([string]$u.SID -match '-500$') { $builtinAdminEnabled = [bool]$u.Enabled } + } catch { } + try { + if ($u.Enabled -and $u.PasswordNeverExpires) { $neverExpire += [string]$u.Name } + } catch { } + } + } catch { } + + Set-Fact 'builtin_admin_enabled' $builtinAdminEnabled + Set-Fact 'accounts_password_never_expires' $neverExpire + + Add-Finding -Id 'sec.local_admins.list' -Category 'security' -Severity 'info' ` + -Title ('Local administrators (' + $adminMembers.Count + ')') ` + -Detail 'Members of the local Administrators group. Review for unexpected or unknown accounts (especially leftover MSP/vendor accounts from a prior provider).' ` + -Evidence ($adminMembers -join "`n") + + if ($builtinAdminEnabled -eq $true) { + Add-Finding -Id 'sec.local_admins.builtin_enabled' -Category 'security' -Severity 'warning' ` + -Title 'Built-in Administrator account is enabled' ` + -Detail 'The built-in Administrator (RID 500) is enabled. It is a well-known target for brute force and lateral movement. Disable it or ensure it is managed by LAPS with a strong unique password.' ` + -Evidence 'Get-LocalUser SID ...-500 Enabled=True' + } + + if ($neverExpire.Count -gt 0) { + Add-Finding -Id 'sec.local_admins.never_expire' -Category 'security' -Severity 'warning' ` + -Title ('Enabled accounts with non-expiring passwords: ' + ($neverExpire -join ', ')) ` + -Detail 'These enabled local accounts have PasswordNeverExpires set. Non-expiring passwords increase the window of exposure if credentials leak. Review and apply a rotation policy or LAPS.' ` + -Evidence ($neverExpire -join "`n") + } +} + +# --- Patch posture --- +Invoke-Check -Id 'sec.patch' -Category 'security' -Title 'Patch posture and OS support' -Body { + # Last installed hotfix + $lastHotfix = $null + try { + $hf = Get-HotFix -ErrorAction Stop | Where-Object { $_.InstalledOn } | Sort-Object InstalledOn -Descending | Select-Object -First 1 + if ($hf) { + $lastHotfix = @{ + hotfix_id = [string]$hf.HotFixID + installed_on = $hf.InstalledOn.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + } + } + } catch { } + Set-Fact 'last_hotfix' $lastHotfix + + # Pending Windows Update count (COM). Best-effort; can be slow or blocked. + $pendingCount = $null + try { + $session = New-Object -ComObject Microsoft.Update.Session + $searcher = $session.CreateUpdateSearcher() + $result = $searcher.Search("IsInstalled=0 and IsHidden=0") + $pendingCount = [int]$result.Updates.Count + } catch { $pendingCount = $null } + Set-Fact 'pending_updates' $pendingCount + + # OS build + EOL map + $build = $script:OsInfo.build + $caption = $script:OsInfo.caption + Set-Fact 'os_build' $build + + # EOL map keyed by build number (consumer/Pro mainstream support cutoffs). + # Dates are end-of-servicing for that build. Kept small and explicit. + $eolMap = @{ + # Windows 10 builds + '10240' = @{ name='Win10 1507'; eol='2017-05-09' } + '10586' = @{ name='Win10 1511'; eol='2017-10-10' } + '14393' = @{ name='Win10 1607'; eol='2018-04-10' } + '15063' = @{ name='Win10 1703'; eol='2018-10-09' } + '16299' = @{ name='Win10 1709'; eol='2019-04-09' } + '17134' = @{ name='Win10 1803'; eol='2019-11-12' } + '17763' = @{ name='Win10 1809'; eol='2020-11-10' } + '18362' = @{ name='Win10 1903'; eol='2019-12-08' } + '18363' = @{ name='Win10 1909'; eol='2021-05-11' } + '19041' = @{ name='Win10 2004'; eol='2021-12-14' } + '19042' = @{ name='Win10 20H2'; eol='2022-05-10' } + '19043' = @{ name='Win10 21H1'; eol='2022-12-13' } + '19044' = @{ name='Win10 21H2'; eol='2023-06-13' } + '19045' = @{ name='Win10 22H2'; eol='2025-10-14' } + # Windows 11 builds + '22000' = @{ name='Win11 21H2'; eol='2023-10-10' } + '22621' = @{ name='Win11 22H2'; eol='2024-10-08' } + '22631' = @{ name='Win11 23H2'; eol='2025-11-11' } + '26100' = @{ name='Win11 24H2'; eol='2026-10-13' } + '26200' = @{ name='Win11 25H2'; eol='2027-10-12' } + } + + $now = (Get-Date).ToUniversalTime() + $eolEntry = $null + if ($eolMap.ContainsKey([string]$build)) { $eolEntry = $eolMap[[string]$build] } + + if ($eolEntry) { + $eolDate = $null + try { $eolDate = [datetime]::ParseExact($eolEntry.eol, 'yyyy-MM-dd', $null) } catch { } + Set-Fact 'os_eol' @{ release=$eolEntry.name; eol_date=$eolEntry.eol } + if ($eolDate -and $now -gt $eolDate) { + Add-Finding -Id 'sec.patch.os_eol' -Category 'security' -Severity 'critical' ` + -Title ('OS build is end-of-life: ' + $eolEntry.name) ` + -Detail ('This OS build (' + $build + ', ' + $eolEntry.name + ') passed end-of-servicing on ' + $eolEntry.eol + '. It no longer receives security updates. Plan a feature update or OS upgrade.') ` + -Evidence "$caption build $build; EOL $($eolEntry.eol)" + } elseif ($eolDate -and $now -gt $eolDate.AddDays(-90)) { + Add-Finding -Id 'sec.patch.os_eol_soon' -Category 'security' -Severity 'warning' ` + -Title ('OS build nearing end-of-life: ' + $eolEntry.name) ` + -Detail ('This OS build reaches end-of-servicing on ' + $eolEntry.eol + ' (within 90 days). Schedule a feature update.') ` + -Evidence "$caption build $build; EOL $($eolEntry.eol)" + } else { + Add-Finding -Id 'sec.patch.os_supported' -Category 'security' -Severity 'info' ` + -Title ('OS build supported: ' + $eolEntry.name) ` + -Detail ('Build ' + $build + ' (' + $eolEntry.name + ') is in support until ' + $eolEntry.eol + '.') ` + -Evidence "$caption build $build" + } + } else { + Add-Finding -Id 'sec.patch.os_build_unknown' -Category 'security' -Severity 'unknown' ` + -Title ('OS build not in EOL map: ' + $build) ` + -Detail 'The build number is not in the local EOL reference map. Verify support status manually. This may be a Server SKU or a build newer than the map.' ` + -Evidence "$caption build $build" + } + + if ($pendingCount -ne $null -and $pendingCount -gt 0) { + $sev = 'warning' + Add-Finding -Id 'sec.patch.pending' -Category 'security' -Severity $sev ` + -Title ($pendingCount.ToString() + ' pending Windows updates') ` + -Detail 'Windows Update reports pending (not installed, not hidden) updates. Some may be security updates. Approve/install on the next maintenance window.' ` + -Evidence "Microsoft.Update.Session search IsInstalled=0 and IsHidden=0 -> $pendingCount" + } + + if ($lastHotfix) { + Add-Finding -Id 'sec.patch.last_hotfix' -Category 'security' -Severity 'info' ` + -Title ('Last hotfix: ' + $lastHotfix.hotfix_id) ` + -Detail 'Most recently installed update (from Get-HotFix; reflects CBS/MSU packages, not all cumulative metadata).' ` + -Evidence ("$($lastHotfix.hotfix_id) installed $($lastHotfix.installed_on)") + } +} + +# --- Exposure surface: RDP/NLA, SMBv1, UAC, LAPS --- +Invoke-Check -Id 'sec.exposure' -Category 'security' -Title 'Exposure surface (RDP, SMBv1, UAC, LAPS)' -Body { + $exposure = @{} + + # RDP enabled? fDenyTSConnections = 0 means RDP enabled. + $rdpEnabled = $null + try { + $deny = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name 'fDenyTSConnections' -ErrorAction Stop).fDenyTSConnections + $rdpEnabled = ($deny -eq 0) + } catch { $rdpEnabled = $null } + $exposure['rdp_enabled'] = $rdpEnabled + + # NLA (UserAuthentication = 1 means NLA required) + $nla = $null + try { + $ua = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -ErrorAction Stop).UserAuthentication + $nla = ($ua -eq 1) + } catch { $nla = $null } + $exposure['rdp_nla'] = $nla + + if ($rdpEnabled -eq $true) { + if ($nla -eq $false) { + Add-Finding -Id 'sec.exposure.rdp_no_nla' -Category 'security' -Severity 'critical' ` + -Title 'RDP enabled WITHOUT Network Level Authentication' ` + -Detail 'RDP is on and NLA is not required. This exposes the logon screen pre-auth and is vulnerable to pre-auth exploits and brute force. Require NLA, restrict RDP to VPN/allow-listed IPs, or disable RDP.' ` + -Evidence "fDenyTSConnections=0; UserAuthentication=0" + } else { + Add-Finding -Id 'sec.exposure.rdp_on' -Category 'security' -Severity 'warning' ` + -Title 'RDP is enabled' ` + -Detail 'Remote Desktop is enabled (NLA required). Confirm it is restricted to VPN or specific source IPs and not exposed to the internet.' ` + -Evidence "fDenyTSConnections=0; UserAuthentication=$([int]($nla -eq $true))" + } + } + + # SMBv1 + $smb1 = $null + try { + $cfg = Get-SmbServerConfiguration -ErrorAction Stop + $smb1 = [bool]$cfg.EnableSMB1Protocol + } catch { $smb1 = $null } + $exposure['smb1_enabled'] = $smb1 + if ($smb1 -eq $true) { + Add-Finding -Id 'sec.exposure.smb1' -Category 'security' -Severity 'critical' ` + -Title 'SMBv1 is ENABLED' ` + -Detail 'SMBv1 is an obsolete, insecure protocol (WannaCry/EternalBlue vector). Disable it: Set-SmbServerConfiguration -EnableSMB1Protocol $false and remove the SMB1 feature.' ` + -Evidence 'Get-SmbServerConfiguration EnableSMB1Protocol=True' + } elseif ($smb1 -eq $false) { + Add-Finding -Id 'sec.exposure.smb1_off' -Category 'security' -Severity 'info' ` + -Title 'SMBv1 disabled' ` + -Detail 'SMBv1 server protocol is disabled.' ` + -Evidence 'EnableSMB1Protocol=False' + } + + # UAC (EnableLUA = 1 means UAC on) + $uac = $null + try { + $lua = (Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name 'EnableLUA' -ErrorAction Stop).EnableLUA + $uac = ($lua -eq 1) + } catch { $uac = $null } + $exposure['uac_enabled'] = $uac + if ($uac -eq $false) { + Add-Finding -Id 'sec.exposure.uac_off' -Category 'security' -Severity 'critical' ` + -Title 'UAC is disabled' ` + -Detail 'User Account Control is turned off (EnableLUA=0). All processes run elevated silently, removing a key barrier against malware. Re-enable UAC.' ` + -Evidence 'EnableLUA=0' + } + + # LAPS presence (legacy AdmPwd or Windows LAPS) + $lapsPresent = $false + $lapsEvidence = @() + try { + if (Test-Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\LAPS') { $lapsPresent = $true; $lapsEvidence += 'Windows LAPS reg key' } + } catch { } + try { + if (Test-Path 'HKLM:\Software\Policies\Microsoft Services\AdmPwd') { $lapsPresent = $true; $lapsEvidence += 'Legacy AdmPwd policy' } + } catch { } + try { + $lapsDll = "$env:ProgramFiles\LAPS\CSE\AdmPwd.dll" + if (Test-Path $lapsDll) { $lapsPresent = $true; $lapsEvidence += 'AdmPwd CSE installed' } + } catch { } + try { + $lapsSvc = Get-Service -Name 'LAPS' -ErrorAction SilentlyContinue + if ($lapsSvc) { $lapsPresent = $true; $lapsEvidence += 'LAPS service' } + } catch { } + $exposure['laps_present'] = $lapsPresent + + if (-not $lapsPresent) { + Add-Finding -Id 'sec.exposure.no_laps' -Category 'security' -Severity 'info' ` + -Title 'LAPS not detected' ` + -Detail 'No LAPS (Windows LAPS or legacy AdmPwd) detected. Without LAPS, the local admin password is likely static/shared across the fleet. Consider deploying LAPS to randomize and escrow local admin passwords.' ` + -Evidence 'No LAPS registry keys, CSE, or service found' + } else { + Add-Finding -Id 'sec.exposure.laps_present' -Category 'security' -Severity 'info' ` + -Title 'LAPS detected' ` + -Detail 'A LAPS mechanism is present.' ` + -Evidence ($lapsEvidence -join '; ') + } + + Set-Fact 'exposure' $exposure +} + +# =========================================================================== +# HEALTH CHECKS +# =========================================================================== + +# --- Disks: free space + SMART/reliability --- +Invoke-Check -Id 'health.disk_space' -Category 'health' -Title 'Disk free space' -Body { + $volRows = @() + $vols = $null + try { $vols = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveType -eq 'Fixed' -and $_.Size -gt 0 } } catch { $vols = $null } + if ($null -eq $vols) { + # Fallback to Win32_LogicalDisk (DriveType 3 = fixed) + $ld = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue + foreach ($d in $ld) { + $size = [double]$d.Size + if ($size -le 0) { continue } + $free = [double]$d.FreeSpace + $pct = [math]::Round(($free / $size) * 100, 1) + $volRows += [pscustomobject]@{ Drive=[string]$d.DeviceID; FreeGB=[math]::Round($free/1GB,1); SizeGB=[math]::Round($size/1GB,1); FreePct=$pct } + } + } else { + foreach ($v in $vols) { + $size = [double]$v.Size + if ($size -le 0) { continue } + $free = [double]$v.SizeRemaining + $pct = [math]::Round(($free / $size) * 100, 1) + $letter = [string]$v.DriveLetter + $hasLetter = -not [string]::IsNullOrEmpty($letter) + if ($hasLetter) { $drv = $letter + ':' } + elseif (-not [string]::IsNullOrEmpty([string]$v.FileSystemLabel)) { $drv = '[' + [string]$v.FileSystemLabel + ']' } + else { $drv = '[unlabeled]' } + $volRows += [pscustomobject]@{ Drive=$drv; HasLetter=$hasLetter; FreeGB=[math]::Round($free/1GB,1); SizeGB=[math]::Round($size/1GB,1); FreePct=$pct } + } + } + + Set-Fact 'volumes' ($volRows | ForEach-Object { @{ drive=$_.Drive; free_gb=$_.FreeGB; size_gb=$_.SizeGB; free_pct=$_.FreePct } }) + + foreach ($r in $volRows) { + # Only alert on lettered (user/data) volumes. Unlettered partitions + # (EFI, recovery, WinRE) are near-full by design and not actionable. + $rHasLetter = $true + try { $rHasLetter = [bool]$r.HasLetter } catch { $rHasLetter = $true } + if (-not $rHasLetter) { continue } + + $ev = "$($r.Drive) free $($r.FreeGB) GB of $($r.SizeGB) GB ($($r.FreePct)%)" + # Finding id MUST be severity-independent (id keyed only on the drive, not + # the tier) so the runner's diff can see a warn->crit transition on the + # same drive as a REGRESSION rather than a resolved+new pair. The tier + # lives only in the -Severity field. + $driveKey = ($r.Drive -replace '[^A-Za-z0-9]','') + if ($r.FreePct -lt 8) { + Add-Finding -Id ("health.disk_space." + $driveKey) -Category 'health' -Severity 'critical' ` + -Title ("Disk critically low: $($r.Drive) at $($r.FreePct)% free") ` + -Detail 'Less than 8 percent free. Risk of failed updates, crashes, and corruption. Free space or expand the volume urgently.' ` + -Evidence $ev + } elseif ($r.FreePct -lt 15) { + Add-Finding -Id ("health.disk_space." + $driveKey) -Category 'health' -Severity 'warning' ` + -Title ("Disk low: $($r.Drive) at $($r.FreePct)% free") ` + -Detail 'Less than 15 percent free. Plan cleanup or expansion.' ` + -Evidence $ev + } + } +} + +Invoke-Check -Id 'health.disk_smart' -Category 'health' -Title 'Physical disk health (SMART)' -Body { + $disks = $null + try { $disks = Get-PhysicalDisk -ErrorAction Stop } catch { $disks = $null } + if ($null -eq $disks) { + Add-Finding -Id 'health.disk_smart.unavailable' -Category 'health' -Severity 'unknown' ` + -Title 'Physical disk health unavailable' ` + -Detail 'Get-PhysicalDisk is unavailable (older OS / RAID controller hiding disks). Verify drive health via vendor tools.' ` + -Evidence 'Get-PhysicalDisk returned null' + return + } + $diskFacts = @() + foreach ($d in $disks) { + $hs = [string]$d.HealthStatus + $model = [string]$d.FriendlyName + $diskFacts += @{ model=$model; health=$hs; media_type=[string]$d.MediaType } + + # Reliability counters (best-effort; not all disks expose them) + $relEv = '' + try { + $rel = $d | Get-StorageReliabilityCounter -ErrorAction Stop + if ($rel) { + $relEv = "Wear=$($rel.Wear); ReadErrorsTotal=$($rel.ReadErrorsTotal); Temperature=$($rel.Temperature)" + } + } catch { } + + if ($hs -and $hs -ne 'Healthy') { + # Severity-independent id (keyed on disk model only, not the health + # tier) so the runner diff treats a health change on the same disk as + # a regression, not new+resolved. Tier lives only in -Severity. + Add-Finding -Id ("health.disk_smart." + ($model -replace '[^A-Za-z0-9]+','_').ToLower()) -Category 'health' -Severity 'critical' ` + -Title ("Disk not healthy: $model ($hs)") ` + -Detail 'A physical disk reports a non-Healthy SMART/health status. Imminent failure risk. Back up immediately and plan replacement.' ` + -Evidence (("HealthStatus=$hs; " + $relEv).Trim()) + } + } + Set-Fact 'physical_disks' $diskFacts +} + +# --- Stability: unexpected shutdowns, BSOD, disk errors (last 14 days) --- +Invoke-Check -Id 'health.stability' -Category 'health' -Title 'Stability events (14 days)' -Body { + $start = (Get-Date).AddDays(-14) + + function Count-Events { + param([string]$LogName, [int[]]$Ids, [string]$Source) + $filter = @{ LogName = $LogName; StartTime = $start; Id = $Ids } + $cnt = 0 + try { + $evts = Get-WinEvent -FilterHashtable $filter -ErrorAction Stop + if ($Source) { $evts = $evts | Where-Object { $_.ProviderName -eq $Source } } + $cnt = @($evts).Count + } catch { + # "No events found" throws on PS5.1; treat as zero. + $cnt = 0 + } + return $cnt + } + + $unexpected = Count-Events -LogName 'System' -Ids @(41) + $bugcheck = Count-Events -LogName 'System' -Ids @(1001) -Source 'Microsoft-Windows-WER-SystemErrorReporting' + if ($bugcheck -eq 0) { $bugcheck = Count-Events -LogName 'System' -Ids @(1001) -Source 'BugCheck' } + $diskErr = Count-Events -LogName 'System' -Ids @(7,51,153) -Source 'disk' + if ($diskErr -eq 0) { $diskErr = Count-Events -LogName 'System' -Ids @(7,51,153) } + + Set-Fact 'stability_14d' @{ + unexpected_shutdowns = $unexpected + bugchecks = $bugcheck + disk_errors = $diskErr + } + + $report = "Unexpected shutdowns (id 41)=$unexpected; Bugchecks/BSOD (id 1001)=$bugcheck; Disk errors (id 7/51/153)=$diskErr" + + if ($unexpected -ge 3 -or $bugcheck -ge 3 -or $diskErr -ge 3) { + Add-Finding -Id 'health.stability.recurring' -Category 'health' -Severity 'critical' ` + -Title 'Recurring stability events in the last 14 days' ` + -Detail 'Three or more of one event class (unexpected shutdown, BSOD, or disk error) in 14 days indicates a hardware or driver problem. Investigate memory, disk, PSU, and drivers.' ` + -Evidence $report + } elseif ($unexpected -gt 0 -or $bugcheck -gt 0 -or $diskErr -gt 0) { + Add-Finding -Id 'health.stability.some' -Category 'health' -Severity 'warning' ` + -Title 'Stability events present in the last 14 days' ` + -Detail 'One or more unexpected shutdowns, BSODs, or disk errors occurred recently. Monitor and correlate with user reports.' ` + -Evidence $report + } else { + Add-Finding -Id 'health.stability.clean' -Category 'health' -Severity 'info' ` + -Title 'No stability events in the last 14 days' ` + -Detail 'No unexpected shutdowns, BSODs, or disk errors logged.' ` + -Evidence $report + } +} + +# --- Pending reboot + uptime + failed auto-start services --- +Invoke-Check -Id 'health.reboot_uptime' -Category 'health' -Title 'Pending reboot and uptime' -Body { + $pending = $false + $reasons = @() + try { if (Test-Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') { $pending = $true; $reasons += 'CBS RebootPending' } } catch { } + try { if (Test-Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') { $pending = $true; $reasons += 'WU RebootRequired' } } catch { } + try { + $pfro = (Get-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction Stop).PendingFileRenameOperations + if ($pfro) { $pending = $true; $reasons += 'PendingFileRenameOperations' } + } catch { } + Set-Fact 'pending_reboot' $pending + + if ($pending) { + Add-Finding -Id 'health.reboot_uptime.pending' -Category 'health' -Severity 'warning' ` + -Title 'Reboot pending' ` + -Detail 'A reboot is pending. Pending reboots can block patches and leave the system in a half-updated state. Schedule a restart.' ` + -Evidence ($reasons -join '; ') + } + + # Uptime + try { + $lastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime + $uptimeDays = [math]::Round(((Get-Date) - $lastBoot).TotalDays, 1) + Set-Fact 'uptime_days' $uptimeDays + if ($uptimeDays -gt 30) { + Add-Finding -Id 'health.reboot_uptime.long_uptime' -Category 'health' -Severity 'warning' ` + -Title ("Uptime is $uptimeDays days") ` + -Detail 'Uptime exceeds 30 days. Long uptime usually means pending updates have not been applied (reboots deferred). Schedule maintenance.' ` + -Evidence ("LastBootUpTime=" + $lastBoot.ToString('u')) + } + } catch { } +} + +Invoke-Check -Id 'health.failed_services' -Category 'health' -Title 'Failed auto-start services' -Body { + $failed = @() + try { + $svcs = Get-CimInstance -ClassName Win32_Service -ErrorAction Stop | + Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' } + foreach ($s in $svcs) { + # Ignore services with delayed start that are simply not yet started? Keep them; they should be running. + $failed += [pscustomobject]@{ Name=[string]$s.Name; Display=[string]$s.DisplayName; State=[string]$s.State } + } + } catch { } + + # Filter out known benign trigger-start services masquerading as Auto in some images. + $benign = 'gupdate|gupdatem|MapsBroker|RemoteRegistry|sppsvc|edgeupdate|edgeupdatem|cbdhsvc|WbioSrvc' + $real = @() + foreach ($f in $failed) { + if ($f.Name -notmatch $benign) { $real += $f } + } + Set-Fact 'failed_autostart_services' ($real | ForEach-Object { @{ name=$_.Name; display=$_.Display; state=$_.State } }) + + if ($real.Count -gt 0) { + $list = ($real | ForEach-Object { "$($_.Name) ($($_.Display)) = $($_.State)" }) -join "`n" + Add-Finding -Id 'health.failed_services.stopped' -Category 'health' -Severity 'warning' ` + -Title ($real.Count.ToString() + ' auto-start service(s) not running') ` + -Detail 'These services are set to start automatically but are not running. Some may be benign; review for security agents, backup agents, or AV that should be running.' ` + -Evidence $list + } else { + Add-Finding -Id 'health.failed_services.ok' -Category 'health' -Severity 'info' ` + -Title 'All auto-start services running' ` + -Detail 'No automatic-start services found stopped (excluding known trigger-start/update services).' ` + -Evidence 'Win32_Service StartMode=Auto State!=Running -> none significant' + } +} + +# --- Domain secure channel + time skew --- +Invoke-Check -Id 'health.domain' -Category 'health' -Title 'Domain membership and secure channel' -Body { + $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop + $partOfDomain = [bool]$cs.PartOfDomain + $domain = [string]$cs.Domain + Set-Fact 'domain_joined' $partOfDomain + Set-Fact 'domain' $domain + + if ($partOfDomain) { + $scOk = $null + try { $scOk = Test-ComputerSecureChannel -ErrorAction Stop } catch { $scOk = $null } + Set-Fact 'secure_channel_ok' $scOk + if ($scOk -eq $false) { + Add-Finding -Id 'health.domain.secure_channel_broken' -Category 'health' -Severity 'critical' ` + -Title 'Domain secure channel is BROKEN' ` + -Detail 'Test-ComputerSecureChannel returned false. The machine trust relationship with the domain is broken (Group Policy, Kerberos, and domain logon will fail). Repair with Test-ComputerSecureChannel -Repair or rejoin.' ` + -Evidence "PartOfDomain=True; Test-ComputerSecureChannel=False; Domain=$domain" + } elseif ($scOk -eq $true) { + Add-Finding -Id 'health.domain.secure_channel_ok' -Category 'health' -Severity 'info' ` + -Title 'Domain secure channel healthy' ` + -Detail 'Machine trust relationship with the domain is intact.' ` + -Evidence "Domain=$domain" + } + } else { + Add-Finding -Id 'health.domain.workgroup' -Category 'health' -Severity 'info' ` + -Title 'Not domain-joined (workgroup)' ` + -Detail ('This machine is in workgroup/Azure AD only mode (Domain=' + $domain + '). No on-prem AD secure channel applies.') ` + -Evidence "PartOfDomain=False; Domain=$domain" + } +} + +Invoke-Check -Id 'health.time' -Category 'health' -Title 'Time service status' -Body { + # w32tm /query /status is non-blocking (unlike /stripchart). Parse leniently. + $statusText = '' + try { $statusText = (w32tm /query /status 2>&1 | Out-String) } catch { $statusText = '' } + $source = '' + try { + $src = (w32tm /query /source 2>&1 | Out-String).Trim() + if ($src) { $source = $src } + } catch { } + Set-Fact 'time_source' $source + + if ($statusText -match 'Local CMOS Clock' -or $source -match 'Local CMOS Clock') { + Add-Finding -Id 'health.time.local_cmos' -Category 'health' -Severity 'warning' ` + -Title 'Time source is local CMOS clock (not NTP)' ` + -Detail 'The system is not syncing time from an NTP source. Clock drift breaks Kerberos and certificate validation. Configure a reliable time source (domain hierarchy or pool.ntp.org).' ` + -Evidence ("Source=$source") + } else { + Add-Finding -Id 'health.time.source' -Category 'health' -Severity 'info' ` + -Title 'Time service source' ` + -Detail 'Current Windows Time service source.' ` + -Evidence ("Source=$source") + } +} + +# --- Battery health (laptops) --- +Invoke-Check -Id 'health.battery' -Category 'health' -Title 'Battery health' -Body { + if (-not $script:IsLaptop) { + Set-Fact 'battery' @{ present = $false } + return + } + $bat = Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue + if ($null -eq $bat) { + Set-Fact 'battery' @{ present = $false } + Add-Finding -Id 'health.battery.none' -Category 'health' -Severity 'info' ` + -Title 'Laptop chassis but no battery reported' ` + -Detail 'Chassis indicates a portable device but Win32_Battery returned nothing (battery removed, or detection failed).' ` + -Evidence 'Win32_Battery returned null' + return + } + $b = $bat | Select-Object -First 1 + $charge = '' + try { $charge = [string]$b.EstimatedChargeRemaining } catch { } + Set-Fact 'battery' @{ present = $true; estimated_charge_remaining = $charge; status = [string]$b.BatteryStatus } + Add-Finding -Id 'health.battery.present' -Category 'health' -Severity 'info' ` + -Title 'Battery present' ` + -Detail 'Battery detected. (Wear-level / design-vs-full-capacity requires a powercfg battery report, not collected here.)' ` + -Evidence ("EstimatedChargeRemaining=$charge%; BatteryStatus=$($b.BatteryStatus)") +} + +# --- Backup agent presence --- +Invoke-Check -Id 'health.backup' -Category 'health' -Title 'Backup agent presence' -Body { + # Anchor with \b to avoid substring false positives (e.g. bare 'cove' + # matched 'Recovery'/'Discovery' service display names). + $patterns = @( + @{ rx='datto.*workplace|datto.*backup'; label='Datto Workplace' }, + @{ rx='\bmsp360\b|\bcloudberry\b'; label='MSP360 / CloudBerry' }, + @{ rx='\bveeam\b'; label='Veeam' }, + @{ rx='\bacronis\b'; label='Acronis' }, + @{ rx='\bcarbonite\b'; label='Carbonite' }, + @{ rx='\bbackblaze\b'; label='Backblaze' }, + @{ rx='shadowprotect|storagecraft'; label='ShadowProtect / StorageCraft' }, + @{ rx='\baxcient\b|\breplibit\b|\bx360\b'; label='Axcient' }, + @{ rx='\bcove\b|n-able backup'; label='Cove / N-able Backup' }, + @{ rx='\bveritas\b|backup exec|\bbackupexec\b'; label='Veritas / Backup Exec' }, + @{ rx='datto.*siris|datto.*alto|\bsiris\b'; label='Datto SIRIS/ALTO agent' }, + @{ rx='comet backup|\bcometbackup\b|\bcomet-backup\b'; label='Comet Backup' }, + @{ rx='\bdruva\b|\binsync\b'; label='Druva' } + ) + $svcRows = @() + try { $svcRows = Get-CimInstance -ClassName Win32_Service -ErrorAction Stop | Select-Object Name, DisplayName, State } catch { } + + $found = @() + foreach ($pat in $patterns) { + foreach ($s in $svcRows) { + $sn = [string]$s.Name; $sd = [string]$s.DisplayName + if ($sn -match $pat.rx -or $sd -match $pat.rx) { + $found += [pscustomobject]@{ Label=$pat.label; Service=$sn; Display=$sd; State=[string]$s.State } + } + } + } + Set-Fact 'backup_agents' ($found | ForEach-Object { @{ label=$_.Label; service=$_.Service; state=$_.State } }) + + if (@($found).Count -gt 0) { + $running = @($found | Where-Object { $_.State -eq 'Running' }) + $list = ($found | ForEach-Object { "$($_.Label): $($_.Service) = $($_.State)" }) -join "`n" + if ($running.Count -gt 0) { + Add-Finding -Id 'health.backup.present' -Category 'health' -Severity 'info' ` + -Title 'Backup agent installed and running' ` + -Detail 'A backup agent service is present and running. Confirm the backup is actually configured and reporting successful jobs (presence != working backup).' ` + -Evidence $list + } else { + Add-Finding -Id 'health.backup.stopped' -Category 'health' -Severity 'warning' ` + -Title 'Backup agent installed but NOT running' ` + -Detail 'A backup agent service exists but is not running. Backups may not be occurring. Investigate.' ` + -Evidence $list + } + } else { + Add-Finding -Id 'health.backup.none' -Category 'health' -Severity 'info' ` + -Title 'No backup agent detected' ` + -Detail 'No known backup agent service found. Backup expectation varies by endpoint; confirm whether this machine is supposed to have local/cloud backup and whether server-side or M365 backup covers it.' ` + -Evidence 'No matching backup service in Win32_Service' + } +} + +# =========================================================================== +# INVENTORY BASELINE (facts, info-level) +# =========================================================================== + +Invoke-Check -Id 'inv.hardware' -Category 'inventory' -Title 'Hardware inventory' -Body { + $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue + $bios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction SilentlyContinue + $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1 + + $hw = @{} + if ($cs) { + $hw['manufacturer'] = [string]$cs.Manufacturer + $hw['model'] = [string]$cs.Model + $hw['ram_gb'] = [math]::Round(([double]$cs.TotalPhysicalMemory)/1GB, 1) + } + if ($bios) { + $hw['serial'] = [string]$bios.SerialNumber + $hw['bios_version'] = [string]($bios.SMBIOSBIOSVersion) + try { $hw['bios_date'] = $bios.ReleaseDate.ToUniversalTime().ToString('yyyy-MM-dd') } catch { } + } + if ($cpu) { + $hw['cpu'] = [string]$cpu.Name + $hw['cpu_cores'] = [int]$cpu.NumberOfCores + $hw['cpu_logical']= [int]$cpu.NumberOfLogicalProcessors + } + Set-Fact 'hardware' $hw +} + +Invoke-Check -Id 'inv.tpm_secureboot' -Category 'inventory' -Title 'TPM and Secure Boot' -Body { + $tpm = @{} + try { + $t = Get-Tpm -ErrorAction Stop + $tpm['present'] = [bool]$t.TpmPresent + $tpm['ready'] = [bool]$t.TpmReady + $tpm['enabled'] = [bool]$t.TpmEnabled + } catch { $tpm['present'] = $null; $tpm['error'] = 'Get-Tpm unavailable' } + Set-Fact 'tpm' $tpm + + $sb = $null + try { $sb = Confirm-SecureBootUEFI -ErrorAction Stop } catch { $sb = $null } + Set-Fact 'secure_boot' $sb +} + +Invoke-Check -Id 'inv.activation' -Category 'inventory' -Title 'OS edition and activation' -Body { + $lic = $null + try { + $lic = Get-CimInstance -ClassName SoftwareLicensingProduct -ErrorAction Stop | + Where-Object { $_.PartialProductKey -and $_.ApplicationID -eq '55c92734-d682-4d71-983e-d6ec3f16059f' } | + Select-Object -First 1 + } catch { $lic = $null } + $act = @{} + if ($lic) { + $st = [int]$lic.LicenseStatus # 1 = licensed + $act['license_status_code'] = $st + $act['licensed'] = ($st -eq 1) + $act['description'] = [string]$lic.Description + } else { + $act['licensed'] = $null + } + $act['edition'] = $script:OsInfo.caption + Set-Fact 'activation' $act +} + +Invoke-Check -Id 'inv.software' -Category 'inventory' -Title 'Installed software' -Body { + $sw = @() + $paths = @( + 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', + 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' + ) + foreach ($p in $paths) { + try { + $items = Get-ItemProperty $p -ErrorAction SilentlyContinue + foreach ($it in $items) { + if ($it.DisplayName) { + $sw += [pscustomobject]@{ + name = [string]$it.DisplayName + version = [string]$it.DisplayVersion + publisher = [string]$it.Publisher + } + } + } + } catch { } + } + # De-dup by name+version, sort by name. + $uniq = $sw | Sort-Object name, version -Unique + Set-Fact 'installed_software' ($uniq | ForEach-Object { @{ name=$_.name; version=$_.version; publisher=$_.publisher } }) + Set-Fact 'installed_software_count' @($uniq).Count +} + +Invoke-Check -Id 'inv.users' -Category 'inventory' -Title 'Local users and groups' -Body { + $users = @() + try { + foreach ($u in (Get-LocalUser -ErrorAction Stop)) { + $ll = '' + try { if ($u.LastLogon) { $ll = $u.LastLogon.ToString('yyyy-MM-dd') } } catch { } + $users += @{ + name = [string]$u.Name + enabled = [bool]$u.Enabled + password_never_expires= [bool]$u.PasswordNeverExpires + last_logon = $ll + } + } + } catch { } + Set-Fact 'local_users' $users + + $groups = @() + try { + foreach ($g in (Get-LocalGroup -ErrorAction Stop)) { $groups += [string]$g.Name } + } catch { } + Set-Fact 'local_groups' $groups +} + +Invoke-Check -Id 'inv.network' -Category 'inventory' -Title 'Network configuration' -Body { + $net = @() + try { + $cfgs = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=True' -ErrorAction Stop + foreach ($c in $cfgs) { + $net += @{ + description = [string]$c.Description + mac = [string]$c.MACAddress + ip = @($c.IPAddress) + gateway = @($c.DefaultIPGateway) + dns = @($c.DNSServerSearchOrder) + dhcp = [bool]$c.DHCPEnabled + } + } + } catch { } + Set-Fact 'network_adapters' $net +} + +Invoke-Check -Id 'inv.autoruns' -Category 'inventory' -Title 'Autoruns (Run keys) and scheduled tasks' -Body { + # Run keys (HKLM + HKCU + WOW6432Node) + $runEntries = @() + $runKeys = @( + 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run', + 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run', + 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run', + 'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce' + ) + foreach ($rk in $runKeys) { + try { + if (Test-Path $rk) { + $props = Get-ItemProperty -Path $rk -ErrorAction SilentlyContinue + foreach ($prop in $props.PSObject.Properties) { + if ($prop.Name -notmatch '^PS') { + $runEntries += @{ key=$rk; name=$prop.Name; value=[string]$prop.Value } + } + } + } + } catch { } + } + Set-Fact 'autoruns_run_keys' $runEntries + + # Scheduled tasks (non-Microsoft, enabled) snapshot + $taskList = @() + try { + $tasks = Get-ScheduledTask -ErrorAction Stop | + Where-Object { $_.State -ne 'Disabled' -and $_.TaskPath -notmatch '^\\Microsoft\\' } + foreach ($t in $tasks) { + $taskList += @{ name=[string]$t.TaskName; path=[string]$t.TaskPath; state=[string]$t.State } + } + } catch { } + Set-Fact 'scheduled_tasks' $taskList + Set-Fact 'scheduled_tasks_count' @($taskList).Count +} + +# --------------------------------------------------------------------------- +# Assemble and emit +# --------------------------------------------------------------------------- +$report = [ordered]@{ + host = $HostName + collected_at_utc = $CollectedAtUtc + os = $OsInfo + facts = $Facts + findings = @($Findings) +} + +$json = '' +try { + $json = $report | ConvertTo-Json -Depth 12 -Compress +} catch { + # Last-ditch fallback: emit a minimal valid object so the runner never gets garbage. + $errMsg = '' + try { $errMsg = $_.Exception.Message } catch { } + $fallback = [ordered]@{ + host = $HostName + collected_at_utc = $CollectedAtUtc + os = $OsInfo + facts = @{} + findings = @( + [ordered]@{ id='probe.serialize_error'; category='probe'; severity='unknown'; + title='JSON serialization failed'; + detail='The probe ran but the result could not be serialized to JSON. Re-run.'; + evidence=$errMsg } + ) + } + $json = $fallback | ConvertTo-Json -Depth 6 -Compress +} + +# Strip any non-ASCII that may have crept in (defensive; replace with '?'). +$sb = New-Object System.Text.StringBuilder +foreach ($ch in $json.ToCharArray()) { + if ([int]$ch -ge 32 -and [int]$ch -le 126) { [void]$sb.Append($ch) } + elseif ([int]$ch -eq 9 -or [int]$ch -eq 10 -or [int]$ch -eq 13) { [void]$sb.Append($ch) } + else { [void]$sb.Append('?') } +} +$json = $sb.ToString() + +Write-Output '===DIAG-JSON-START===' +Write-Output $json +Write-Output '===DIAG-JSON-END===' diff --git a/.claude/scripts/run-onboarding-diagnostic.sh b/.claude/scripts/run-onboarding-diagnostic.sh new file mode 100644 index 0000000..13c60ea --- /dev/null +++ b/.claude/scripts/run-onboarding-diagnostic.sh @@ -0,0 +1,574 @@ +#!/usr/bin/env bash +# run-onboarding-diagnostic.sh - GuruRMM onboarding diagnostic runner (Phase 1). +# +# Dispatches .claude/scripts/onboarding-diagnostic.ps1 to a Windows agent via the +# GuruRMM RMM API, extracts the fenced JSON result, grades it RED/AMBER/GREEN, +# writes an immutable baseline (JSON + Markdown report) under +# clients//onboarding-baselines/, diffs against any prior baseline, and +# alerts #dev-alerts on RED / critical findings. +# +# Usage: +# bash run-onboarding-diagnostic.sh [client-slug] +# +# Mirrors the plumbing in .claude/commands/rmm.md (vault auth -> JWT -> dispatch +# -> poll -> command_text/stdout). Read-only against the endpoint; the probe only +# collects, it changes nothing. + +set -u + +# --------------------------------------------------------------------------- +# Args +# --------------------------------------------------------------------------- +TARGET="${1:-}" +CLIENT_SLUG="${2:-}" + +if [ -z "$TARGET" ]; then + echo "[ERROR] Usage: bash run-onboarding-diagnostic.sh [client-slug]" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Bootstrap (resolve repo root, vault, RMM base) +# --------------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +VAULT="$REPO_ROOT/.claude/scripts/vault.sh" +PROBE="$SCRIPT_DIR/onboarding-diagnostic.ps1" +ALERT="$REPO_ROOT/.claude/scripts/post-bot-alert.sh" +RMM="http://172.16.3.30:3001" + +if [ ! -f "$PROBE" ]; then + echo "[ERROR] Probe script not found: $PROBE" >&2 + exit 1 +fi + +for tool in jq curl; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "[ERROR] Required tool not found: $tool" >&2 + exit 1 + fi +done + +# Soft-fail wrapper for the bot alert so an alerting failure never aborts the run. +post_alert() { + local msg="$1" + if [ -f "$ALERT" ]; then + bash "$ALERT" "$msg" >/dev/null 2>&1 || true + fi +} + +# --------------------------------------------------------------------------- +# Authenticate +# --------------------------------------------------------------------------- +RMM_EMAIL="$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-email 2>/dev/null)" +RMM_PASS="$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-password 2>/dev/null)" + +if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ] || [ "$RMM_EMAIL" = "null" ]; then + echo "[ERROR] Could not read GuruRMM credentials from vault (infrastructure/gururmm-server.sops.yaml)" >&2 + exit 1 +fi + +LOGIN_PAYLOAD="$(jq -nc --arg e "$RMM_EMAIL" --arg p "$RMM_PASS" '{email:$e, password:$p}')" +TOKEN="$(curl -s -m 30 -X POST "$RMM/api/auth/login" \ + -H "Content-Type: application/json" \ + --data-binary "$LOGIN_PAYLOAD" | jq -r '.token // empty')" + +if [ -z "$TOKEN" ]; then + echo "[ERROR] RMM login failed (no token returned)" >&2 + exit 1 +fi +echo "[OK] Authenticated to GuruRMM" + +# --------------------------------------------------------------------------- +# Resolve agent (by exact UUID, exact hostname, then partial hostname) +# --------------------------------------------------------------------------- +AGENTS="$(curl -s -m 30 "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")" +if [ -z "$AGENTS" ] || ! echo "$AGENTS" | jq -e 'type=="array"' >/dev/null 2>&1; then + echo "[ERROR] Could not retrieve agent list" >&2 + exit 1 +fi + +# UUID-shaped target -> match by id; otherwise match by hostname. +AGENT="" +if echo "$TARGET" | grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then + AGENT="$(echo "$AGENTS" | jq --arg id "$TARGET" '[.[] | select(.id==$id)] | .[0] // empty')" +else + # exact hostname (case-insensitive) first + AGENT="$(echo "$AGENTS" | jq --arg h "$TARGET" '[.[] | select((.hostname|ascii_downcase)==($h|ascii_downcase))] | .[0] // empty')" + if [ -z "$AGENT" ] || [ "$AGENT" = "null" ]; then + # partial match + MATCHES="$(echo "$AGENTS" | jq --arg h "$TARGET" '[.[] | select(.hostname|ascii_downcase|contains($h|ascii_downcase))]')" + COUNT="$(echo "$MATCHES" | jq 'length')" + if [ "$COUNT" = "0" ]; then + AGENT="" + elif [ "$COUNT" = "1" ]; then + AGENT="$(echo "$MATCHES" | jq '.[0]')" + else + echo "[ERROR] Multiple agents match '$TARGET' - be more specific:" >&2 + echo "$MATCHES" | jq -r '.[] | " \(.hostname) (\(.os_type)) id=\(.id) client=\(.client_name)"' >&2 + exit 1 + fi + fi +fi + +if [ -z "$AGENT" ] || [ "$AGENT" = "null" ]; then + echo "[ERROR] No agent found matching '$TARGET'. Run /rmm agents to list enrolled agents." >&2 + exit 1 +fi + +AGENT_ID="$(echo "$AGENT" | jq -r '.id // empty')" +AGENT_HOST="$(echo "$AGENT" | jq -r '.hostname // empty')" +AGENT_OS="$(echo "$AGENT" | jq -r '.os_type // empty')" +AGENT_STATUS="$(echo "$AGENT" | jq -r '.status // "unknown"')" +AGENT_CONNECTED="$(echo "$AGENT" | jq -r '.is_connected // "null"')" +AGENT_CLIENT="$(echo "$AGENT" | jq -r '.client_name // empty')" +AGENT_LAST="$(echo "$AGENT" | jq -r '.last_seen // "never"')" + +echo "[OK] Agent: $AGENT_HOST ($AGENT_OS) status=$AGENT_STATUS connected=$AGENT_CONNECTED client=$AGENT_CLIENT last_seen=$AGENT_LAST id=$AGENT_ID" + +if [ "$AGENT_OS" != "windows" ]; then + echo "[ERROR] This diagnostic is Windows-only. Agent os_type='$AGENT_OS'." >&2 + exit 1 +fi + +# Treat online if status==online OR is_connected==true (is_connected can be null even when online). +if [ "$AGENT_STATUS" != "online" ] && [ "$AGENT_CONNECTED" != "true" ]; then + echo "[WARNING] Agent appears offline (status=$AGENT_STATUS). The command will queue and run when it reconnects." +fi + +# Derive client slug if not supplied: prefer explicit arg; else slugify client_name. +if [ -z "$CLIENT_SLUG" ]; then + if [ -n "$AGENT_CLIENT" ]; then + CLIENT_SLUG="$(echo "$AGENT_CLIENT" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" + echo "[INFO] No client slug supplied; derived '$CLIENT_SLUG' from client name '$AGENT_CLIENT'." + else + CLIENT_SLUG="_unsorted" + echo "[WARNING] No client slug and no client name; using '_unsorted'." + fi +fi + +# --------------------------------------------------------------------------- +# Command dispatch helper +# --------------------------------------------------------------------------- +# The agent caps the inline command body at roughly 32-40 KB (above that it +# returns "Failed to execute command" before PowerShell ever runs). The probe is +# ~60 KB, so we cannot send it inline. Instead we: +# 1. base64-encode the probe locally, +# 2. upload it to a temp file on the endpoint in <24 KB chunks (one command +# each: first writes, the rest append), +# 3. send a final small command that decodes the file to a .ps1, runs it, +# prints the fenced JSON, and deletes both temp files. +# Each dispatched command stays well under the agent limit, so this scales no +# matter how large the probe grows in later phases. + +WORK_DIR="$(mktemp -d 2>/dev/null || echo "${TMPDIR:-/tmp}/onboard-diag-$$")" +mkdir -p "$WORK_DIR" 2>/dev/null || true +cleanup() { rm -rf "$WORK_DIR" 2>/dev/null || true; } +trap cleanup EXIT + +# dispatch_one -> echoes result JSON, returns 0/1 +dispatch_one() { + local script_file="$1" + local to="$2" + local payload_file resp cmd_id status result count + + payload_file="$WORK_DIR/payload.json" + jq -nc --rawfile cmd "$script_file" --argjson to "$to" \ + '{command_type:"powershell", command:$cmd, timeout_seconds:$to}' > "$payload_file" + + resp="$(curl -s -m 30 -X POST "$RMM/api/agents/$AGENT_ID/command" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + --data-binary "@$payload_file")" + cmd_id="$(echo "$resp" | jq -r '.command_id // empty')" + if [ -z "$cmd_id" ]; then + echo "[ERROR] Dispatch failed: $resp" >&2 + return 1 + fi + + count=0 + while [ $count -lt 72 ]; do + result="$(curl -s -m 30 "$RMM/api/commands/$cmd_id" -H "Authorization: Bearer $TOKEN")" + status="$(echo "$result" | jq -r '.status // empty')" + case "$status" in + completed|failed|cancelled|interrupted) + # Persist the command id to a file: this function runs in a $( ) + # subshell, so a plain variable assignment would not survive. + printf '%s' "$cmd_id" > "$WORK_DIR/last_cmd_id" 2>/dev/null || true + echo "$result" + return 0 + ;; + running|pending|"") count=$((count + 1)); sleep 5 ;; + *) count=$((count + 1)); sleep 5 ;; + esac + done + echo "[ERROR] Command $cmd_id did not finish (last status=$status)" >&2 + return 1 +} + +# --------------------------------------------------------------------------- +# Upload probe (base64, chunked) then execute +# --------------------------------------------------------------------------- +echo "[INFO] Uploading probe to endpoint (chunked base64)..." + +# Stable-ish remote temp names; unique per run via timestamp+pid. +REMOTE_TAG="grmm_onboard_$(date -u +%Y%m%d%H%M%S)_$$" +REMOTE_B64="\$env:TEMP\\${REMOTE_TAG}.b64" +REMOTE_PS1="\$env:TEMP\\${REMOTE_TAG}.ps1" + +# Produce base64 (single line) and split into chunks. +B64_FILE="$WORK_DIR/probe.b64" +base64 -w0 "$PROBE" > "$B64_FILE" 2>/dev/null || base64 "$PROBE" | tr -d '\n' > "$B64_FILE" +CHUNK_DIR="$WORK_DIR/chunks" +mkdir -p "$CHUNK_DIR" +split -b 24000 "$B64_FILE" "$CHUNK_DIR/chunk_" +CHUNKS=$(ls -1 "$CHUNK_DIR"/chunk_* | sort) +N_CHUNKS=$(echo "$CHUNKS" | wc -l | tr -d ' ') +echo "[INFO] Probe is $(wc -c < "$PROBE") bytes -> $N_CHUNKS chunk(s)" + +IDX=0 +for ch in $CHUNKS; do + IDX=$((IDX + 1)) + # INVARIANT: DATA is RFC4648 standard base64 (alphabet A-Za-z0-9+/ with '=' + # padding). None of those characters are PowerShell metacharacters, so DATA + # is safe to interpolate raw into the here-doc below. If this is ever changed + # to base64url (alphabet adds '-' and '_'), it stays safe too - but revisit + # this assertion before swapping the encoder, do not assume silently. + DATA="$(cat "$ch")" + SCRIPT_FILE="$WORK_DIR/chunkcmd.ps1" + if [ "$IDX" -eq 1 ]; then + # First chunk: create/overwrite the file (no newline appended). + cat > "$SCRIPT_FILE" < "$SCRIPT_FILE" <&2; exit 1; } + CH_STATUS="$(echo "$CH_RESULT" | jq -r '.status')" + if [ "$CH_STATUS" != "completed" ]; then + echo "[ERROR] Chunk $IDX upload failed: status=$CH_STATUS stderr=$(echo "$CH_RESULT" | jq -r '.stderr' | head -c 200)" >&2 + exit 1 + fi + echo "[OK] Uploaded chunk $IDX/$N_CHUNKS" +done + +echo "[INFO] Decoding and executing probe on endpoint (timeout 240s)..." + +# Final command: decode base64 file -> .ps1, run it, then clean up both temp files. +RUN_SCRIPT="$WORK_DIR/runcmd.ps1" +cat > "$RUN_SCRIPT" <&2; exit 1; } +CMD_ID="$(cat "$WORK_DIR/last_cmd_id" 2>/dev/null || echo unknown)" + +FINAL_STATUS="$(echo "$RESULT" | jq -r '.status // empty')" +EXIT_CODE="$(echo "$RESULT" | jq -r '.exit_code // "null"')" +STDOUT="$(echo "$RESULT" | jq -r '.stdout // ""')" +STDERR="$(echo "$RESULT" | jq -r '.stderr // ""')" + +echo "[INFO] Probe finished: status=$FINAL_STATUS exit_code=$EXIT_CODE stdout_len=${#STDOUT} stderr_len=${#STDERR} cmd=$CMD_ID" + +# --------------------------------------------------------------------------- +# Extract fenced JSON from stdout +# --------------------------------------------------------------------------- +# Pull text strictly between the markers. awk handles arbitrary surrounding noise. +DIAG_JSON="$(printf '%s' "$STDOUT" | awk ' + /===DIAG-JSON-START===/ { capture=1; next } + /===DIAG-JSON-END===/ { capture=0 } + capture { print } +')" + +if [ -z "$DIAG_JSON" ] || ! echo "$DIAG_JSON" | jq -e '.host' >/dev/null 2>&1; then + echo "[ERROR] Could not extract valid diagnostic JSON from probe output." >&2 + echo "[ERROR] status=$FINAL_STATUS exit_code=$EXIT_CODE" >&2 + if [ -n "$STDERR" ]; then + echo "--- stderr ---" >&2 + printf '%s\n' "$STDERR" | head -40 >&2 + fi + echo "--- stdout (first 60 lines) ---" >&2 + printf '%s\n' "$STDOUT" | head -60 >&2 + exit 1 +fi + +echo "[OK] Extracted diagnostic JSON ($(echo "$DIAG_JSON" | wc -c | tr -d ' ') bytes)" + +# --------------------------------------------------------------------------- +# Grade: RED (any critical) / AMBER (any warning, no critical) / GREEN (none) +# --------------------------------------------------------------------------- +N_CRIT="$(echo "$DIAG_JSON" | jq '[.findings[] | select(.severity=="critical")] | length')" +N_WARN="$(echo "$DIAG_JSON" | jq '[.findings[] | select(.severity=="warning")] | length')" +N_UNK="$(echo "$DIAG_JSON" | jq '[.findings[] | select(.severity=="unknown")] | length')" +N_INFO="$(echo "$DIAG_JSON" | jq '[.findings[] | select(.severity=="info")] | length')" + +if [ "$N_CRIT" -gt 0 ]; then + GRADE="RED" +elif [ "$N_WARN" -gt 0 ]; then + GRADE="AMBER" +else + GRADE="GREEN" +fi + +PROBE_HOST="$(echo "$DIAG_JSON" | jq -r '.host // empty')" +[ -z "$PROBE_HOST" ] && PROBE_HOST="$AGENT_HOST" +COLLECTED="$(echo "$DIAG_JSON" | jq -r '.collected_at_utc // empty')" + +echo "[INFO] Grade=$GRADE critical=$N_CRIT warning=$N_WARN unknown=$N_UNK info=$N_INFO" + +# --------------------------------------------------------------------------- +# Output paths +# --------------------------------------------------------------------------- +BASE_DIR="$REPO_ROOT/clients/$CLIENT_SLUG/onboarding-baselines" +mkdir -p "$BASE_DIR" + +UTC_STAMP="$(date -u +%Y%m%dT%H%M%S)" +SAFE_HOST="$(echo "$PROBE_HOST" | sed -E 's/[^A-Za-z0-9._-]+/_/g')" +JSON_PATH="$BASE_DIR/${SAFE_HOST}-${UTC_STAMP}.json" +MD_PATH="$BASE_DIR/${SAFE_HOST}-${UTC_STAMP}.md" + +# Immutability guard: the per-second UTC_STAMP can collide if two runs land in +# the same second (or a re-run of the same dispatch). A baseline is immutable +# once written, so never truncate an existing one - append a PID uniquifier +# instead so the prior baseline survives intact. +if [ -e "$JSON_PATH" ]; then JSON_PATH="${JSON_PATH%.json}-$$.json"; MD_PATH="${MD_PATH%.md}-$$.md"; fi + +# Find the most recent PRIOR baseline json for this host (before we write the new one). +PRIOR_JSON="" +PRIOR_JSON="$(ls -1 "$BASE_DIR/${SAFE_HOST}-"*.json 2>/dev/null | sort | tail -n 1)" + +# Write the immutable raw snapshot (pretty-printed for readability/diffing). +echo "$DIAG_JSON" | jq '.' > "$JSON_PATH" + +# --------------------------------------------------------------------------- +# Build the Markdown report +# --------------------------------------------------------------------------- +{ + echo "# Onboarding Diagnostic Baseline - $PROBE_HOST" + echo "" + echo "- **Grade:** $GRADE" + echo "- **Host:** $PROBE_HOST" + echo "- **Client:** ${AGENT_CLIENT:-$CLIENT_SLUG} (\`$CLIENT_SLUG\`)" + echo "- **Collected (UTC):** $COLLECTED" + echo "- **Agent ID:** $AGENT_ID" + echo "- **Command ID:** $CMD_ID" + echo "- **Findings:** $N_CRIT critical / $N_WARN warning / $N_INFO info / $N_UNK unknown" + echo "" + OS_CAPTION="$(echo "$DIAG_JSON" | jq -r '.os.caption // "?"')" + OS_BUILD="$(echo "$DIAG_JSON" | jq -r '.os.build // "?"')" + echo "- **OS:** $OS_CAPTION (build $OS_BUILD)" + echo "" + echo "---" + echo "" + + for sev in critical warning info unknown; do + SEV_COUNT="$(echo "$DIAG_JSON" | jq --arg s "$sev" '[.findings[] | select(.severity==$s)] | length')" + [ "$SEV_COUNT" = "0" ] && continue + SEV_LABEL="$(echo "$sev" | tr '[:lower:]' '[:upper:]')" + echo "## $SEV_LABEL ($SEV_COUNT)" + echo "" + echo "$DIAG_JSON" | jq -r --arg s "$sev" ' + .findings[] | select(.severity==$s) | + "### " + .title + "\n" + + "- **Category:** " + (.category // "?") + "\n" + + "- **ID:** `" + (.id // "?") + "`\n" + + "- " + (.detail // "") + "\n" + + (if (.evidence // "") != "" then "\n```\n" + .evidence + "\n```\n" else "" end) + ' + echo "" + done + + echo "---" + echo "" + echo "## Inventory Baseline Summary" + echo "" + echo "$DIAG_JSON" | jq -r ' + .facts as $f | + "- **Manufacturer / Model:** " + (($f.hardware.manufacturer // "?") + " / " + ($f.hardware.model // "?")) + "\n" + + "- **Serial:** " + ($f.hardware.serial // "?") + "\n" + + "- **CPU:** " + ($f.hardware.cpu // "?") + " (" + (($f.hardware.cpu_cores // 0)|tostring) + " cores / " + (($f.hardware.cpu_logical // 0)|tostring) + " logical)\n" + + "- **RAM (GB):** " + (($f.hardware.ram_gb // 0)|tostring) + "\n" + + "- **BIOS:** " + ($f.hardware.bios_version // "?") + " (" + ($f.hardware.bios_date // "?") + ")\n" + + "- **Chassis is laptop:** " + (($f.is_laptop // false)|tostring) + "\n" + + "- **TPM present / Secure Boot:** " + (($f.tpm.present // "?")|tostring) + " / " + (($f.secure_boot // "?")|tostring) + "\n" + + "- **Domain joined:** " + (($f.domain_joined // false)|tostring) + " (" + ($f.domain // "?") + ")\n" + + "- **OS activation licensed:** " + (($f.activation.licensed // "?")|tostring) + "\n" + + "- **Uptime (days):** " + (($f.uptime_days // "?")|tostring) + "\n" + + "- **Pending reboot:** " + (($f.pending_reboot // false)|tostring) + "\n" + + "- **Installed software count:** " + (($f.installed_software_count // 0)|tostring) + "\n" + + "- **Scheduled tasks (non-MS, enabled):** " + (($f.scheduled_tasks_count // 0)|tostring) + "\n" + + "- **Local administrators:** " + (($f.local_administrators // []) | join(", ")) + ' + echo "" + echo "### Fixed volumes" + echo "" + echo "$DIAG_JSON" | jq -r ' + (.facts.volumes // []) | .[] | + "- " + (.drive // "?") + " - " + ((.free_gb // 0)|tostring) + " GB free of " + ((.size_gb // 0)|tostring) + " GB (" + ((.free_pct // 0)|tostring) + "%)" + ' + echo "" + echo "### Network adapters" + echo "" + echo "$DIAG_JSON" | jq -r ' + (.facts.network_adapters // []) | .[] | + "- " + (.description // "?") + " - IP: " + ((.ip // []) | join(", ")) + " - DNS: " + ((.dns // []) | join(", ")) + " - DHCP: " + ((.dhcp // false)|tostring) + ' + echo "" + + # ----------------------------------------------------------------------- + # DIFF section vs prior baseline + # ----------------------------------------------------------------------- + if [ -n "$PRIOR_JSON" ] && [ -f "$PRIOR_JSON" ]; then + PRIOR_STAMP="$(basename "$PRIOR_JSON")" + echo "---" + echo "" + echo "## Diff vs Prior Baseline" + echo "" + echo "- **Compared against:** \`$PRIOR_STAMP\`" + echo "" + + # New findings: ids present now but not before. + NEW_FINDINGS="$(jq -n \ + --slurpfile cur "$JSON_PATH" \ + --slurpfile old "$PRIOR_JSON" ' + ($old[0].findings // []) as $o | + ($cur[0].findings // []) as $c | + ($o | map(.id)) as $oids | + [ $c[] | select(.severity!="info") | select(.id as $id | ($oids | index($id)) | not) ] + ')" + # Resolved findings: ids present before but not now. + RESOLVED_FINDINGS="$(jq -n \ + --slurpfile cur "$JSON_PATH" \ + --slurpfile old "$PRIOR_JSON" ' + ($old[0].findings // []) as $o | + ($cur[0].findings // []) as $c | + ($c | map(.id)) as $cids | + [ $o[] | select(.severity!="info") | select(.id as $id | ($cids | index($id)) | not) ] + ')" + # Regressed: same id, severity got worse (info rank($om[.id])) | + {id, title, was: $om[.id], now: .severity} ] + ')" + + echo "**New findings:**" + echo "" + if [ "$(echo "$NEW_FINDINGS" | jq 'length')" = "0" ]; then + echo "- (none)" + else + echo "$NEW_FINDINGS" | jq -r '.[] | "- [" + (.severity|ascii_upcase) + "] " + .title' + fi + echo "" + echo "**Resolved findings:**" + echo "" + if [ "$(echo "$RESOLVED_FINDINGS" | jq 'length')" = "0" ]; then + echo "- (none)" + else + echo "$RESOLVED_FINDINGS" | jq -r '.[] | "- [" + (.severity|ascii_upcase) + "] " + .title' + fi + echo "" + echo "**Regressed findings:**" + echo "" + if [ "$(echo "$REGRESSED" | jq 'length')" = "0" ]; then + echo "- (none)" + else + echo "$REGRESSED" | jq -r '.[] | "- " + .title + " (" + .was + " -> " + .now + ")"' + fi + echo "" + + # Installed-software deltas + SW_ADDED="$(jq -n \ + --slurpfile cur "$JSON_PATH" \ + --slurpfile old "$PRIOR_JSON" ' + ((($old[0].facts.installed_software // []) | map(.name)) | unique) as $o | + ((($cur[0].facts.installed_software // []) | map(.name)) | unique) as $c | + [ $c[] | select(. as $n | ($o | index($n)) | not) ] + ')" + SW_REMOVED="$(jq -n \ + --slurpfile cur "$JSON_PATH" \ + --slurpfile old "$PRIOR_JSON" ' + ((($old[0].facts.installed_software // []) | map(.name)) | unique) as $o | + ((($cur[0].facts.installed_software // []) | map(.name)) | unique) as $c | + [ $o[] | select(. as $n | ($c | index($n)) | not) ] + ')" + + echo "**Software added:**" + echo "" + if [ "$(echo "$SW_ADDED" | jq 'length')" = "0" ]; then + echo "- (none)" + else + echo "$SW_ADDED" | jq -r '.[] | "- " + .' + fi + echo "" + echo "**Software removed:**" + echo "" + if [ "$(echo "$SW_REMOVED" | jq 'length')" = "0" ]; then + echo "- (none)" + else + echo "$SW_REMOVED" | jq -r '.[] | "- " + .' + fi + echo "" + else + echo "---" + echo "" + echo "## Diff vs Prior Baseline" + echo "" + echo "- No prior baseline found for this host. This is the first baseline." + echo "" + fi + + echo "---" + echo "" + echo "_Generated by run-onboarding-diagnostic.sh (GuruRMM onboarding diagnostic, Phase 1). Raw snapshot: \`$(basename "$JSON_PATH")\` (immutable)._" +} > "$MD_PATH" + +# --------------------------------------------------------------------------- +# Alerts (soft-fail): one line for RED overall, one per critical finding (capped) +# --------------------------------------------------------------------------- +if [ "$GRADE" = "RED" ]; then + CRIT_TITLES="$(echo "$DIAG_JSON" | jq -r '[.findings[] | select(.severity=="critical") | .title] | .[0:3] | join("; ")')" + MORE="" + if [ "$N_CRIT" -gt 3 ]; then MORE=" (+$((N_CRIT - 3)) more)"; fi + post_alert "[RMM] Onboarding diag $PROBE_HOST ($CLIENT_SLUG) = RED: $N_CRIT critical - ${CRIT_TITLES}${MORE}" +elif [ "$GRADE" = "AMBER" ]; then + post_alert "[RMM] Onboarding diag $PROBE_HOST ($CLIENT_SLUG) = AMBER: $N_WARN warning, 0 critical" +fi + +# --------------------------------------------------------------------------- +# Final console summary +# --------------------------------------------------------------------------- +echo "" +echo "==========================================================" +echo " Onboarding diagnostic complete" +echo " Host: $PROBE_HOST" +echo " Client: ${AGENT_CLIENT:-$CLIENT_SLUG} ($CLIENT_SLUG)" +echo " Grade: $GRADE ($N_CRIT critical / $N_WARN warning / $N_INFO info / $N_UNK unknown)" +echo " JSON: $JSON_PATH" +echo " Report: $MD_PATH" +echo "==========================================================" +echo "" +echo "Report path: $MD_PATH" diff --git a/clients/rednour/onboarding-baselines/FRONTDESKRECEPT-20260529T195614.json b/clients/rednour/onboarding-baselines/FRONTDESKRECEPT-20260529T195614.json new file mode 100644 index 0000000..ae2bc79 --- /dev/null +++ b/clients/rednour/onboarding-baselines/FRONTDESKRECEPT-20260529T195614.json @@ -0,0 +1,774 @@ +{ + "host": "FRONTDESKRECEPT", + "collected_at_utc": "2026-05-29T19:55:43Z", + "os": { + "caption": "Microsoft Windows 11 Pro", + "version": "10.0.26200", + "build": "26200", + "install_date": "2025-08-07T02:15:17Z", + "last_boot_utc": "2026-05-13T00:39:47Z", + "architecture": "64-bit" + }, + "facts": { + "builtin_admin_enabled": false, + "os_eol": { + "eol_date": "2027-10-12", + "release": "Win11 25H2" + }, + "pending_updates": 2, + "pending_reboot": true, + "uptime_days": 16.8, + "scheduled_tasks": [ + { + "path": "\\", + "name": "CorelUpdateHelperTask-34FAD43C54B1AA7B7D45D9CFCA371A7C", + "state": "Ready" + }, + { + "path": "\\", + "name": "CorelUpdateHelperTaskCore", + "state": "Ready" + }, + { + "path": "\\", + "name": "MicrosoftEdgeUpdateTaskMachineCore1d8299e406acf5f", + "state": "Ready" + }, + { + "path": "\\", + "name": "MicrosoftEdgeUpdateTaskMachineUA", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Per-Machine Standalone Update Task", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Reporting Task-S-1-5-21-1826020299-2037390372-3224229966-1001", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Startup Task-S-1-5-21-1826020299-2037390372-3224229966-1001", + "state": "Ready" + }, + { + "path": "\\GoogleSystem\\GoogleUpdater\\", + "name": "GoogleUpdaterTaskSystem149.0.7814.0{A725F2F7-591B-48E3-A0A9-9AAD55F1DAF1}", + "state": "Ready" + }, + { + "path": "\\SoftLanding\\S-1-5-21-1826020299-2037390372-3224229966-1001\\", + "name": "SoftLandingCreativeManagementTask", + "state": "Ready" + }, + { + "path": "\\SoftLanding\\S-1-5-21-1826020299-2037390372-3224229966-1001\\", + "name": "SoftLandingDeferralTask-{0c3db634-1f6a-464d-bbe9-5526a48a6e15}", + "state": "Ready" + } + ], + "hardware": { + "model": "OptiPlex 3080", + "manufacturer": "Dell Inc.", + "bios_date": "2025-12-01", + "cpu_logical": 12, + "bios_version": "2.34.0", + "cpu_cores": 6, + "ram_gb": 15.8, + "serial": "DPZK1G3", + "cpu": "Intel(R) Core(TM) i5-10505 CPU @ 3.20GHz" + }, + "os_build": "26200", + "secure_boot": false, + "backup_agents": null, + "autoruns_run_keys": [ + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "SecurityHealth", + "value": "C:\\WINDOWS\\system32\\SecurityHealthSystray.exe" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "RtkAudUService", + "value": "\"C:\\WINDOWS\\System32\\DriverStore\\FileRepository\\realtekservice.inf_amd64_a4555e9b35287491\\RtkAudUService64.exe\" -background" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "WavesSvc", + "value": "\"C:\\WINDOWS\\System32\\DriverStore\\FileRepository\\wavesapo9de.inf_amd64_c6bfc5767fc0181c\\WavesSvc64.exe\" -Jack" + }, + { + "key": "HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "QuickFinder Scheduler", + "value": "\"c:\\Program Files (x86)\\Corel\\WordPerfect Office 2021\\Programs\\QFSCHD210.EXE\"" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", + "name": "Delete Cached Update Binary", + "value": "C:\\WINDOWS\\system32\\cmd.exe /q /c del /q \"C:\\Program Files\\Microsoft OneDrive\\Update\\OneDriveSetup.exe\"" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", + "name": "Delete Cached Standalone Update Binary", + "value": "C:\\WINDOWS\\system32\\cmd.exe /q /c del /q \"C:\\Program Files\\Microsoft OneDrive\\StandaloneUpdater\\OneDriveSetup.exe\"" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", + "name": "msedge_cleanup_{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "value": "\"C:\\Program Files (x86)\\Microsoft\\EdgeWebView\\Application\\148.0.3967.83\\Installer\\setup.exe\" --msedgewebview --delete-old-versions --system-level --verbose-logging --on-logon" + } + ], + "physical_disks": [ + { + "health": "Healthy", + "model": "KingFast", + "media_type": "SSD" + } + ], + "local_users": [ + { + "last_logon": "", + "name": "Administrator", + "password_never_expires": false, + "enabled": false + }, + { + "last_logon": "", + "name": "DefaultAccount", + "password_never_expires": false, + "enabled": false + }, + { + "last_logon": "", + "name": "Guest", + "password_never_expires": false, + "enabled": false + }, + { + "last_logon": "2026-05-29", + "name": "guru", + "password_never_expires": false, + "enabled": true + }, + { + "last_logon": "", + "name": "localadmin", + "password_never_expires": false, + "enabled": true + }, + { + "last_logon": "", + "name": "WDAGUtilityAccount", + "password_never_expires": false, + "enabled": false + } + ], + "scheduled_tasks_count": 10, + "volumes": [ + { + "drive": "[unlabeled]", + "size_gb": 0.1, + "free_pct": 35.9, + "free_gb": 0 + }, + { + "drive": "[unlabeled]", + "size_gb": 0.8, + "free_pct": 14.4, + "free_gb": 0.1 + }, + { + "drive": "[Recovery]", + "size_gb": 0.5, + "free_pct": 97.4, + "free_gb": 0.5 + }, + { + "drive": "C:", + "size_gb": 475.5, + "free_pct": 82.8, + "free_gb": 394 + } + ], + "network_adapters": [ + { + "dhcp": true, + "description": "Realtek PCIe GbE Family Controller", + "gateway": [ + "192.168.10.1" + ], + "mac": "70:B5:E8:7A:80:7B", + "ip": [ + "192.168.10.115", + "fe80::b17c:c1aa:150b:e65b" + ], + "dns": [ + "192.168.10.1" + ] + } + ], + "failed_autostart_services": [ + { + "name": "GoogleUpdaterInternalService149.0.7814.0", + "display": "Google Updater Internal Service (GoogleUpdaterInternalService149.0.7814.0)", + "state": "Stopped" + }, + { + "name": "GoogleUpdaterService149.0.7814.0", + "display": "Google Updater Service (GoogleUpdaterService149.0.7814.0)", + "state": "Stopped" + }, + { + "name": "Intel(R) TPM Provisioning Service", + "display": "Intel(R) TPM Provisioning Service", + "state": "Stopped" + } + ], + "stability_14d": { + "unexpected_shutdowns": 0, + "disk_errors": 0, + "bugchecks": 0 + }, + "exposure": { + "smb1_enabled": false, + "laps_present": true, + "rdp_enabled": false, + "uac_enabled": true, + "rdp_nla": true + }, + "accounts_password_never_expires": [], + "installed_software": [ + { + "publisher": "Microsoft Corporation", + "name": "Copilot", + "version": "148.0.3967.70" + }, + { + "publisher": "Corel corporation", + "name": "Corel Update Manager", + "version": "2.16.673" + }, + { + "publisher": "Google LLC", + "name": "Google Chrome", + "version": "148.0.7778.216" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft 365 Apps for business - en-us", + "version": "16.0.19929.20172" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Command Line Utilities 11 for SQL Server", + "version": "11.0.2270.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Edge", + "version": "148.0.3967.83" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Edge WebView2 Runtime", + "version": "148.0.3967.83" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft ODBC Driver 11 for SQL Server", + "version": "11.0.2270.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft OneDrive", + "version": "26.084.0504.0007" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Update Health Tools", + "version": "5.72.0.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual Basic for Applications 7.1 (x86)", + "version": "7.1.00.00" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual Basic for Applications 7.1 (x86) English", + "version": "7.1.0.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2005 Redistributable", + "version": "8.0.56336" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2005 Redistributable (x64)", + "version": "8.0.56336" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 Redistributable (x64) - 11.0.50727", + "version": "11.0.50727.1" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 Redistributable (x86) - 11.0.61030", + "version": "11.0.61030.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x64 Additional Runtime - 11.0.50727", + "version": "11.0.50727" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x64 Minimum Runtime - 11.0.50727", + "version": "11.0.50727" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x86 Additional Runtime - 11.0.61030", + "version": "11.0.61030" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x86 Minimum Runtime - 11.0.61030", + "version": "11.0.61030" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.44.35211", + "version": "14.44.35211.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2015-2022 Redistributable (x86) - 14.44.35211", + "version": "14.44.35211.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X64 Additional Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X64 Minimum Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X86 Additional Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X86 Minimum Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual Studio 2010 Tools for Office Runtime (x64)", + "version": "10.0.31119" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual Studio 2010 Tools for Office Runtime (x64)", + "version": "10.0.31124" + }, + { + "publisher": "Microsoft Corporation", + "name": "Office 16 Click-to-Run Extensibility Component", + "version": "16.0.19929.20172" + }, + { + "publisher": "ScreenConnect Software", + "name": "ScreenConnect Client (1912bf3444b41a08)", + "version": "26.1.24.9579" + }, + { + "publisher": "Splashtop Inc.", + "name": "Splashtop Streamer", + "version": "3.8.2.0" + }, + { + "publisher": "Servably, Inc.", + "name": "Syncro", + "version": "1.0.201.18410" + }, + { + "publisher": "Microsoft Corporation", + "name": "Teams Machine-Wide Installer", + "version": "1.4.0.32771" + }, + { + "publisher": "PCLaw | Time Matters?", + "name": "Time Matters?", + "version": "21.0.0.123" + }, + { + "publisher": "Microsoft Corporation", + "name": "Update for Windows 10 for x64-based Systems (KB5001716)", + "version": "4.91.0.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Windows PC Health Check", + "version": "3.6.2204.08001" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021", + "version": "21.1.1.194" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Common Files", + "version": "21.1.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Common Files English", + "version": "21.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - IPM", + "version": "21.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - IPM Content", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - IPM Content TBYB ", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - IPM TBYB", + "version": "21.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Lightning Files", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Lightning Files English", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Presentations Files", + "version": "21.1.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Presentations Files English", + "version": "21.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Quattro Pro Files", + "version": "21.1.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Quattro Pro Files English", + "version": "21.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Redists", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - Setup Files", + "version": "21.1.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - WordPerfect Files", + "version": "21.1.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - WordPerfect Files English", + "version": "21.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office 2021 - WPD format Props x64", + "version": "21.0" + }, + { + "publisher": " Corel Corporation", + "name": "WordPerfect Office 2021 - Writing Tools", + "version": "21.0" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office IFilter 32-bit", + "version": "1.8" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office IFilter 64-bit", + "version": "1.8" + } + ], + "tpm": { + "enabled": true, + "ready": true, + "present": true + }, + "local_groups": [ + "Access Control Assistance Operators", + "Administrators", + "Backup Operators", + "Cryptographic Operators", + "Device Owners", + "Distributed COM Users", + "Event Log Readers", + "Guests", + "Hyper-V Administrators", + "IIS_IUSRS", + "Network Configuration Operators", + "OpenSSH Users", + "Performance Log Users", + "Performance Monitor Users", + "Power Users", + "Remote Desktop Users", + "Remote Management Users", + "Replicator", + "System Managed Accounts Group", + "User Mode Hardware Operators", + "Users" + ], + "battery": { + "present": false + }, + "activation": { + "edition": "Microsoft Windows 11 Pro", + "description": "Windows(R) Operating System, OEM_DM channel", + "licensed": true, + "license_status_code": 1 + }, + "time_source": "time.windows.com,0x9", + "chassis_types": [ + 3 + ], + "last_hotfix": { + "hotfix_id": "KB5089549", + "installed_on": "2026-05-13T07:00:00Z" + }, + "antivirus_products": [ + "Windows Defender" + ], + "domain_joined": false, + "defender": { + "antispyware_signature_age": 0, + "tamper_protected": true, + "real_time_protection": true, + "nis_enabled": true, + "available": true, + "antivirus_enabled": true, + "am_service_enabled": true + }, + "bitlocker": { + "os_volume": "C:", + "key_protectors": [], + "recovery_key_present": false, + "available": true, + "encryption_percent": 0, + "protection_status": "Off" + }, + "is_laptop": false, + "installed_software_count": 58, + "local_administrators": [ + "FRONTDESKRECEPT\\Administrator", + "FRONTDESKRECEPT\\guru", + "FRONTDESKRECEPT\\localadmin" + ], + "firewall_profiles": { + "Private": true, + "Domain": true, + "Public": true + }, + "domain": "WORKGROUP", + "foreign_agents": [ + "ScreenConnect / ConnectWise Control", + "Splashtop (SOS/Streamer)", + "Syncro / Kabuto" + ] + }, + "findings": [ + { + "id": "sec.defender.ok", + "category": "security", + "severity": "info", + "title": "Defender active and current", + "detail": "Real-time protection on, service running, signatures current.", + "evidence": "RealTimeProtectionEnabled=True; AMServiceEnabled=True; AntispywareSignatureAge=0 days; IsTamperProtected=True" + }, + { + "id": "sec.av_products.defender_only", + "category": "security", + "severity": "info", + "title": "Defender is the only registered AV", + "detail": "Only Microsoft/Windows Defender is registered in Security Center.", + "evidence": "Windows Defender" + }, + { + "id": "sec.foreign_agents.screenconnect_connectwise_control", + "category": "security", + "severity": "critical", + "title": "Foreign management/remote-access agent: ScreenConnect / ConnectWise Control", + "detail": "A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.", + "evidence": "program: ScreenConnect Client (1912bf3444b41a08) 26.1.24.9579\nservice: ScreenConnect Client (1912bf3444b41a08) (ScreenConnect Client (1912bf3444b41a08)) Running" + }, + { + "id": "sec.foreign_agents.splashtop_sos_streamer_", + "category": "security", + "severity": "critical", + "title": "Foreign management/remote-access agent: Splashtop (SOS/Streamer)", + "detail": "A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.", + "evidence": "program: Splashtop Streamer 3.8.2.0\nservice: SplashtopRemoteService (Splashtop? Remote Service) Running" + }, + { + "id": "sec.foreign_agents.syncro_kabuto", + "category": "security", + "severity": "critical", + "title": "Foreign management/remote-access agent: Syncro / Kabuto", + "detail": "A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.", + "evidence": "program: Syncro 1.0.201.18410\nservice: Syncro (Syncro) Running" + }, + { + "id": "sec.firewall.ok", + "category": "security", + "severity": "info", + "title": "All firewall profiles enabled", + "detail": "Domain, Private, and Public firewall profiles are all enabled.", + "evidence": "Private=True; Domain=True; Public=True" + }, + { + "id": "sec.bitlocker.unencrypted", + "category": "security", + "severity": "warning", + "title": "OS volume is NOT encrypted with BitLocker", + "detail": "The operating system volume is unencrypted. Data is exposed if the disk is removed or the device is lost. Enable BitLocker and escrow the recovery key.", + "evidence": "Volume=C:; ProtectionStatus=Off; EncryptionPercentage=0; KeyProtectors=" + }, + { + "id": "sec.local_admins.list", + "category": "security", + "severity": "info", + "title": "Local administrators (3)", + "detail": "Members of the local Administrators group. Review for unexpected or unknown accounts (especially leftover MSP/vendor accounts from a prior provider).", + "evidence": "FRONTDESKRECEPT\\Administrator\nFRONTDESKRECEPT\\guru\nFRONTDESKRECEPT\\localadmin" + }, + { + "id": "sec.patch.os_supported", + "category": "security", + "severity": "info", + "title": "OS build supported: Win11 25H2", + "detail": "Build 26200 (Win11 25H2) is in support until 2027-10-12.", + "evidence": "Microsoft Windows 11 Pro build 26200" + }, + { + "id": "sec.patch.pending", + "category": "security", + "severity": "warning", + "title": "2 pending Windows updates", + "detail": "Windows Update reports pending (not installed, not hidden) updates. Some may be security updates. Approve/install on the next maintenance window.", + "evidence": "Microsoft.Update.Session search IsInstalled=0 and IsHidden=0 -> 2" + }, + { + "id": "sec.patch.last_hotfix", + "category": "security", + "severity": "info", + "title": "Last hotfix: KB5089549", + "detail": "Most recently installed update (from Get-HotFix; reflects CBS/MSU packages, not all cumulative metadata).", + "evidence": "KB5089549 installed 2026-05-13T07:00:00Z" + }, + { + "id": "sec.exposure.smb1_off", + "category": "security", + "severity": "info", + "title": "SMBv1 disabled", + "detail": "SMBv1 server protocol is disabled.", + "evidence": "EnableSMB1Protocol=False" + }, + { + "id": "sec.exposure.laps_present", + "category": "security", + "severity": "info", + "title": "LAPS detected", + "detail": "A LAPS mechanism is present.", + "evidence": "Windows LAPS reg key" + }, + { + "id": "health.stability.clean", + "category": "health", + "severity": "info", + "title": "No stability events in the last 14 days", + "detail": "No unexpected shutdowns, BSODs, or disk errors logged.", + "evidence": "Unexpected shutdowns (id 41)=0; Bugchecks/BSOD (id 1001)=0; Disk errors (id 7/51/153)=0" + }, + { + "id": "health.reboot_uptime.pending", + "category": "health", + "severity": "warning", + "title": "Reboot pending", + "detail": "A reboot is pending. Pending reboots can block patches and leave the system in a half-updated state. Schedule a restart.", + "evidence": "PendingFileRenameOperations" + }, + { + "id": "health.failed_services.stopped", + "category": "health", + "severity": "warning", + "title": "3 auto-start service(s) not running", + "detail": "These services are set to start automatically but are not running. Some may be benign; review for security agents, backup agents, or AV that should be running.", + "evidence": "GoogleUpdaterInternalService149.0.7814.0 (Google Updater Internal Service (GoogleUpdaterInternalService149.0.7814.0)) = Stopped\nGoogleUpdaterService149.0.7814.0 (Google Updater Service (GoogleUpdaterService149.0.7814.0)) = Stopped\nIntel(R) TPM Provisioning Service (Intel(R) TPM Provisioning Service) = Stopped" + }, + { + "id": "health.domain.workgroup", + "category": "health", + "severity": "info", + "title": "Not domain-joined (workgroup)", + "detail": "This machine is in workgroup/Azure AD only mode (Domain=WORKGROUP). No on-prem AD secure channel applies.", + "evidence": "PartOfDomain=False; Domain=WORKGROUP" + }, + { + "id": "health.time.source", + "category": "health", + "severity": "info", + "title": "Time service source", + "detail": "Current Windows Time service source.", + "evidence": "Source=time.windows.com,0x9" + }, + { + "id": "health.backup.none", + "category": "health", + "severity": "info", + "title": "No backup agent detected", + "detail": "No known backup agent service found. Backup expectation varies by endpoint; confirm whether this machine is supposed to have local/cloud backup and whether server-side or M365 backup covers it.", + "evidence": "No matching backup service in Win32_Service" + } + ] +} diff --git a/clients/rednour/onboarding-baselines/FRONTDESKRECEPT-20260529T195614.md b/clients/rednour/onboarding-baselines/FRONTDESKRECEPT-20260529T195614.md new file mode 100644 index 0000000..8b1f7de --- /dev/null +++ b/clients/rednour/onboarding-baselines/FRONTDESKRECEPT-20260529T195614.md @@ -0,0 +1,240 @@ +# Onboarding Diagnostic Baseline - FRONTDESKRECEPT + +- **Grade:** RED +- **Host:** FRONTDESKRECEPT +- **Client:** Rednour Law Offices (`rednour`) +- **Collected (UTC):** 2026-05-29T19:55:43Z +- **Agent ID:** 04765560-3e8a-46e5-a507-c5f5f4ead6eb +- **Command ID:** 94966f79-74ea-4b47-98c5-6e0f33f819fa +- **Findings:** 3 critical / 4 warning / 12 info / 0 unknown + +- **OS:** Microsoft Windows 11 Pro (build 26200) + +--- + +## CRITICAL (3) + +### Foreign management/remote-access agent: ScreenConnect / ConnectWise Control +- **Category:** security +- **ID:** `sec.foreign_agents.screenconnect_connectwise_control` +- A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it. + +``` +program: ScreenConnect Client (1912bf3444b41a08) 26.1.24.9579 +service: ScreenConnect Client (1912bf3444b41a08) (ScreenConnect Client (1912bf3444b41a08)) Running +``` + +### Foreign management/remote-access agent: Splashtop (SOS/Streamer) +- **Category:** security +- **ID:** `sec.foreign_agents.splashtop_sos_streamer_` +- A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it. + +``` +program: Splashtop Streamer 3.8.2.0 +service: SplashtopRemoteService (Splashtop? Remote Service) Running +``` + +### Foreign management/remote-access agent: Syncro / Kabuto +- **Category:** security +- **ID:** `sec.foreign_agents.syncro_kabuto` +- A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it. + +``` +program: Syncro 1.0.201.18410 +service: Syncro (Syncro) Running +``` + + +## WARNING (4) + +### OS volume is NOT encrypted with BitLocker +- **Category:** security +- **ID:** `sec.bitlocker.unencrypted` +- The operating system volume is unencrypted. Data is exposed if the disk is removed or the device is lost. Enable BitLocker and escrow the recovery key. + +``` +Volume=C:; ProtectionStatus=Off; EncryptionPercentage=0; KeyProtectors= +``` + +### 2 pending Windows updates +- **Category:** security +- **ID:** `sec.patch.pending` +- Windows Update reports pending (not installed, not hidden) updates. Some may be security updates. Approve/install on the next maintenance window. + +``` +Microsoft.Update.Session search IsInstalled=0 and IsHidden=0 -> 2 +``` + +### Reboot pending +- **Category:** health +- **ID:** `health.reboot_uptime.pending` +- A reboot is pending. Pending reboots can block patches and leave the system in a half-updated state. Schedule a restart. + +``` +PendingFileRenameOperations +``` + +### 3 auto-start service(s) not running +- **Category:** health +- **ID:** `health.failed_services.stopped` +- These services are set to start automatically but are not running. Some may be benign; review for security agents, backup agents, or AV that should be running. + +``` +GoogleUpdaterInternalService149.0.7814.0 (Google Updater Internal Service (GoogleUpdaterInternalService149.0.7814.0)) = Stopped +GoogleUpdaterService149.0.7814.0 (Google Updater Service (GoogleUpdaterService149.0.7814.0)) = Stopped +Intel(R) TPM Provisioning Service (Intel(R) TPM Provisioning Service) = Stopped +``` + + +## INFO (12) + +### Defender active and current +- **Category:** security +- **ID:** `sec.defender.ok` +- Real-time protection on, service running, signatures current. + +``` +RealTimeProtectionEnabled=True; AMServiceEnabled=True; AntispywareSignatureAge=0 days; IsTamperProtected=True +``` + +### Defender is the only registered AV +- **Category:** security +- **ID:** `sec.av_products.defender_only` +- Only Microsoft/Windows Defender is registered in Security Center. + +``` +Windows Defender +``` + +### All firewall profiles enabled +- **Category:** security +- **ID:** `sec.firewall.ok` +- Domain, Private, and Public firewall profiles are all enabled. + +``` +Private=True; Domain=True; Public=True +``` + +### Local administrators (3) +- **Category:** security +- **ID:** `sec.local_admins.list` +- Members of the local Administrators group. Review for unexpected or unknown accounts (especially leftover MSP/vendor accounts from a prior provider). + +``` +FRONTDESKRECEPT\Administrator +FRONTDESKRECEPT\guru +FRONTDESKRECEPT\localadmin +``` + +### OS build supported: Win11 25H2 +- **Category:** security +- **ID:** `sec.patch.os_supported` +- Build 26200 (Win11 25H2) is in support until 2027-10-12. + +``` +Microsoft Windows 11 Pro build 26200 +``` + +### Last hotfix: KB5089549 +- **Category:** security +- **ID:** `sec.patch.last_hotfix` +- Most recently installed update (from Get-HotFix; reflects CBS/MSU packages, not all cumulative metadata). + +``` +KB5089549 installed 2026-05-13T07:00:00Z +``` + +### SMBv1 disabled +- **Category:** security +- **ID:** `sec.exposure.smb1_off` +- SMBv1 server protocol is disabled. + +``` +EnableSMB1Protocol=False +``` + +### LAPS detected +- **Category:** security +- **ID:** `sec.exposure.laps_present` +- A LAPS mechanism is present. + +``` +Windows LAPS reg key +``` + +### No stability events in the last 14 days +- **Category:** health +- **ID:** `health.stability.clean` +- No unexpected shutdowns, BSODs, or disk errors logged. + +``` +Unexpected shutdowns (id 41)=0; Bugchecks/BSOD (id 1001)=0; Disk errors (id 7/51/153)=0 +``` + +### Not domain-joined (workgroup) +- **Category:** health +- **ID:** `health.domain.workgroup` +- This machine is in workgroup/Azure AD only mode (Domain=WORKGROUP). No on-prem AD secure channel applies. + +``` +PartOfDomain=False; Domain=WORKGROUP +``` + +### Time service source +- **Category:** health +- **ID:** `health.time.source` +- Current Windows Time service source. + +``` +Source=time.windows.com,0x9 +``` + +### No backup agent detected +- **Category:** health +- **ID:** `health.backup.none` +- No known backup agent service found. Backup expectation varies by endpoint; confirm whether this machine is supposed to have local/cloud backup and whether server-side or M365 backup covers it. + +``` +No matching backup service in Win32_Service +``` + + +--- + +## Inventory Baseline Summary + +- **Manufacturer / Model:** Dell Inc. / OptiPlex 3080 +- **Serial:** DPZK1G3 +- **CPU:** Intel(R) Core(TM) i5-10505 CPU @ 3.20GHz (6 cores / 12 logical) +- **RAM (GB):** 15.8 +- **BIOS:** 2.34.0 (2025-12-01) +- **Chassis is laptop:** false +- **TPM present / Secure Boot:** true / ? +- **Domain joined:** false (WORKGROUP) +- **OS activation licensed:** true +- **Uptime (days):** 16.8 +- **Pending reboot:** true +- **Installed software count:** 58 +- **Scheduled tasks (non-MS, enabled):** 10 +- **Local administrators:** FRONTDESKRECEPT\Administrator, FRONTDESKRECEPT\guru, FRONTDESKRECEPT\localadmin + +### Fixed volumes + +- [unlabeled] - 0 GB free of 0.1 GB (35.9%) +- [unlabeled] - 0.1 GB free of 0.8 GB (14.4%) +- [Recovery] - 0.5 GB free of 0.5 GB (97.4%) +- C: - 394 GB free of 475.5 GB (82.8%) + +### Network adapters + +- Realtek PCIe GbE Family Controller - IP: 192.168.10.115, fe80::b17c:c1aa:150b:e65b - DNS: 192.168.10.1 - DHCP: true + +--- + +## Diff vs Prior Baseline + +- No prior baseline found for this host. This is the first baseline. + +--- + +_Generated by run-onboarding-diagnostic.sh (GuruRMM onboarding diagnostic, Phase 1). Raw snapshot: `FRONTDESKRECEPT-20260529T195614.json` (immutable)._ diff --git a/clients/rednour/onboarding-baselines/LEGALASST-20260529T200647.json b/clients/rednour/onboarding-baselines/LEGALASST-20260529T200647.json new file mode 100644 index 0000000..88ff507 --- /dev/null +++ b/clients/rednour/onboarding-baselines/LEGALASST-20260529T200647.json @@ -0,0 +1,833 @@ +{ + "host": "LEGALASST", + "collected_at_utc": "2026-05-29T20:05:50Z", + "os": { + "caption": "Microsoft Windows 10 Pro", + "version": "10.0.19045", + "build": "19045", + "install_date": "2020-10-14T20:37:24Z", + "last_boot_utc": "2026-04-16T17:07:07Z", + "architecture": "64-bit" + }, + "facts": { + "builtin_admin_enabled": false, + "os_eol": { + "eol_date": "2025-10-14", + "release": "Win10 22H2" + }, + "pending_updates": 1, + "pending_reboot": true, + "uptime_days": 43.1, + "scheduled_tasks": [ + { + "path": "\\", + "name": "Adobe Acrobat Update Task", + "state": "Ready" + }, + { + "path": "\\", + "name": "Launch Adobe CCXProcess", + "state": "Ready" + }, + { + "path": "\\", + "name": "MicrosoftEdgeUpdateTaskMachineCore", + "state": "Ready" + }, + { + "path": "\\", + "name": "MicrosoftEdgeUpdateTaskMachineUA", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Per-Machine Standalone Update Task", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Reporting Task-S-1-5-21-1572446079-3930123124-3343558679-1001", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Reporting Task-S-1-5-21-1572446079-3930123124-3343558679-1002", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Reporting Task-S-1-5-21-1572446079-3930123124-3343558679-1009", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Startup Task-S-1-5-21-1572446079-3930123124-3343558679-1001", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Startup Task-S-1-5-21-1572446079-3930123124-3343558679-1002", + "state": "Ready" + }, + { + "path": "\\", + "name": "OneDrive Startup Task-S-1-5-21-1572446079-3930123124-3343558679-1009", + "state": "Ready" + }, + { + "path": "\\", + "name": "TM_Scheduled_Backup_Task", + "state": "Ready" + }, + { + "path": "\\GoogleSystem\\GoogleUpdater\\", + "name": "GoogleUpdaterTaskSystem149.0.7814.0{3238CEEE-C349-45C0-BB24-92859BBE402D}", + "state": "Ready" + } + ], + "hardware": { + "model": "To Be Filled By O.E.M.", + "manufacturer": "To Be Filled By O.E.M.", + "bios_date": "2019-05-15", + "cpu_logical": 4, + "bios_version": "P3.50", + "cpu_cores": 4, + "ram_gb": 5.9, + "serial": "To Be Filled By O.E.M.", + "cpu": "AMD Ryzen 3 3200G with Radeon Vega Graphics " + }, + "os_build": "19045", + "secure_boot": false, + "backup_agents": null, + "autoruns_run_keys": [ + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "SecurityHealth", + "value": "C:\\Windows\\system32\\SecurityHealthSystray.exe" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "Logitech Download Assistant", + "value": "C:\\Windows\\system32\\rundll32.exe C:\\Windows\\System32\\LogiLDA.dll,LogiFetch" + }, + { + "key": "HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "QuickFinder Scheduler", + "value": "\"c:\\Program Files (x86)\\Corel\\WordPerfect Office X6\\Programs\\QFSCHD160.EXE\"" + }, + { + "key": "HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "SunJavaUpdateSched", + "value": "\"C:\\Program Files (x86)\\Common Files\\Java\\Java Update\\jusched.exe\"" + }, + { + "key": "HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "Adobe CCXProcess", + "value": "C:\\Program Files (x86)\\Adobe\\Adobe Creative Cloud Experience\\CCXProcess.exe" + }, + { + "key": "HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", + "name": "Adobe Creative Cloud", + "value": "\"C:\\Program Files\\Adobe\\Adobe Creative Cloud\\ACC\\Creative Cloud.exe\" --showwindow=false --onOSstartup=true" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", + "name": "Delete Cached Update Binary", + "value": "C:\\Windows\\system32\\cmd.exe /q /c del /q \"C:\\Program Files\\Microsoft OneDrive\\Update\\OneDriveSetup.exe\"" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", + "name": "Delete Cached Standalone Update Binary", + "value": "C:\\Windows\\system32\\cmd.exe /q /c del /q \"C:\\Program Files\\Microsoft OneDrive\\StandaloneUpdater\\OneDriveSetup.exe\"" + }, + { + "key": "HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce", + "name": "msedge_cleanup_{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "value": "\"C:\\Program Files (x86)\\Microsoft\\EdgeWebView\\Application\\148.0.3967.83\\Installer\\setup.exe\" --msedgewebview --delete-old-versions --system-level --verbose-logging --on-logon" + } + ], + "local_users": [ + { + "last_logon": "", + "name": "Administrator", + "password_never_expires": false, + "enabled": false + }, + { + "last_logon": "2022-05-17", + "name": "Ale", + "password_never_expires": false, + "enabled": true + }, + { + "last_logon": "", + "name": "DefaultAccount", + "password_never_expires": false, + "enabled": false + }, + { + "last_logon": "2026-05-29", + "name": "Emma", + "password_never_expires": false, + "enabled": true + }, + { + "last_logon": "", + "name": "Guest", + "password_never_expires": false, + "enabled": false + }, + { + "last_logon": "2020-10-14", + "name": "localadmin", + "password_never_expires": false, + "enabled": true + }, + { + "last_logon": "", + "name": "WDAGUtilityAccount", + "password_never_expires": false, + "enabled": false + } + ], + "scheduled_tasks_count": 13, + "volumes": { + "drive": "C:", + "size_gb": 465.1, + "free_pct": 75.2, + "free_gb": 349.6 + }, + "network_adapters": [ + { + "dhcp": true, + "description": "Realtek PCIe GbE Family Controller", + "gateway": [ + "192.168.10.1" + ], + "mac": "70:85:C2:CD:6D:65", + "ip": [ + "192.168.10.213" + ], + "dns": [ + "192.168.10.1" + ] + } + ], + "failed_autostart_services": [ + { + "name": "WMPNetworkSvc", + "display": "Windows Media Player Network Sharing Service", + "state": "Stopped" + }, + { + "name": "GoogleUpdaterInternalService149.0.7814.0", + "display": "Google Updater Internal Service (GoogleUpdaterInternalService149.0.7814.0)", + "state": "Stopped" + }, + { + "name": "GoogleUpdaterService149.0.7814.0", + "display": "Google Updater Service (GoogleUpdaterService149.0.7814.0)", + "state": "Stopped" + } + ], + "stability_14d": { + "unexpected_shutdowns": 0, + "disk_errors": 1, + "bugchecks": 0 + }, + "exposure": { + "smb1_enabled": false, + "laps_present": true, + "rdp_enabled": false, + "uac_enabled": true, + "rdp_nla": true + }, + "accounts_password_never_expires": [], + "installed_software": [ + { + "publisher": "Adobe", + "name": "Adobe Acrobat (64-bit)", + "version": "26.001.21483" + }, + { + "publisher": "Adobe Inc.", + "name": "Adobe Creative Cloud", + "version": "6.9.1.1" + }, + { + "publisher": "Adobe Systems Incorporated", + "name": "Adobe Refresh Manager", + "version": "1.8.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Copilot", + "version": "148.0.3967.96" + }, + { + "publisher": "Corel Corporation", + "name": "Corel Compatibility Pack", + "version": "12.4518.1018" + }, + { + "publisher": "Eclipse Adoptium", + "name": "Eclipse Temurin JDK with Hotspot 17.0.2+8 (x64)", + "version": "17.0.2.8" + }, + { + "publisher": "Google LLC", + "name": "Google Chrome", + "version": "148.0.7778.179" + }, + { + "publisher": "Oracle Corporation", + "name": "Java 8 Update 341 (64-bit)", + "version": "8.0.3410.10" + }, + { + "publisher": "Oracle Corporation", + "name": "Java Auto Updater", + "version": "2.8.341.10" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft 365 Apps for business - en-us", + "version": "16.0.19929.20106" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Command Line Utilities 11 for SQL Server", + "version": "11.0.2270.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Edge", + "version": "148.0.3967.83" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Edge WebView2 Runtime", + "version": "148.0.3967.83" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft ODBC Driver 11 for SQL Server", + "version": "11.0.2270.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft OneDrive", + "version": "26.078.0426.0002" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Update Health Tools", + "version": "3.74.0.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2005 Redistributable", + "version": "8.0.56336" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2005 Redistributable (x64)", + "version": "8.0.56336" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 Redistributable (x64) - 11.0.50727", + "version": "11.0.50727.1" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 Redistributable (x86) - 11.0.61030", + "version": "11.0.61030.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x64 Additional Runtime - 11.0.50727", + "version": "11.0.50727" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x64 Minimum Runtime - 11.0.50727", + "version": "11.0.50727" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x86 Additional Runtime - 11.0.61030", + "version": "11.0.61030" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2012 x86 Minimum Runtime - 11.0.61030", + "version": "11.0.61030" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2013 Redistributable (x64) - 12.0.30501", + "version": "12.0.30501.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2013 x64 Additional Runtime - 12.0.21005", + "version": "12.0.21005" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2013 x64 Minimum Runtime - 12.0.21005", + "version": "12.0.21005" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.44.35211", + "version": "14.44.35211.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2015-2022 Redistributable (x86) - 14.44.35211", + "version": "14.44.35211.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X64 Additional Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X64 Minimum Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X86 Additional Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual C++ 2022 X86 Minimum Runtime - 14.44.35211", + "version": "14.44.35211" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual Studio 2010 Tools for Office Runtime (x64)", + "version": "10.0.31119" + }, + { + "publisher": "Microsoft Corporation", + "name": "Microsoft Visual Studio 2010 Tools for Office Runtime (x64)", + "version": "10.0.31124" + }, + { + "publisher": "Microsoft Corporation", + "name": "Office 15 Click-to-Run Extensibility Component", + "version": "15.0.5603.1000" + }, + { + "publisher": "Microsoft Corporation", + "name": "Office 15 Click-to-Run Licensing Component", + "version": "15.0.5603.1000" + }, + { + "publisher": "Microsoft Corporation", + "name": "Office 15 Click-to-Run Localization Component", + "version": "15.0.5603.1000" + }, + { + "publisher": "Microsoft Corporation", + "name": "Office 16 Click-to-Run Extensibility Component", + "version": "16.0.19929.20106" + }, + { + "publisher": "ScreenConnect Software", + "name": "ScreenConnect Client (1912bf3444b41a08)", + "version": "26.1.24.9579" + }, + { + "publisher": "Splashtop Inc.", + "name": "Splashtop Streamer", + "version": "3.8.2.0" + }, + { + "publisher": "Stamps.com, Inc.", + "name": "Stamps.com", + "version": "20.0.1.5122" + }, + { + "publisher": "Stamps.com, Inc.", + "name": "Stamps.com Classic", + "version": "20.8.0.10190" + }, + { + "publisher": "Servably, Inc.", + "name": "Syncro", + "version": "1.0.201.18410" + }, + { + "publisher": "PCLaw | Time Matters?", + "name": "Time Matters?", + "version": "21.0.0.123" + }, + { + "publisher": "Tweaking.com", + "name": "Tweaking.com - Windows Repair", + "version": "4.14.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Update for x64-based Windows Systems (KB5001716)", + "version": "8.94.0.0" + }, + { + "publisher": "Microsoft Corporation", + "name": "Windows PC Health Check", + "version": "3.6.2204.08001" + }, + { + "publisher": "Microsoft Corporation", + "name": "Windows PC Health Check", + "version": "4.0.2410.23001" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office IFilter 32-bit", + "version": "1.4" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office IFilter 64-bit", + "version": "1.4" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6", + "version": "16.0.0.429" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Common Files", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Common Files English", + "version": "16.3.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - IPM", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Lightning Files", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Lightning Files English", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Oxford", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Presentations Files", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Presentations Files English", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Quattro Pro Files", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Quattro Pro Files English", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - Setup Files", + "version": "16.3.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - System Files", + "version": "16.1" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - WordPerfect Files", + "version": "16.3" + }, + { + "publisher": "Corel Corporation", + "name": "WordPerfect Office X6 - WordPerfect Files English", + "version": "16.3" + }, + { + "publisher": " Corel Corporation", + "name": "WordPerfect Office X6 - WT", + "version": "16.1" + } + ], + "tpm": { + "enabled": false, + "ready": false, + "present": false + }, + "local_groups": [ + "Access Control Assistance Operators", + "Administrators", + "Backup Operators", + "Cryptographic Operators", + "Device Owners", + "Distributed COM Users", + "Event Log Readers", + "Guests", + "Hyper-V Administrators", + "IIS_IUSRS", + "Network Configuration Operators", + "Performance Log Users", + "Performance Monitor Users", + "Power Users", + "Remote Desktop Users", + "Remote Management Users", + "Replicator", + "System Managed Accounts Group", + "Users" + ], + "battery": { + "present": false + }, + "activation": { + "edition": "Microsoft Windows 10 Pro", + "description": "Windows(R) Operating System, RETAIL channel", + "licensed": false, + "license_status_code": 5 + }, + "time_source": "time.windows.com,0x9", + "chassis_types": [ + 3 + ], + "last_hotfix": { + "hotfix_id": "KB5075039", + "installed_on": "2026-03-04T07:00:00Z" + }, + "antivirus_products": [ + "Windows Defender" + ], + "domain_joined": false, + "defender": { + "antispyware_signature_age": 0, + "tamper_protected": true, + "real_time_protection": true, + "nis_enabled": true, + "available": true, + "antivirus_enabled": true, + "am_service_enabled": true + }, + "bitlocker": { + "available": false, + "os_volume": "C:" + }, + "is_laptop": false, + "installed_software_count": 68, + "local_administrators": [ + "LEGALASST\\Administrator", + "LEGALASST\\Ale", + "LEGALASST\\Emma", + "LEGALASST\\localadmin" + ], + "domain": "WORKGROUP", + "foreign_agents": [ + "ScreenConnect / ConnectWise Control", + "Splashtop (SOS/Streamer)", + "Syncro / Kabuto" + ] + }, + "findings": [ + { + "id": "sec.defender.ok", + "category": "security", + "severity": "info", + "title": "Defender active and current", + "detail": "Real-time protection on, service running, signatures current.", + "evidence": "RealTimeProtectionEnabled=True; AMServiceEnabled=True; AntispywareSignatureAge=0 days; IsTamperProtected=True" + }, + { + "id": "sec.av_products.defender_only", + "category": "security", + "severity": "info", + "title": "Defender is the only registered AV", + "detail": "Only Microsoft/Windows Defender is registered in Security Center.", + "evidence": "Windows Defender" + }, + { + "id": "sec.foreign_agents.screenconnect_connectwise_control", + "category": "security", + "severity": "critical", + "title": "Foreign management/remote-access agent: ScreenConnect / ConnectWise Control", + "detail": "A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.", + "evidence": "program: ScreenConnect Client (1912bf3444b41a08) 26.1.24.9579\nservice: ScreenConnect Client (1912bf3444b41a08) (ScreenConnect Client (1912bf3444b41a08)) Running" + }, + { + "id": "sec.foreign_agents.splashtop_sos_streamer_", + "category": "security", + "severity": "critical", + "title": "Foreign management/remote-access agent: Splashtop (SOS/Streamer)", + "detail": "A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.", + "evidence": "program: Splashtop Streamer 3.8.2.0\nservice: SplashtopRemoteService (Splashtop? Remote Service) Running" + }, + { + "id": "sec.foreign_agents.syncro_kabuto", + "category": "security", + "severity": "critical", + "title": "Foreign management/remote-access agent: Syncro / Kabuto", + "detail": "A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it.", + "evidence": "program: Syncro 1.0.201.18410\nservice: Syncro (Syncro) Running" + }, + { + "id": "sec.firewall.error", + "category": "security", + "severity": "unknown", + "title": "Check failed: Windows Firewall profiles", + "detail": "The probe could not complete this check. Manual review recommended.", + "evidence": "Invalid class " + }, + { + "id": "sec.bitlocker.unavailable", + "category": "security", + "severity": "unknown", + "title": "BitLocker status unavailable", + "detail": "Get-BitLockerVolume failed for the OS volume. BitLocker may not be installed (Home edition) or the cmdlet is unavailable. Verify encryption manually (manage-bde -status).", + "evidence": "MountPoint=C:, Get-BitLockerVolume returned null" + }, + { + "id": "sec.local_admins.list", + "category": "security", + "severity": "info", + "title": "Local administrators (4)", + "detail": "Members of the local Administrators group. Review for unexpected or unknown accounts (especially leftover MSP/vendor accounts from a prior provider).", + "evidence": "LEGALASST\\Administrator\nLEGALASST\\Ale\nLEGALASST\\Emma\nLEGALASST\\localadmin" + }, + { + "id": "sec.patch.os_eol", + "category": "security", + "severity": "critical", + "title": "OS build is end-of-life: Win10 22H2", + "detail": "This OS build (19045, Win10 22H2) passed end-of-servicing on 2025-10-14. It no longer receives security updates. Plan a feature update or OS upgrade.", + "evidence": "Microsoft Windows 10 Pro build 19045; EOL 2025-10-14" + }, + { + "id": "sec.patch.pending", + "category": "security", + "severity": "warning", + "title": "1 pending Windows updates", + "detail": "Windows Update reports pending (not installed, not hidden) updates. Some may be security updates. Approve/install on the next maintenance window.", + "evidence": "Microsoft.Update.Session search IsInstalled=0 and IsHidden=0 -> 1" + }, + { + "id": "sec.patch.last_hotfix", + "category": "security", + "severity": "info", + "title": "Last hotfix: KB5075039", + "detail": "Most recently installed update (from Get-HotFix; reflects CBS/MSU packages, not all cumulative metadata).", + "evidence": "KB5075039 installed 2026-03-04T07:00:00Z" + }, + { + "id": "sec.exposure.smb1_off", + "category": "security", + "severity": "info", + "title": "SMBv1 disabled", + "detail": "SMBv1 server protocol is disabled.", + "evidence": "EnableSMB1Protocol=False" + }, + { + "id": "sec.exposure.laps_present", + "category": "security", + "severity": "info", + "title": "LAPS detected", + "detail": "A LAPS mechanism is present.", + "evidence": "Windows LAPS reg key" + }, + { + "id": "health.disk_smart.unavailable", + "category": "health", + "severity": "unknown", + "title": "Physical disk health unavailable", + "detail": "Get-PhysicalDisk is unavailable (older OS / RAID controller hiding disks). Verify drive health via vendor tools.", + "evidence": "Get-PhysicalDisk returned null" + }, + { + "id": "health.stability.some", + "category": "health", + "severity": "warning", + "title": "Stability events present in the last 14 days", + "detail": "One or more unexpected shutdowns, BSODs, or disk errors occurred recently. Monitor and correlate with user reports.", + "evidence": "Unexpected shutdowns (id 41)=0; Bugchecks/BSOD (id 1001)=0; Disk errors (id 7/51/153)=1" + }, + { + "id": "health.reboot_uptime.pending", + "category": "health", + "severity": "warning", + "title": "Reboot pending", + "detail": "A reboot is pending. Pending reboots can block patches and leave the system in a half-updated state. Schedule a restart.", + "evidence": "PendingFileRenameOperations" + }, + { + "id": "health.reboot_uptime.long_uptime", + "category": "health", + "severity": "warning", + "title": "Uptime is 43.1 days", + "detail": "Uptime exceeds 30 days. Long uptime usually means pending updates have not been applied (reboots deferred). Schedule maintenance.", + "evidence": "LastBootUpTime=2026-04-16 10:07:07Z" + }, + { + "id": "health.failed_services.stopped", + "category": "health", + "severity": "warning", + "title": "3 auto-start service(s) not running", + "detail": "These services are set to start automatically but are not running. Some may be benign; review for security agents, backup agents, or AV that should be running.", + "evidence": "WMPNetworkSvc (Windows Media Player Network Sharing Service) = Stopped\nGoogleUpdaterInternalService149.0.7814.0 (Google Updater Internal Service (GoogleUpdaterInternalService149.0.7814.0)) = Stopped\nGoogleUpdaterService149.0.7814.0 (Google Updater Service (GoogleUpdaterService149.0.7814.0)) = Stopped" + }, + { + "id": "health.domain.workgroup", + "category": "health", + "severity": "info", + "title": "Not domain-joined (workgroup)", + "detail": "This machine is in workgroup/Azure AD only mode (Domain=WORKGROUP). No on-prem AD secure channel applies.", + "evidence": "PartOfDomain=False; Domain=WORKGROUP" + }, + { + "id": "health.time.source", + "category": "health", + "severity": "info", + "title": "Time service source", + "detail": "Current Windows Time service source.", + "evidence": "Source=time.windows.com,0x9" + }, + { + "id": "health.backup.none", + "category": "health", + "severity": "info", + "title": "No backup agent detected", + "detail": "No known backup agent service found. Backup expectation varies by endpoint; confirm whether this machine is supposed to have local/cloud backup and whether server-side or M365 backup covers it.", + "evidence": "No matching backup service in Win32_Service" + } + ] +} diff --git a/clients/rednour/onboarding-baselines/LEGALASST-20260529T200647.md b/clients/rednour/onboarding-baselines/LEGALASST-20260529T200647.md new file mode 100644 index 0000000..6982cb5 --- /dev/null +++ b/clients/rednour/onboarding-baselines/LEGALASST-20260529T200647.md @@ -0,0 +1,258 @@ +# Onboarding Diagnostic Baseline - LEGALASST + +- **Grade:** RED +- **Host:** LEGALASST +- **Client:** Rednour Law Offices (`rednour`) +- **Collected (UTC):** 2026-05-29T20:05:50Z +- **Agent ID:** 18825ea7-df58-47bb-b492-822cb16fb5ec +- **Command ID:** beb27c88-4161-4183-a2b9-c43ec1ea0c0b +- **Findings:** 4 critical / 5 warning / 9 info / 3 unknown + +- **OS:** Microsoft Windows 10 Pro (build 19045) + +--- + +## CRITICAL (4) + +### Foreign management/remote-access agent: ScreenConnect / ConnectWise Control +- **Category:** security +- **ID:** `sec.foreign_agents.screenconnect_connectwise_control` +- A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it. + +``` +program: ScreenConnect Client (1912bf3444b41a08) 26.1.24.9579 +service: ScreenConnect Client (1912bf3444b41a08) (ScreenConnect Client (1912bf3444b41a08)) Running +``` + +### Foreign management/remote-access agent: Splashtop (SOS/Streamer) +- **Category:** security +- **ID:** `sec.foreign_agents.splashtop_sos_streamer_` +- A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it. + +``` +program: Splashtop Streamer 3.8.2.0 +service: SplashtopRemoteService (Splashtop? Remote Service) Running +``` + +### Foreign management/remote-access agent: Syncro / Kabuto +- **Category:** security +- **ID:** `sec.foreign_agents.syncro_kabuto` +- A competitor RMM or unmanaged remote-access tool is present. At onboarding this is a security and control risk (a prior MSP or attacker may retain remote access). Verify it is authorized; if not, remove it. + +``` +program: Syncro 1.0.201.18410 +service: Syncro (Syncro) Running +``` + +### OS build is end-of-life: Win10 22H2 +- **Category:** security +- **ID:** `sec.patch.os_eol` +- This OS build (19045, Win10 22H2) passed end-of-servicing on 2025-10-14. It no longer receives security updates. Plan a feature update or OS upgrade. + +``` +Microsoft Windows 10 Pro build 19045; EOL 2025-10-14 +``` + + +## WARNING (5) + +### 1 pending Windows updates +- **Category:** security +- **ID:** `sec.patch.pending` +- Windows Update reports pending (not installed, not hidden) updates. Some may be security updates. Approve/install on the next maintenance window. + +``` +Microsoft.Update.Session search IsInstalled=0 and IsHidden=0 -> 1 +``` + +### Stability events present in the last 14 days +- **Category:** health +- **ID:** `health.stability.some` +- One or more unexpected shutdowns, BSODs, or disk errors occurred recently. Monitor and correlate with user reports. + +``` +Unexpected shutdowns (id 41)=0; Bugchecks/BSOD (id 1001)=0; Disk errors (id 7/51/153)=1 +``` + +### Reboot pending +- **Category:** health +- **ID:** `health.reboot_uptime.pending` +- A reboot is pending. Pending reboots can block patches and leave the system in a half-updated state. Schedule a restart. + +``` +PendingFileRenameOperations +``` + +### Uptime is 43.1 days +- **Category:** health +- **ID:** `health.reboot_uptime.long_uptime` +- Uptime exceeds 30 days. Long uptime usually means pending updates have not been applied (reboots deferred). Schedule maintenance. + +``` +LastBootUpTime=2026-04-16 10:07:07Z +``` + +### 3 auto-start service(s) not running +- **Category:** health +- **ID:** `health.failed_services.stopped` +- These services are set to start automatically but are not running. Some may be benign; review for security agents, backup agents, or AV that should be running. + +``` +WMPNetworkSvc (Windows Media Player Network Sharing Service) = Stopped +GoogleUpdaterInternalService149.0.7814.0 (Google Updater Internal Service (GoogleUpdaterInternalService149.0.7814.0)) = Stopped +GoogleUpdaterService149.0.7814.0 (Google Updater Service (GoogleUpdaterService149.0.7814.0)) = Stopped +``` + + +## INFO (9) + +### Defender active and current +- **Category:** security +- **ID:** `sec.defender.ok` +- Real-time protection on, service running, signatures current. + +``` +RealTimeProtectionEnabled=True; AMServiceEnabled=True; AntispywareSignatureAge=0 days; IsTamperProtected=True +``` + +### Defender is the only registered AV +- **Category:** security +- **ID:** `sec.av_products.defender_only` +- Only Microsoft/Windows Defender is registered in Security Center. + +``` +Windows Defender +``` + +### Local administrators (4) +- **Category:** security +- **ID:** `sec.local_admins.list` +- Members of the local Administrators group. Review for unexpected or unknown accounts (especially leftover MSP/vendor accounts from a prior provider). + +``` +LEGALASST\Administrator +LEGALASST\Ale +LEGALASST\Emma +LEGALASST\localadmin +``` + +### Last hotfix: KB5075039 +- **Category:** security +- **ID:** `sec.patch.last_hotfix` +- Most recently installed update (from Get-HotFix; reflects CBS/MSU packages, not all cumulative metadata). + +``` +KB5075039 installed 2026-03-04T07:00:00Z +``` + +### SMBv1 disabled +- **Category:** security +- **ID:** `sec.exposure.smb1_off` +- SMBv1 server protocol is disabled. + +``` +EnableSMB1Protocol=False +``` + +### LAPS detected +- **Category:** security +- **ID:** `sec.exposure.laps_present` +- A LAPS mechanism is present. + +``` +Windows LAPS reg key +``` + +### Not domain-joined (workgroup) +- **Category:** health +- **ID:** `health.domain.workgroup` +- This machine is in workgroup/Azure AD only mode (Domain=WORKGROUP). No on-prem AD secure channel applies. + +``` +PartOfDomain=False; Domain=WORKGROUP +``` + +### Time service source +- **Category:** health +- **ID:** `health.time.source` +- Current Windows Time service source. + +``` +Source=time.windows.com,0x9 +``` + +### No backup agent detected +- **Category:** health +- **ID:** `health.backup.none` +- No known backup agent service found. Backup expectation varies by endpoint; confirm whether this machine is supposed to have local/cloud backup and whether server-side or M365 backup covers it. + +``` +No matching backup service in Win32_Service +``` + + +## UNKNOWN (3) + +### Check failed: Windows Firewall profiles +- **Category:** security +- **ID:** `sec.firewall.error` +- The probe could not complete this check. Manual review recommended. + +``` +Invalid class +``` + +### BitLocker status unavailable +- **Category:** security +- **ID:** `sec.bitlocker.unavailable` +- Get-BitLockerVolume failed for the OS volume. BitLocker may not be installed (Home edition) or the cmdlet is unavailable. Verify encryption manually (manage-bde -status). + +``` +MountPoint=C:, Get-BitLockerVolume returned null +``` + +### Physical disk health unavailable +- **Category:** health +- **ID:** `health.disk_smart.unavailable` +- Get-PhysicalDisk is unavailable (older OS / RAID controller hiding disks). Verify drive health via vendor tools. + +``` +Get-PhysicalDisk returned null +``` + + +--- + +## Inventory Baseline Summary + +- **Manufacturer / Model:** To Be Filled By O.E.M. / To Be Filled By O.E.M. +- **Serial:** To Be Filled By O.E.M. +- **CPU:** AMD Ryzen 3 3200G with Radeon Vega Graphics (4 cores / 4 logical) +- **RAM (GB):** 5.9 +- **BIOS:** P3.50 (2019-05-15) +- **Chassis is laptop:** false +- **TPM present / Secure Boot:** ? / ? +- **Domain joined:** false (WORKGROUP) +- **OS activation licensed:** ? +- **Uptime (days):** 43.1 +- **Pending reboot:** true +- **Installed software count:** 68 +- **Scheduled tasks (non-MS, enabled):** 13 +- **Local administrators:** LEGALASST\Administrator, LEGALASST\Ale, LEGALASST\Emma, LEGALASST\localadmin + +### Fixed volumes + + +### Network adapters + +- Realtek PCIe GbE Family Controller - IP: 192.168.10.213 - DNS: 192.168.10.1 - DHCP: true + +--- + +## Diff vs Prior Baseline + +- No prior baseline found for this host. This is the first baseline. + +--- + +_Generated by run-onboarding-diagnostic.sh (GuruRMM onboarding diagnostic, Phase 1). Raw snapshot: `LEGALASST-20260529T200647.json` (immutable)._