Files
claudetools/projects/msp-tools/guru-scan/Invoke-PostRebootCleanup.ps1
Howard Enos 3a0c83dd42 feat: add GuruScan standalone multi-scanner security suite
Adds a complete PowerShell-based malware scanning toolkit:

- Invoke-GuruScan.ps1: main orchestrator running RKill, AdwCleaner,
  Emsisoft, HitmanPro, and ESET in sequence with pre/post cleanup,
  whitelist support, ForceRemove blacklist, and -Headless switch
- Invoke-PostRebootCleanup.ps1: post-reboot temp-user session that
  shows a fullscreen splash, verifies boot-time cleanup completed,
  removes scanner files, and restores the original user login name
- Download-Scanners.ps1: downloads/refreshes scanner EXEs
- Get-ScanSummary.ps1: parses results.json with optional Ollama AI analysis
- Invoke-Remediation.ps1: re-runs scanners in clean mode

Key features: exit-code-based reboot detection, whoami-based user
capture (SYSTEM-safe via quser fallback), domain\user and local
MACHINE\user restore on login screen after cleanup reboot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:40:56 -07:00

216 lines
9.5 KiB
PowerShell

#Requires -RunAsAdministrator
<#
.SYNOPSIS
Post-reboot cleanup agent for GuruScan. Runs automatically as GuruRMM-Temp
after a reboot triggered by scanner exit code 2 (pending removal required).
Shows a full-screen splash, verifies cleanup completed, removes scanner files,
restores the original user's login name, then logs off.
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Base = 'C:\GuruScan'
$StateFile = "$Base\cleanup-state.json"
$TempUser = 'GuruRMM-Temp'
$WlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
$SpecialKey= 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList'
# ---------------------------------------------------------------------------
# Kill Explorer so we have a clean screen before showing splash
# ---------------------------------------------------------------------------
Get-Process -Name explorer -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
# ---------------------------------------------------------------------------
# Full-screen splash in a STA runspace (non-blocking)
# ---------------------------------------------------------------------------
$sync = [hashtable]::Synchronized(@{ Close = $false })
$uiRS = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$uiRS.ApartmentState = 'STA'
$uiRS.ThreadOptions = 'ReuseThread'
$uiRS.Open()
$uiPS = [System.Management.Automation.PowerShell]::Create()
$uiPS.Runspace = $uiRS
[void]$uiPS.AddScript({
param($s)
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase
$win = New-Object System.Windows.Window
$win.WindowStyle = 'None'
$win.WindowState = 'Maximized'
$win.Background = [System.Windows.Media.Brushes]::Black
$win.Topmost = $true
$win.Title = 'GuruRMM'
$win.Cursor = [System.Windows.Input.Cursors]::None
$panel = New-Object System.Windows.Controls.StackPanel
$panel.VerticalAlignment = 'Center'
$panel.HorizontalAlignment = 'Center'
$title = New-Object System.Windows.Controls.TextBlock
$title.Text = 'GuruRMM'
$title.Foreground = [System.Windows.Media.Brushes]::White
$title.FontSize = 42
$title.FontFamily = New-Object System.Windows.Media.FontFamily('Segoe UI')
$title.FontWeight = [System.Windows.FontWeights]::Light
$title.TextAlignment = 'Center'
$title.Margin = New-Object System.Windows.Thickness(0,0,0,24)
$body = New-Object System.Windows.Controls.TextBlock
$body.Text = "Security cleanup is being completed on this machine.`n`nPlease do not power off this computer.`n`nThis process will finish shortly."
$body.Foreground = [System.Windows.Media.SolidColorBrush][System.Windows.Media.Color]::FromRgb(180,180,180)
$body.FontSize = 22
$body.FontFamily = New-Object System.Windows.Media.FontFamily('Segoe UI')
$body.TextAlignment = 'Center'
$body.LineHeight = 38
[void]$panel.Children.Add($title)
[void]$panel.Children.Add($body)
$win.Content = $panel
$timer = New-Object System.Windows.Threading.DispatcherTimer
$timer.Interval = [TimeSpan]::FromMilliseconds(500)
$timer.Add_Tick({ if ($s.Close) { $win.Close(); $timer.Stop() } })
$timer.Start()
[void]$win.ShowDialog()
}).AddArgument($sync)
$uiHandle = $uiPS.BeginInvoke()
# ---------------------------------------------------------------------------
# Read state file
# ---------------------------------------------------------------------------
$state = @{ original_user = ''; scan_id = ''; log_root = '' }
if (Test-Path $StateFile) {
try { $state = Get-Content $StateFile -Raw | ConvertFrom-Json } catch {}
}
$logRoot = $state.log_root
$scanId = $state.scan_id
$origUser = $state.original_user
# ---------------------------------------------------------------------------
# Wait for boot-time cleanup to settle
# ---------------------------------------------------------------------------
Start-Sleep -Seconds 60
# ---------------------------------------------------------------------------
# Verify: check PendingFileRenameOperations (empty = boot cleanup completed)
# ---------------------------------------------------------------------------
$pendingKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
$pendingOps = (Get-ItemProperty -Path $pendingKey -Name PendingFileRenameOperations -ErrorAction SilentlyContinue).PendingFileRenameOperations
$pendingDone = (-not $pendingOps -or $pendingOps.Count -eq 0)
# Check scanner post-reboot logs
$adwLog = Get-ChildItem 'C:\AdwCleaner\Logs' -Filter '*.txt' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
$hitLog = if ($logRoot -and (Test-Path "$logRoot\HitmanPro_Scan_Log.txt")) {
Get-Item "$logRoot\HitmanPro_Scan_Log.txt"
} else { $null }
$verification = [ordered]@{
checked_at = (Get-Date).ToUniversalTime().ToString('o')
pending_ops_cleared = $pendingDone
adwcleaner_log = if ($adwLog) { $adwLog.FullName } else { 'not found' }
hitmanpro_log = if ($hitLog) { $hitLog.FullName } else { 'not found' }
result = if ($pendingDone) { 'clean' } else { 'pending_items_remain' }
}
# Write verification result alongside original results.json
if ($logRoot -and (Test-Path $logRoot)) {
$verification | ConvertTo-Json | Set-Content "$logRoot\post_reboot_verification.json" -Encoding UTF8
}
# ---------------------------------------------------------------------------
# Remove scanner installation files from this machine
# ---------------------------------------------------------------------------
$scannerPaths = @(
'C:\EmsisoftCmd',
'C:\AdwCleaner',
'C:\ProgramData\HitmanPro',
'C:\ProgramData\HitmanPro.Alert'
)
foreach ($p in $scannerPaths) {
Remove-Item -Path $p -Recurse -Force -ErrorAction SilentlyContinue
}
# ---------------------------------------------------------------------------
# Flag logs for GuruRMM to pull (agent reads this file)
# ---------------------------------------------------------------------------
@{
scan_id = $scanId
log_root = $logRoot
zip_path = "$Base\reports\$scanId.zip"
verified = $verification.result
flagged_at = (Get-Date).ToUniversalTime().ToString('o')
} | ConvertTo-Json | Set-Content "$Base\logs-ready.json" -Encoding UTF8
# ---------------------------------------------------------------------------
# Restore original user's name in the login screen.
# Win32_ComputerSystem.UserName returns "DOMAIN\user" or "MACHINE\user".
# Split so the login screen pre-fills both fields correctly.
# ---------------------------------------------------------------------------
if ($origUser) {
try {
if ($origUser -match '^(.+)\\(.+)$') {
$restoreDomain = $Matches[1]
$restoreUser = $Matches[2]
} else {
$restoreDomain = $env:COMPUTERNAME
$restoreUser = $origUser
}
Set-ItemProperty -Path $WlKey -Name 'DefaultUserName' -Value $restoreUser
Set-ItemProperty -Path $WlKey -Name 'DefaultDomainName' -Value $restoreDomain
} catch {}
}
# ---------------------------------------------------------------------------
# Clear autologon settings
# ---------------------------------------------------------------------------
try {
Set-ItemProperty -Path $WlKey -Name 'AutoAdminLogon' -Value '0'
Remove-ItemProperty -Path $WlKey -Name 'DefaultPassword' -ErrorAction SilentlyContinue
} catch {}
# ---------------------------------------------------------------------------
# Remove our scheduled logon task
# ---------------------------------------------------------------------------
Unregister-ScheduledTask -TaskName 'GuruRMM-PostRebootCleanup' -Confirm:$false -ErrorAction SilentlyContinue
# ---------------------------------------------------------------------------
# Schedule SYSTEM task to delete GuruRMM-Temp 2 minutes from now
# (can't delete the account we're currently logged into)
# ---------------------------------------------------------------------------
try {
$deleteScript = @"
Remove-LocalUser -Name '$TempUser' -ErrorAction SilentlyContinue
Remove-ItemProperty -Path '$SpecialKey' -Name '$TempUser' -ErrorAction SilentlyContinue
Remove-Item -Path 'C:\Users\$TempUser' -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path '$StateFile' -Force -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName 'GuruRMM-TempUserDelete' -Confirm:`$false -ErrorAction SilentlyContinue
"@
$deleteScript | Set-Content "$Base\delete-temp-user.ps1" -Encoding UTF8
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$Base\delete-temp-user.ps1`""
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2)
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
Register-ScheduledTask -TaskName 'GuruRMM-TempUserDelete' -Action $action `
-Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
} catch {}
# ---------------------------------------------------------------------------
# Close splash and log off
# ---------------------------------------------------------------------------
$sync.Close = $true
Start-Sleep -Seconds 3
$uiPS.Stop()
$uiRS.Close()
& logoff