sync: auto-sync from HOWARD-HOME at 2026-05-26 21:58:00
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-05-26 21:58:00
This commit is contained in:
@@ -33,262 +33,11 @@ param(
|
||||
[string]$OllamaModel = 'qwen3.6:latest'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ollama helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Invoke-Ollama {
|
||||
param([string]$Prompt, [string]$Model = $OllamaModel, [string]$BaseUrl = $OllamaUrl)
|
||||
$body = @{ model = $Model; prompt = $Prompt; stream = $false } | ConvertTo-Json -Depth 3
|
||||
try {
|
||||
$r = Invoke-RestMethod -Uri "$BaseUrl/api/generate" -Method POST `
|
||||
-Body $body -ContentType 'application/json' -TimeoutSec 180
|
||||
return ($r.response -replace '</?think[^>]*>', '' -replace '(?s)<think>.*?</think>', '').Trim()
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locate results.json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if (-not $ResultsFile) {
|
||||
$scanLogsRoot = 'C:\ScanLogs'
|
||||
if (-not (Test-Path $scanLogsRoot)) {
|
||||
Write-Host "[ERROR] No results file specified and C:\ScanLogs does not exist." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$latestJson = Get-ChildItem -Path $scanLogsRoot -Recurse -Filter 'results.json' -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $latestJson) {
|
||||
Write-Host "[ERROR] No results.json found under C:\ScanLogs\" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ResultsFile = $latestJson.FullName
|
||||
Write-Host "[INFO] Using latest results: $ResultsFile" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ResultsFile)) {
|
||||
Write-Host "[ERROR] Results file not found: $ResultsFile" -ForegroundColor Red
|
||||
$moduleManifest = Join-Path $PSScriptRoot 'GuruScan.psd1'
|
||||
if (-not (Test-Path $moduleManifest)) {
|
||||
Write-Host "[ERROR] GuruScan module not found: $moduleManifest" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse JSON
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$data = Get-Content $ResultsFile -Raw | ConvertFrom-Json
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Header
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$startDt = [datetime]::Parse($data.started_at).ToLocalTime()
|
||||
$endDt = [datetime]::Parse($data.completed_at).ToLocalTime()
|
||||
$totalDurMin = [math]::Round(($endDt - $startDt).TotalMinutes, 1)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " GuruScan Report" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Machine : $($data.machine)"
|
||||
Write-Host " Scan ID : $($data.scan_id)"
|
||||
Write-Host " Mode : $($data.scan_mode)"
|
||||
Write-Host " Started : $($startDt.ToString('yyyy-MM-dd HH:mm:ss'))"
|
||||
Write-Host " Completed : $($endDt.ToString('yyyy-MM-dd HH:mm:ss'))"
|
||||
Write-Host " Duration : $totalDurMin min total"
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-scanner table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$rows = foreach ($s in $data.scanners) {
|
||||
$threatStr = if ($s.threats_found -gt 0) { "YES ($($s.threats_found))" } else { 'Clean' }
|
||||
$statusColor = switch ($s.status) {
|
||||
'completed' { 'Green' }
|
||||
{ $_ -like 'TIMED*' } { 'Yellow' }
|
||||
default { 'Red' }
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Scanner = $s.name
|
||||
Status = $s.status
|
||||
ExitCode = $s.exit_code
|
||||
Duration = "$($s.duration_min) min"
|
||||
Threats = $threatStr
|
||||
'_Color' = $statusColor
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host " Scanner Results:" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
$header = ' {0,-18} {1,-28} {2,-10} {3,-12} {4}' -f 'Scanner', 'Status', 'ExitCode', 'Duration', 'Threats'
|
||||
Write-Host $header -ForegroundColor Gray
|
||||
Write-Host (' ' + ('-' * 78)) -ForegroundColor DarkGray
|
||||
|
||||
foreach ($row in $rows) {
|
||||
$line = ' {0,-18} {1,-28} {2,-10} {3,-12} {4}' -f $row.Scanner, $row.Status, $row.ExitCode, $row.Duration, $row.Threats
|
||||
Write-Host $line -ForegroundColor $row.'_Color'
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Threat summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$threatScanners = $data.scanners | Where-Object { $_.threats_found -gt 0 }
|
||||
$nonClean = $data.scanners | Where-Object { $_.status -ne 'completed' }
|
||||
|
||||
if ($data.total_threats -gt 0) {
|
||||
Write-Host "================================================================" -ForegroundColor Yellow
|
||||
Write-Host " [WARNING] THREATS DETECTED" -ForegroundColor Yellow
|
||||
Write-Host "================================================================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host " Scanners that detected threats:" -ForegroundColor Yellow
|
||||
foreach ($ts in $threatScanners) {
|
||||
Write-Host " - $($ts.name) (exit code $($ts.exit_code), log: $($ts.log_path))" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
} else {
|
||||
Write-Host " [OK] No threats detected across all scanners." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($nonClean) {
|
||||
Write-Host " [WARNING] Scanners with non-completed status:" -ForegroundColor Yellow
|
||||
foreach ($nc in $nonClean) {
|
||||
Write-Host " - $($nc.name): $($nc.status)" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Recommendation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Recommendation" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
|
||||
if ($data.reboot_required) {
|
||||
Write-Host " [WARNING] A REBOOT IS REQUIRED to complete remediation." -ForegroundColor Yellow
|
||||
Write-Host " Reboot the machine, then re-run GuruScan to verify." -ForegroundColor Yellow
|
||||
}
|
||||
elseif ($data.total_threats -gt 0 -and $data.scan_mode -eq 'scan') {
|
||||
Write-Host " [INFO] Threats were detected in scan-only mode." -ForegroundColor Cyan
|
||||
Write-Host " Run: .\Invoke-GuruScan.ps1 (without -ScanOnly)" -ForegroundColor Cyan
|
||||
Write-Host " Or: .\Invoke-Remediation.ps1 -LogRoot `"$(Split-Path $ResultsFile)`"" -ForegroundColor Cyan
|
||||
}
|
||||
elseif ($data.total_threats -gt 0) {
|
||||
Write-Host " [INFO] Clean mode was run. Review logs to confirm threats removed." -ForegroundColor Cyan
|
||||
Write-Host " Log folder: $(Split-Path $ResultsFile)" -ForegroundColor Cyan
|
||||
}
|
||||
else {
|
||||
Write-Host " [OK] System appears clean. No further action required." -ForegroundColor Green
|
||||
Write-Host " Consider running ESET Online Scanner for a second opinion." -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Full logs: $(Split-Path $ResultsFile)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ollama AI analysis (optional — requires -AI switch)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if ($AI) {
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " AI Analysis (Ollama)" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Model : $OllamaModel"
|
||||
Write-Host " Server : $OllamaUrl"
|
||||
Write-Host ""
|
||||
|
||||
# Collect log content from all scanner log folders (cap at 8KB each to avoid huge prompts)
|
||||
$logFolder = Split-Path $ResultsFile
|
||||
$logContent = [System.Text.StringBuilder]::new()
|
||||
Get-ChildItem -Path $logFolder -Recurse -Include '*.log','*.txt','*.csv' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Length -gt 0 -and $_.Length -lt 200KB } |
|
||||
ForEach-Object {
|
||||
$snippet = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
|
||||
if ($snippet) {
|
||||
$snippet = if ($snippet.Length -gt 8192) { $snippet.Substring(0, 8192) + "`n[TRUNCATED]" } else { $snippet }
|
||||
[void]$logContent.AppendLine("=== $($_.Name) ===")
|
||||
[void]$logContent.AppendLine($snippet)
|
||||
[void]$logContent.AppendLine()
|
||||
}
|
||||
}
|
||||
|
||||
$logText = if ($logContent.Length -gt 0) { $logContent.ToString() } else { 'No log files found.' }
|
||||
|
||||
$scanJson = $data | ConvertTo-Json -Depth 5
|
||||
|
||||
# Step 1: Extract specific threat names / findings
|
||||
Write-Host " [....] Extracting threat details..." -ForegroundColor Cyan
|
||||
$threatPrompt = @"
|
||||
You are a malware analyst. Below is a JSON scan summary and raw log excerpts from a multi-engine malware scan.
|
||||
|
||||
SCAN SUMMARY JSON:
|
||||
$scanJson
|
||||
|
||||
RAW LOG EXCERPTS:
|
||||
$logText
|
||||
|
||||
Task: Extract a concise list of specific threat names, file paths, registry keys, or other findings from the logs.
|
||||
Format your response as a plain numbered list. If no specific threats are named in the logs, say "No named threats found in logs."
|
||||
Do not add commentary — only the list.
|
||||
"@
|
||||
|
||||
$threatDetails = Invoke-Ollama -Prompt $threatPrompt
|
||||
if ($threatDetails) {
|
||||
Write-Host ""
|
||||
Write-Host " Identified Threats / Findings:" -ForegroundColor Yellow
|
||||
$threatDetails -split "`n" | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
|
||||
} else {
|
||||
Write-Host " [WARNING] Ollama unavailable — skipping threat extraction." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Step 2: Prioritized remediation recommendations
|
||||
Write-Host ""
|
||||
Write-Host " [....] Generating remediation recommendations..." -ForegroundColor Cyan
|
||||
$remediationPrompt = @"
|
||||
You are an MSP technician. A multi-engine malware scan produced these results:
|
||||
|
||||
$scanJson
|
||||
|
||||
Threat details extracted from logs:
|
||||
$threatDetails
|
||||
|
||||
Task: Write a short, prioritized remediation checklist (max 8 steps) for the technician.
|
||||
Include: immediate actions, follow-up verification steps, and whether a reboot is needed.
|
||||
Plain numbered list, no markdown headers, no padding text. Be specific.
|
||||
"@
|
||||
|
||||
$recommendations = Invoke-Ollama -Prompt $remediationPrompt
|
||||
if ($recommendations) {
|
||||
Write-Host ""
|
||||
Write-Host " Remediation Checklist:" -ForegroundColor Cyan
|
||||
$recommendations -split "`n" | ForEach-Object { Write-Host " $_" -ForegroundColor Cyan }
|
||||
} else {
|
||||
Write-Host " [WARNING] Ollama unavailable — skipping recommendations." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor DarkGray
|
||||
Write-Host " NOTE: AI output is advisory. Review logs before acting." -ForegroundColor DarkGray
|
||||
Write-Host "================================================================" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
exit 0
|
||||
Import-Module $moduleManifest -Force
|
||||
Get-ScanSummary @PSBoundParameters
|
||||
|
||||
27
projects/msp-tools/guru-scan/GuruScan.psd1
Normal file
27
projects/msp-tools/guru-scan/GuruScan.psd1
Normal file
@@ -0,0 +1,27 @@
|
||||
@{
|
||||
RootModule = 'GuruScan.psm1'
|
||||
ModuleVersion = '1.0.0'
|
||||
GUID = 'a3f2c1d4-8e5b-4a7f-9c2e-1b3d5f7a9e0c'
|
||||
Author = 'Arizona Computer Guru'
|
||||
CompanyName = 'Arizona Computer Guru LLC'
|
||||
Description = 'Multi-engine malware scan orchestrator for GuruRMM'
|
||||
PowerShellVersion = '5.1'
|
||||
|
||||
FunctionsToExport = @(
|
||||
'Invoke-GuruScan',
|
||||
'Invoke-Remediation',
|
||||
'Get-ScanSummary',
|
||||
'Invoke-PostRebootCleanup'
|
||||
)
|
||||
|
||||
CmdletsToExport = @()
|
||||
VariablesToExport = @()
|
||||
AliasesToExport = @()
|
||||
|
||||
PrivateData = @{
|
||||
PSData = @{
|
||||
Tags = @('malware', 'scanner', 'remediation', 'msp', 'security')
|
||||
ProjectUri = 'https://git.azcomputerguru.com/azcomputerguru/claudetools'
|
||||
}
|
||||
}
|
||||
}
|
||||
1391
projects/msp-tools/guru-scan/GuruScan.psm1
Normal file
1391
projects/msp-tools/guru-scan/GuruScan.psm1
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,215 +1,13 @@
|
||||
#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.
|
||||
Manually triggers GuruScan post-scan cleanup (removes scanner files).
|
||||
Normally this runs automatically via the GuruRMM-ScannerCleanup scheduled task.
|
||||
#>
|
||||
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 {}
|
||||
$moduleManifest = Join-Path $PSScriptRoot 'GuruScan.psd1'
|
||||
if (-not (Test-Path $moduleManifest)) {
|
||||
Write-Host "[ERROR] GuruScan module not found: $moduleManifest" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$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
|
||||
Import-Module $moduleManifest -Force
|
||||
Invoke-PostRebootCleanup
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -RunAsAdministrator
|
||||
#Requires -RunAsAdministrator
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Re-runs GuruScan scanners in clean mode against a previous scan's log folder.
|
||||
@@ -25,436 +25,11 @@ param(
|
||||
[string[]]$Scanners
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers (duplicated here so this script is self-contained)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
function Expand-TokenizedArgs {
|
||||
param([string[]]$Args, [string]$LogRoot, [string]$ExeDir)
|
||||
$out = foreach ($a in $Args) {
|
||||
$a = $a -replace '\{LOG_ROOT\}', $LogRoot
|
||||
$a = $a -replace '\{EXE_DIR\}', $ExeDir
|
||||
$a
|
||||
}
|
||||
return $out
|
||||
}
|
||||
|
||||
function Expand-EnvPath {
|
||||
param([string]$Path)
|
||||
return [System.Environment]::ExpandEnvironmentVariables($Path)
|
||||
}
|
||||
|
||||
function Remove-PathSilent {
|
||||
param([string]$Path)
|
||||
$expanded = Expand-EnvPath $Path
|
||||
if (Test-Path $expanded) {
|
||||
Remove-Item -Path $expanded -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ProcessWithTimeout {
|
||||
param(
|
||||
[System.Diagnostics.Process]$Process,
|
||||
[int]$TimeoutSeconds
|
||||
)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
while (-not $Process.HasExited) {
|
||||
if ((Get-Date) -gt $deadline) {
|
||||
try { $Process.Kill() } catch { }
|
||||
return $false
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Wait-ServicesToStop {
|
||||
param([string[]]$ServiceNames, [int]$TimeoutSeconds = 120)
|
||||
if (-not $ServiceNames -or $ServiceNames.Count -eq 0) { return }
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
foreach ($svc in $ServiceNames) {
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$s = Get-Service -Name $svc -ErrorAction SilentlyContinue
|
||||
if (-not $s -or $s.Status -ne 'Running') { break }
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-HitmanProTrialReset {
|
||||
Remove-Item -Path "HKLM:\SOFTWARE\HitmanPro" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "HKLM:\SOFTWARE\HitmanPro.Alert" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -Path "HKLM:\SOFTWARE\HitmanPro" -Force | Out-Null
|
||||
New-Item -Path "HKLM:\SOFTWARE\HitmanPro.Alert" -Force | Out-Null
|
||||
}
|
||||
|
||||
function Get-ExitCodeThreats {
|
||||
param([string]$ScannerName, [int]$ExitCode)
|
||||
switch ($ScannerName) {
|
||||
'AdwCleaner' { if ($ExitCode -eq 1) { return 1 } }
|
||||
'MSERT' { if ($ExitCode -ne 0) { return 1 } }
|
||||
'TDSSKiller' { if ($ExitCode -eq 1) { return 1 } }
|
||||
'HitmanPro' { if ($ExitCode -eq 1) { return 1 } }
|
||||
'Emsisoft' { if ($ExitCode -ge 1) { return 1 } }
|
||||
'Stinger' { if ($ExitCode -eq 13) { return 1 } }
|
||||
'ESET' { if ($ExitCode -ne 0) { return 1 } }
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validate inputs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$ScriptRoot = $PSScriptRoot
|
||||
$ConfigPath = Join-Path $ScriptRoot 'scanners.json'
|
||||
$PriorResults = Join-Path $LogRoot 'results.json'
|
||||
|
||||
if (-not (Test-Path $LogRoot)) {
|
||||
Write-Host "[ERROR] LogRoot not found: $LogRoot" -ForegroundColor Red
|
||||
$moduleManifest = Join-Path $PSScriptRoot 'GuruScan.psd1'
|
||||
if (-not (Test-Path $moduleManifest)) {
|
||||
Write-Host "[ERROR] GuruScan module not found: $moduleManifest" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $PriorResults)) {
|
||||
Write-Host "[ERROR] results.json not found in: $LogRoot" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ConfigPath)) {
|
||||
Write-Host "[ERROR] scanners.json not found: $ConfigPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load config and prior results
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||
$priorData = Get-Content $PriorResults -Raw | ConvertFrom-Json
|
||||
|
||||
$priorScannerNames = $priorData.scanners |
|
||||
Where-Object { $_.status -eq 'completed' } |
|
||||
ForEach-Object { $_.name }
|
||||
|
||||
$scannerList = $config.scanners | Where-Object { $priorScannerNames -contains $_.name }
|
||||
|
||||
if ($Scanners -and $Scanners.Count -gt 0) {
|
||||
$scannerList = $scannerList | Where-Object { $Scanners -contains $_.name }
|
||||
}
|
||||
|
||||
if (-not $scannerList) {
|
||||
Write-Host "[WARNING] No eligible scanners to remediate." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== GuruScan Remediation ===" -ForegroundColor Cyan
|
||||
Write-Host " Log root : $LogRoot"
|
||||
Write-Host " Scanners : $($scannerList.name -join ', ')"
|
||||
Write-Host ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Run scanners in clean mode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$results = [System.Collections.Generic.List[pscustomobject]]::new()
|
||||
$startedAt = Get-Date
|
||||
$totalThreats = 0
|
||||
|
||||
foreach ($s in $scannerList) {
|
||||
Write-Host "------------------------------------------------------------" -ForegroundColor DarkGray
|
||||
Write-Host " Scanner : $($s.name) [CLEAN MODE]" -ForegroundColor Cyan
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Resolve EXE paths
|
||||
# ------------------------------------------------------------------
|
||||
$mainExePath = $null
|
||||
$installerExePath = $null
|
||||
|
||||
if ($s.installer_exe) {
|
||||
$installerExePath = if ([System.IO.Path]::IsPathRooted($s.installer_exe)) {
|
||||
$s.installer_exe
|
||||
} else {
|
||||
Join-Path $ScriptRoot $s.installer_exe
|
||||
}
|
||||
}
|
||||
|
||||
$rawExe = $s.exe
|
||||
if ([System.IO.Path]::IsPathRooted($rawExe)) {
|
||||
$mainExePath = $rawExe
|
||||
} else {
|
||||
$mainExePath = Join-Path $ScriptRoot $rawExe
|
||||
}
|
||||
|
||||
$exeToCheck = if ($installerExePath) { $installerExePath } else { $mainExePath }
|
||||
|
||||
if (-not (Test-Path $exeToCheck)) {
|
||||
Write-Host " [WARNING] EXE not found — skipping: $exeToCheck" -ForegroundColor Yellow
|
||||
$results.Add([pscustomobject]@{
|
||||
Scanner = $s.name
|
||||
Status = 'SKIPPED (missing)'
|
||||
ExitCode = $null
|
||||
ThreatsFound = 0
|
||||
Duration = '0 min'
|
||||
LogPath = ''
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Resolve args (always clean_args)
|
||||
# ------------------------------------------------------------------
|
||||
$exeDir = Split-Path $mainExePath -Parent
|
||||
$expandedArgs = Expand-TokenizedArgs -Args @($s.clean_args) -LogRoot $LogRoot -ExeDir $exeDir
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# HitmanPro trial reset
|
||||
# ------------------------------------------------------------------
|
||||
if ($s.hitmanpro_trial_reset -eq $true) {
|
||||
Write-Host " [INFO] Resetting HitmanPro trial registry..." -ForegroundColor Cyan
|
||||
Invoke-HitmanProTrialReset
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Pre-clean
|
||||
# ------------------------------------------------------------------
|
||||
foreach ($p in $s.pre_clean_paths) {
|
||||
Remove-PathSilent $p
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Timeout
|
||||
# ------------------------------------------------------------------
|
||||
$timeoutSec = $s.timeout_min * 60
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Two-step install (Emsisoft)
|
||||
# ------------------------------------------------------------------
|
||||
if ($installerExePath) {
|
||||
Write-Host " [INFO] Running installer: $installerExePath" -ForegroundColor Cyan
|
||||
try {
|
||||
$instProc = Start-Process -FilePath $installerExePath -ArgumentList '/S' -PassThru -NoNewWindow
|
||||
$instCompleted = Wait-ProcessWithTimeout -Process $instProc -TimeoutSeconds 300
|
||||
if (-not $instCompleted) {
|
||||
Write-Host " [WARNING] Installer timed out — skipping" -ForegroundColor Yellow
|
||||
$results.Add([pscustomobject]@{
|
||||
Scanner = $s.name
|
||||
Status = 'FAILED (installer timeout)'
|
||||
ExitCode = $null
|
||||
ThreatsFound = 0
|
||||
Duration = '0 min'
|
||||
LogPath = ''
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " [ERROR] Installer failed: $_" -ForegroundColor Red
|
||||
$results.Add([pscustomobject]@{
|
||||
Scanner = $s.name
|
||||
Status = "FAILED (installer error)"
|
||||
ExitCode = $null
|
||||
ThreatsFound = 0
|
||||
Duration = '0 min'
|
||||
LogPath = ''
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Test-Path $mainExePath)) {
|
||||
Write-Host " [ERROR] Main EXE missing after install: $mainExePath" -ForegroundColor Red
|
||||
$results.Add([pscustomobject]@{
|
||||
Scanner = $s.name
|
||||
Status = 'FAILED (post-install EXE missing)'
|
||||
ExitCode = $null
|
||||
ThreatsFound = 0
|
||||
Duration = '0 min'
|
||||
LogPath = ''
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host " [INFO] Updating Emsisoft definitions..." -ForegroundColor Cyan
|
||||
try {
|
||||
$updateProc = Start-Process -FilePath $mainExePath -ArgumentList '/update' -PassThru -NoNewWindow
|
||||
Wait-ProcessWithTimeout -Process $updateProc -TimeoutSeconds 300 | Out-Null
|
||||
}
|
||||
catch {
|
||||
Write-Host " [WARNING] Update step failed: $_ — continuing with existing definitions" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Randomize EXE (TDSSKiller)
|
||||
# ------------------------------------------------------------------
|
||||
$randomizedExePath = $null
|
||||
$launchExe = $mainExePath
|
||||
|
||||
if ($s.randomize_exe -eq $true) {
|
||||
$tempName = [System.IO.Path]::GetRandomFileName() -replace '\..*$', ''
|
||||
$randomizedExePath = Join-Path $env:TEMP "$tempName.exe"
|
||||
Copy-Item -Path $mainExePath -Destination $randomizedExePath -Force
|
||||
$launchExe = $randomizedExePath
|
||||
Write-Host " [INFO] TDSSKiller randomized to: $randomizedExePath" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Launch
|
||||
# ------------------------------------------------------------------
|
||||
Write-Host " [....] Launching $($s.name) in clean mode..." -ForegroundColor Cyan
|
||||
$scanStart = Get-Date
|
||||
$proc = $null
|
||||
$status = 'completed'
|
||||
$exitCode = $null
|
||||
|
||||
try {
|
||||
$startParams = @{
|
||||
FilePath = $launchExe
|
||||
PassThru = $true
|
||||
NoNewWindow = $true
|
||||
}
|
||||
if ($expandedArgs -and $expandedArgs.Count -gt 0) {
|
||||
$startParams['ArgumentList'] = $expandedArgs
|
||||
}
|
||||
|
||||
$proc = Start-Process @startParams
|
||||
|
||||
$completed = Wait-ProcessWithTimeout -Process $proc -TimeoutSeconds $timeoutSec
|
||||
if (-not $completed) {
|
||||
$status = 'TIMED OUT'
|
||||
Write-Host " [WARNING] $($s.name) timed out after $($s.timeout_min) min" -ForegroundColor Yellow
|
||||
} else {
|
||||
$exitCode = $proc.ExitCode
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$status = 'FAILED'
|
||||
Write-Host " [ERROR] $($s.name) failed: $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Wait for services
|
||||
# ------------------------------------------------------------------
|
||||
if ($s.service_names -and $s.service_names.Count -gt 0) {
|
||||
Wait-ServicesToStop -ServiceNames $s.service_names -TimeoutSeconds 180
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Duration
|
||||
# ------------------------------------------------------------------
|
||||
$durMin = [math]::Round(((Get-Date) - $scanStart).TotalMinutes, 2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Collect logs
|
||||
# ------------------------------------------------------------------
|
||||
$logDestDir = Join-Path $LogRoot "$($s.name)_Remediation_Logs"
|
||||
|
||||
if ($s.log_src) {
|
||||
$expandedLogSrc = Expand-TokenizedArgs -Args @($s.log_src) -LogRoot $LogRoot -ExeDir $exeDir
|
||||
$expandedLogSrc = Expand-EnvPath $expandedLogSrc[0]
|
||||
if (Test-Path $expandedLogSrc) {
|
||||
New-Item -ItemType Directory -Path $logDestDir -Force | Out-Null
|
||||
Copy-Item -Path $expandedLogSrc -Destination $logDestDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Post-clean
|
||||
# ------------------------------------------------------------------
|
||||
foreach ($p in $s.post_clean_paths) {
|
||||
Remove-PathSilent $p
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Randomized EXE cleanup
|
||||
# ------------------------------------------------------------------
|
||||
if ($randomizedExePath -and (Test-Path $randomizedExePath)) {
|
||||
Remove-Item $randomizedExePath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Threats
|
||||
# ------------------------------------------------------------------
|
||||
$threatsFound = 0
|
||||
if ($null -ne $exitCode) {
|
||||
$threatsFound = Get-ExitCodeThreats -ScannerName $s.name -ExitCode $exitCode
|
||||
}
|
||||
$totalThreats += $threatsFound
|
||||
|
||||
$color = if ($status -eq 'completed') { 'Green' } elseif ($status -eq 'TIMED OUT') { 'Yellow' } else { 'Red' }
|
||||
Write-Host " [$status] ExitCode=$exitCode Threats=$threatsFound Duration=${durMin}min" -ForegroundColor $color
|
||||
|
||||
$results.Add([pscustomobject]@{
|
||||
Scanner = $s.name
|
||||
Status = $status
|
||||
ExitCode = $exitCode
|
||||
ThreatsFound = $threatsFound
|
||||
Duration = "$durMin min"
|
||||
LogPath = $logDestDir
|
||||
})
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Write remediation-results.json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$completedAt = Get-Date
|
||||
|
||||
$scannerResults = foreach ($r in $results) {
|
||||
$durValue = 0
|
||||
if ($r.Duration -match '^([\d.]+)') {
|
||||
$durValue = [double]$Matches[1]
|
||||
}
|
||||
[ordered]@{
|
||||
name = $r.Scanner
|
||||
status = $r.Status
|
||||
exit_code = $r.ExitCode
|
||||
threats_found = $r.ThreatsFound
|
||||
duration_min = $durValue
|
||||
log_path = $r.LogPath
|
||||
}
|
||||
}
|
||||
|
||||
$remObj = [ordered]@{
|
||||
scan_id = $priorData.scan_id
|
||||
machine = $env:COMPUTERNAME
|
||||
started_at = $startedAt.ToUniversalTime().ToString('o')
|
||||
completed_at = $completedAt.ToUniversalTime().ToString('o')
|
||||
total_threats = $totalThreats
|
||||
reboot_required = $false
|
||||
scan_mode = 'clean'
|
||||
scanners = @($scannerResults)
|
||||
}
|
||||
|
||||
$remResultsPath = Join-Path $LogRoot 'remediation-results.json'
|
||||
$remObj | ConvertTo-Json -Depth 10 | Set-Content -Path $remResultsPath -Encoding UTF8
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Write-Host " REMEDIATION COMPLETE" -ForegroundColor Cyan
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
$results | Format-Table -AutoSize
|
||||
Write-Host ""
|
||||
|
||||
if ($totalThreats -gt 0) {
|
||||
Write-Host " [WARNING] $totalThreats threat indicator(s) still detected after clean pass." -ForegroundColor Yellow
|
||||
Write-Host " Review logs carefully — some threats may require a reboot before" -ForegroundColor Yellow
|
||||
Write-Host " they can be fully removed. Reboot and re-scan." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " [OK] Clean pass complete — no threats detected in output." -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Remediation results: $remResultsPath" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
exit 0
|
||||
Import-Module $moduleManifest -Force
|
||||
Invoke-Remediation @PSBoundParameters
|
||||
|
||||
169
projects/msp-tools/guru-scan/README.md
Normal file
169
projects/msp-tools/guru-scan/README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# GuruScan
|
||||
|
||||
Multi-engine malware scan orchestrator for GuruRMM. Runs a sequenced chain of
|
||||
portable security scanners, captures all logs, and writes structured JSON results
|
||||
for downstream processing by the RMM agent.
|
||||
|
||||
---
|
||||
|
||||
## Deploy Layout
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `C:\GuruScan\` | Base directory — module files, whitelist, state files |
|
||||
| `C:\GuruScan\downloads\` | Scanner EXEs (populated by `Download-Scanners.ps1`) |
|
||||
| `C:\GuruScan\reports\` | Per-scan zip archives |
|
||||
| `C:\ScanLogs\` | Per-scan log folders (`<HOSTNAME>-<YYYYMMDD-HHmmss>\`) |
|
||||
|
||||
The module files (`GuruScan.psm1`, `GuruScan.psd1`, `scanners.json`) live in the
|
||||
same directory as the launcher scripts (`Invoke-GuruScan.ps1`, etc.). This is
|
||||
typically `C:\GuruScan\` or the RMM deployment path — wherever the scripts are placed.
|
||||
|
||||
---
|
||||
|
||||
## Scanner Chain
|
||||
|
||||
Scanners run in this order. Each stage hands off to the next regardless of findings.
|
||||
|
||||
| # | Scanner | Category | Notes |
|
||||
|---|---------|----------|-------|
|
||||
| 1 | **RKill** | process-killer | Kills malware-related processes before scanners run. Exit 1 = processes were killed (not a threat indicator). |
|
||||
| 2 | **AdwCleaner** | adware | Removes adware, PUPs, and browser hijackers. |
|
||||
| 3 | **Emsisoft Command Line Scanner** | antimalware | Two-step: NSIS installer extracts to `C:\EmsisoftCmd\`, then `/update` fetches latest definitions, then scans. |
|
||||
| 4 | **HitmanPro** | antimalware | Cloud-assisted second-opinion scanner. Trial registry is reset before each run via `Invoke-HitmanProTrialReset`. |
|
||||
| 5 | **ESET Online Scanner** | antimalware | Skipped automatically when running as SYSTEM (requires interactive desktop). |
|
||||
|
||||
MSERT (Microsoft Safety Scanner) is excluded from the default chain — it is too slow
|
||||
for routine remediation runs. Add it back to `scanners.json` if needed.
|
||||
|
||||
---
|
||||
|
||||
## Exit Code Semantics
|
||||
|
||||
| Scanner | Exit 0 | Exit 1 | Exit 2 | Exit 3 | Other |
|
||||
|---------|--------|--------|--------|--------|-------|
|
||||
| RKill | Clean run | Processes killed (not a threat) | — | — | — |
|
||||
| AdwCleaner | Clean | Cleaned, no reboot needed | Cleaned, reboot required | Found but not cleaned (scan-only) | — |
|
||||
| Emsisoft | Clean | Threats found/cleaned | Cleaned, reboot required | — | — |
|
||||
| HitmanPro | Clean | Cleaned | Cleaned, reboot required | — | — |
|
||||
| ESET | Clean | Threats found | Incomplete removal, reboot may help | — | — |
|
||||
| MSERT | Clean | Threats found/cleaned | — | — | Non-zero = threats |
|
||||
| TDSSKiller | Clean | Threats found | — | — | — |
|
||||
| Stinger | Clean | — | — | — | 13 = threats |
|
||||
|
||||
Reboot-required exit codes: AdwCleaner 2, HitmanPro 2, Emsisoft 2, ESET 2.
|
||||
|
||||
---
|
||||
|
||||
## Autologon / Cleanup Lifecycle
|
||||
|
||||
When any scanner exits with a reboot-required code (exit 2), the following sequence runs:
|
||||
|
||||
1. `Invoke-RebootCleanupSetup` writes `cleanup-state.json` with the original user, scan ID, and log path.
|
||||
2. A hidden `GuruRMM-Temp` administrator account is created with a random password.
|
||||
3. One-time autologon (`AutoLogonCount=1`) is configured for `GuruRMM-Temp`. Windows clears the password after the first use.
|
||||
4. The account is hidden from the login screen via the `SpecialAccounts\UserList` registry key.
|
||||
5. A logon-triggered scheduled task (`GuruRMM-PostRebootCleanup`) is registered for `GuruRMM-Temp`.
|
||||
6. The machine reboots after a 15-second warning.
|
||||
7. On next boot, Windows auto-logs in as `GuruRMM-Temp`. The WPF splash appears immediately (full-screen, black, cursor hidden).
|
||||
8. `Invoke-PostRebootCleanup` runs: verifies pending operations cleared, removes scanner files, writes `logs-ready.json`, restores the original user's login name, clears autologon, removes the cleanup task.
|
||||
9. A SYSTEM scheduled task (`GuruRMM-TempUserDelete`) is registered to delete the `GuruRMM-Temp` account 2 minutes later (cannot delete your own account while logged in).
|
||||
10. The splash closes, `logoff` is called, and the machine returns to the normal login screen.
|
||||
|
||||
---
|
||||
|
||||
## Headless / SYSTEM Behavior
|
||||
|
||||
- `-Headless` passes `NoNewWindow` to all scanner launches, suppressing UI windows.
|
||||
Use this when dispatching from an RMM agent that has no interactive desktop.
|
||||
- ESET is automatically skipped when the script detects it is running as the SYSTEM
|
||||
account (`[System.Security.Principal.WindowsIdentity]::GetCurrent().IsSystem`).
|
||||
Pass `-SkipEset` explicitly to skip it under other accounts.
|
||||
|
||||
---
|
||||
|
||||
## Licensing
|
||||
|
||||
| Scanner | License for MSP use |
|
||||
|---------|---------------------|
|
||||
| RKill | Free (BleepingComputer) |
|
||||
| AdwCleaner | Free for personal and commercial use |
|
||||
| Emsisoft Command Line Scanner | Free for personal and MSP remediation use |
|
||||
| HitmanPro | Commercial license required. Each scan uses trial mode; `Invoke-HitmanProTrialReset` resets the trial window. Verify current licensing terms at https://www.hitmanpro.com before deploying at scale. |
|
||||
| ESET Online Scanner | Free for personal and commercial use |
|
||||
|
||||
Always verify current licensing terms with each vendor before large-scale deployment.
|
||||
|
||||
---
|
||||
|
||||
## Stand-alone Usage
|
||||
|
||||
```powershell
|
||||
# Run all scanners in clean mode (default)
|
||||
.\Invoke-GuruScan.ps1
|
||||
|
||||
# Detect only, then auto-remediate if threats found
|
||||
.\Invoke-GuruScan.ps1 -ScanOnly -AutoRemediate
|
||||
|
||||
# Skip ESET (e.g. unattended run)
|
||||
.\Invoke-GuruScan.ps1 -SkipEset
|
||||
|
||||
# Suppress scanner windows (RMM dispatch)
|
||||
.\Invoke-GuruScan.ps1 -Headless
|
||||
|
||||
# View the latest scan results
|
||||
.\Get-ScanSummary.ps1
|
||||
|
||||
# View with AI analysis (requires Ollama)
|
||||
.\Get-ScanSummary.ps1 -AI
|
||||
|
||||
# Re-run clean pass against a prior scan
|
||||
.\Invoke-Remediation.ps1 -LogRoot "C:\ScanLogs\DESKTOP-20260523-143000"
|
||||
|
||||
# Download/refresh scanner EXEs
|
||||
.\Download-Scanners.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Usage
|
||||
|
||||
The launcher scripts are thin wrappers. Import the module directly for
|
||||
scripted/pipeline use:
|
||||
|
||||
```powershell
|
||||
Import-Module .\GuruScan.psd1
|
||||
|
||||
# Disk sink (default) — writes results.json + CSV + zip
|
||||
Invoke-GuruScan
|
||||
|
||||
# RMM sink — returns result object to the pipeline, no disk writes
|
||||
$result = Invoke-GuruScan -OutputSink RMM -Headless
|
||||
if ($result.total_threats -gt 0) { ... }
|
||||
|
||||
# Remediation from a prior scan
|
||||
Invoke-Remediation -LogRoot "C:\ScanLogs\DESKTOP-20260523-143000"
|
||||
|
||||
# Summary report
|
||||
Get-ScanSummary -AI
|
||||
|
||||
# Post-reboot cleanup (called by Invoke-PostRebootCleanup.ps1)
|
||||
Invoke-PostRebootCleanup -StateFile "C:\GuruScan\cleanup-state.json"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
guru-scan\
|
||||
GuruScan.psm1 # Core module — all helpers + exported cmdlets
|
||||
GuruScan.psd1 # Module manifest
|
||||
scanners.json # Scanner definitions (single source of truth)
|
||||
Invoke-GuruScan.ps1 # Thin launcher -> Invoke-GuruScan
|
||||
Invoke-Remediation.ps1 # Thin launcher -> Invoke-Remediation
|
||||
Get-ScanSummary.ps1 # Thin launcher -> Get-ScanSummary
|
||||
Invoke-PostRebootCleanup.ps1 # WPF splash + logoff; delegates cleanup to module
|
||||
Download-Scanners.ps1 # Downloads scanner EXEs from scanners.json URLs
|
||||
downloads\ # Scanner EXEs (gitignored)
|
||||
```
|
||||
143
projects/msp-tools/guru-scan/scanners.json
Normal file
143
projects/msp-tools/guru-scan/scanners.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"scanners": [
|
||||
{
|
||||
"name": "RKill",
|
||||
"category": "process-killer",
|
||||
"exe": "C:\\GuruScan\\downloads\\rkill.exe",
|
||||
"installer_exe": null,
|
||||
"installer_args": null,
|
||||
"run_update_after_install": false,
|
||||
"download_url": "https://download.bleepingcomputer.com/grinler/rkill.exe",
|
||||
"manual_download": false,
|
||||
"manual_download_note": null,
|
||||
"scan_args": ["-s", "-l \"{LOG_ROOT}\\rkill.log\""],
|
||||
"clean_args": ["-s", "-l \"{LOG_ROOT}\\rkill.log\""],
|
||||
"log_src": "{LOG_ROOT}\\rkill.log",
|
||||
"timeout_min": 10,
|
||||
"randomize_exe": false,
|
||||
"pre_close_processes": [],
|
||||
"pre_clean_paths": [],
|
||||
"post_clean_paths": [],
|
||||
"service_names": [],
|
||||
"hitmanpro_trial_reset": false,
|
||||
"whitelist_arg": null,
|
||||
"wait_on_process": null
|
||||
},
|
||||
{
|
||||
"name": "AdwCleaner",
|
||||
"category": "adware",
|
||||
"exe": "C:\\GuruScan\\downloads\\adwcleaner.exe",
|
||||
"installer_exe": null,
|
||||
"installer_args": null,
|
||||
"run_update_after_install": false,
|
||||
"download_url": "https://adwcleaner.malwarebytes.com/adwcleaner?channel=release",
|
||||
"manual_download": true,
|
||||
"manual_download_note": "Malwarebytes blocks automated downloads; download manually from https://www.malwarebytes.com/adwcleaner",
|
||||
"scan_args": ["/eula", "/scan", "/noreboot"],
|
||||
"clean_args": ["/eula", "/clean", "/noreboot"],
|
||||
"log_src": "C:\\AdwCleaner\\Logs",
|
||||
"timeout_min": 60,
|
||||
"randomize_exe": false,
|
||||
"pre_close_processes": [],
|
||||
"pre_clean_paths": ["C:\\AdwCleaner"],
|
||||
"post_clean_paths": ["C:\\AdwCleaner"],
|
||||
"service_names": ["AdwCleanerSvc"],
|
||||
"hitmanpro_trial_reset": false,
|
||||
"whitelist_arg": null,
|
||||
"wait_on_process": "AdwCleaner"
|
||||
},
|
||||
{
|
||||
"name": "Emsisoft",
|
||||
"category": "antimalware",
|
||||
"exe": "C:\\EmsisoftCmd\\a2cmd.exe",
|
||||
"installer_exe": "C:\\GuruScan\\downloads\\EmsisoftCommandlineScanner64.exe",
|
||||
"installer_args": ["/S"],
|
||||
"run_update_after_install": true,
|
||||
"download_url": "https://dl.emsisoft.com/EmsisoftCommandlineScanner64.exe",
|
||||
"manual_download": false,
|
||||
"manual_download_note": null,
|
||||
"scan_args": [
|
||||
"/f=C:\\",
|
||||
"/deep",
|
||||
"/rk",
|
||||
"/m",
|
||||
"/t",
|
||||
"/pup",
|
||||
"/a",
|
||||
"/n",
|
||||
"/ac",
|
||||
"/d",
|
||||
"/wl=\"C:\\GuruScan\\whitelist.txt\"",
|
||||
"/la=\"{LOG_ROOT}\\a2cmd_deep_log.txt\""
|
||||
],
|
||||
"clean_args": [
|
||||
"/f=C:\\",
|
||||
"/deep",
|
||||
"/rk",
|
||||
"/m",
|
||||
"/t",
|
||||
"/c",
|
||||
"/pup",
|
||||
"/a",
|
||||
"/n",
|
||||
"/ac",
|
||||
"/d",
|
||||
"/wl=\"C:\\GuruScan\\whitelist.txt\"",
|
||||
"/la=\"{LOG_ROOT}\\a2cmd_deep_log.txt\""
|
||||
],
|
||||
"log_src": null,
|
||||
"timeout_min": 120,
|
||||
"randomize_exe": false,
|
||||
"pre_close_processes": [],
|
||||
"pre_clean_paths": ["C:\\EmsisoftCmd"],
|
||||
"post_clean_paths": ["C:\\EmsisoftCmd"],
|
||||
"service_names": [],
|
||||
"hitmanpro_trial_reset": false,
|
||||
"whitelist_arg": "emsisoft",
|
||||
"wait_on_process": "a2cmd"
|
||||
},
|
||||
{
|
||||
"name": "HitmanPro",
|
||||
"category": "antimalware",
|
||||
"exe": "C:\\GuruScan\\downloads\\HitmanPro_x64.exe",
|
||||
"installer_exe": null,
|
||||
"installer_args": null,
|
||||
"run_update_after_install": false,
|
||||
"download_url": null,
|
||||
"manual_download": true,
|
||||
"manual_download_note": "Requires a trial/license — download from https://www.hitmanpro.com/en-us/hmp.aspx",
|
||||
"scan_args": [
|
||||
"/noinstall",
|
||||
"/scan",
|
||||
"/log=\"{LOG_ROOT}\\HitmanPro_Scan_Log.txt\"",
|
||||
"/excludelist=\"C:\\GuruScan\\whitelist.txt\""
|
||||
],
|
||||
"clean_args": [
|
||||
"/noinstall",
|
||||
"/clean",
|
||||
"/log=\"{LOG_ROOT}\\HitmanPro_Scan_Log.txt\"",
|
||||
"/excludelist=\"C:\\GuruScan\\whitelist.txt\""
|
||||
],
|
||||
"log_src": null,
|
||||
"timeout_min": 60,
|
||||
"randomize_exe": false,
|
||||
"pre_close_processes": ["chrome", "firefox", "msedge", "brave", "opera", "iexplore", "operagx", "MicrosoftEdge"],
|
||||
"pre_clean_paths": [
|
||||
"C:\\ProgramData\\HitmanPro",
|
||||
"C:\\ProgramData\\HitmanPro.Alert",
|
||||
"%LOCALAPPDATA%\\HitmanPro",
|
||||
"%LOCALAPPDATA%\\HitmanPro.Alert"
|
||||
],
|
||||
"post_clean_paths": [
|
||||
"C:\\ProgramData\\HitmanPro",
|
||||
"C:\\ProgramData\\HitmanPro.Alert",
|
||||
"%LOCALAPPDATA%\\HitmanPro",
|
||||
"%LOCALAPPDATA%\\HitmanPro.Alert"
|
||||
],
|
||||
"service_names": [],
|
||||
"hitmanpro_trial_reset": true,
|
||||
"whitelist_arg": "hitmanpro",
|
||||
"wait_on_process": "HitmanPro_x64"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user