1098 lines
46 KiB
PowerShell
1098 lines
46 KiB
PowerShell
#Requires -RunAsAdministrator
|
|
<#
|
|
.SYNOPSIS
|
|
GuruScan - multi-engine malware scanning orchestrator (single-file, RMM-ready).
|
|
.DESCRIPTION
|
|
Runs a suite of portable malware scanners in sequence, captures logs,
|
|
and writes a structured results.json plus a zip archive of all logs.
|
|
All scanner definitions and the whitelist are embedded inline - no external
|
|
files required beyond the scanner EXEs themselves in C:\GuruScan\downloads\.
|
|
|
|
By default runs all scanners in clean (remediation) mode.
|
|
Use -ScanOnly to detect without cleaning.
|
|
|
|
NOTE: MSERT is no longer included in the default scanner list because it
|
|
takes too long for routine runs. To run MSERT, invoke it directly or pass
|
|
-Scanners MSERT explicitly if you add it back to the definitions.
|
|
.PARAMETER ScanOnly
|
|
Use scan args (detect only) instead of clean args for every scanner.
|
|
.PARAMETER AutoRemediate
|
|
After a scan-only pass, if threats are found, automatically re-run all
|
|
scanners in clean mode.
|
|
.PARAMETER Scanners
|
|
Run only the named scanners (comma-separated or multiple values).
|
|
Names must match the Name field in $ScannerDefs exactly.
|
|
.PARAMETER TimeoutMin
|
|
Override the per-scanner timeout (in minutes) for all scanners.
|
|
.PARAMETER SkipEset
|
|
Skip the ESET scanner. ESET requires user interaction and is automatically
|
|
skipped when running as SYSTEM. Use this flag to skip it explicitly.
|
|
.PARAMETER SkipScanners
|
|
Skip one or more named scanners by name. Names must match the Name field
|
|
in $ScannerDefs exactly. Useful for excluding a single scanner without
|
|
respecifying the entire list.
|
|
.EXAMPLE
|
|
.\Invoke-GuruScan.ps1
|
|
.\Invoke-GuruScan.ps1 -ScanOnly -AutoRemediate
|
|
.\Invoke-GuruScan.ps1 -SkipEset
|
|
.\Invoke-GuruScan.ps1 -SkipScanners Emsisoft
|
|
.\Invoke-GuruScan.ps1 -Headless # suppress scanner windows (used when dispatching via RMM)
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$ScanOnly,
|
|
[switch]$AutoRemediate,
|
|
[string[]]$Scanners,
|
|
[int]$TimeoutMin = 0,
|
|
[switch]$SkipEset,
|
|
[string[]]$SkipScanners = @(),
|
|
[switch]$Headless
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Continue'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Base path - hardcoded so the script works inline or from any location
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$Base = 'C:\GuruScan'
|
|
$LogRoot = "C:\ScanLogs"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Whitelist - written to C:\GuruScan\whitelist.txt before any scanner runs.
|
|
#
|
|
# WHITELIST SUPPORT BY SCANNER:
|
|
# Emsisoft YES /wl= flag -- paths and folders excluded from scan
|
|
# HitmanPro YES /excludelist= -- paths and folders excluded from scan
|
|
# RKill NO no CLI support -- kills by process pattern, no exclusions
|
|
# AdwCleaner NO no CLI support -- GUI-only exclusions, not scriptable
|
|
# ESET NO no CLI support -- no exclusion flag in silent mode
|
|
#
|
|
# Add one entry per line. Paths, folders, or file names.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$Whitelist = @(
|
|
'C:\GuruScan'
|
|
# 'C:\TestWhitelist' # uncomment to test whitelist (place EICAR file here)
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ForceRemove blacklist - items here are ALWAYS removed after all scanners run,
|
|
# regardless of what the scanners detected. The free scanners do not support a
|
|
# user-defined removal list, so this PowerShell stage handles it instead.
|
|
#
|
|
# Supported entry types (set Type accordingly):
|
|
# Path - file or folder (Remove-Item -Recurse -Force)
|
|
# Registry - registry key (Remove-Item -Recurse -Force on the key path)
|
|
# RegValue - registry value (Remove-ItemProperty)
|
|
# Service - Windows service (Stop + delete)
|
|
# Task - scheduled task (Unregister-ScheduledTask)
|
|
# Process - kill by name (Stop-Process -Force)
|
|
#
|
|
# Example entries (uncomment or add your own):
|
|
# @{ Type='Path'; Value='C:\ProgramData\BadTool' }
|
|
# @{ Type='Registry'; Value='HKLM:\SOFTWARE\BadTool' }
|
|
# @{ Type='RegValue'; Key='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'; Value='BadTool' }
|
|
# @{ Type='Service'; Value='BadToolSvc' }
|
|
# @{ Type='Task'; Value='BadToolUpdate' }
|
|
# @{ Type='Process'; Value='badtool' }
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$ForceRemove = @(
|
|
# Add entries here to force-remove specific threats after scanning
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scanner definitions - embedded inline, no scanners.json dependency
|
|
# Args are already fully formed strings; {LOG_ROOT} tokens are expanded at
|
|
# runtime by Expand-TokenizedArgs.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$ScannerDefs = @(
|
|
@{
|
|
Name = 'RKill'
|
|
Category = 'process-killer'
|
|
Exe = "$Base\downloads\rkill.exe"
|
|
InstallerExe = $null
|
|
ScanArgs = @('-s', "-l `"{LOG_ROOT}\rkill.log`"")
|
|
CleanArgs = @('-s', "-l `"{LOG_ROOT}\rkill.log`"")
|
|
LogSrc = '{LOG_ROOT}\rkill.log'
|
|
TimeoutMin = 10
|
|
RandomizeExe = $false
|
|
PreCloseProcesses = @()
|
|
PreCleanPaths = @()
|
|
PostCleanPaths = @()
|
|
ServiceNames = @()
|
|
HitmanProReset = $false
|
|
WhitelistArg = $null
|
|
WaitOnProcess = $null
|
|
},
|
|
@{
|
|
Name = 'AdwCleaner'
|
|
Category = 'adware'
|
|
Exe = "$Base\downloads\adwcleaner.exe"
|
|
InstallerExe = $null
|
|
ScanArgs = @('/eula', '/scan', '/noreboot')
|
|
CleanArgs = @('/eula', '/clean', '/noreboot')
|
|
LogSrc = 'C:\AdwCleaner\Logs'
|
|
TimeoutMin = 60
|
|
RandomizeExe = $false
|
|
PreCloseProcesses = @()
|
|
PreCleanPaths = @('C:\AdwCleaner')
|
|
PostCleanPaths = @('C:\AdwCleaner')
|
|
ServiceNames = @('AdwCleanerSvc')
|
|
HitmanProReset = $false
|
|
WhitelistArg = $null
|
|
WaitOnProcess = 'AdwCleaner'
|
|
},
|
|
@{
|
|
Name = 'Emsisoft'
|
|
Category = 'antimalware'
|
|
Exe = 'C:\EmsisoftCmd\a2cmd.exe'
|
|
InstallerExe = "$Base\downloads\EmsisoftCommandlineScanner64.exe"
|
|
ScanArgs = @('/f=C:\', '/deep', '/rk', '/m', '/t', '/pup', '/a', '/n', '/ac', '/d', "/wl=`"$Base\whitelist.txt`"", "/la=`"{LOG_ROOT}\a2cmd_deep_log.txt`"")
|
|
CleanArgs = @('/f=C:\', '/deep', '/rk', '/m', '/t', '/c', '/pup', '/a', '/n', '/ac', '/d', "/wl=`"$Base\whitelist.txt`"", "/la=`"{LOG_ROOT}\a2cmd_deep_log.txt`"")
|
|
LogSrc = $null
|
|
TimeoutMin = 120
|
|
RandomizeExe = $false
|
|
PreCloseProcesses = @()
|
|
PreCleanPaths = @('C:\EmsisoftCmd')
|
|
PostCleanPaths = @('C:\EmsisoftCmd')
|
|
ServiceNames = @()
|
|
HitmanProReset = $false
|
|
WhitelistArg = 'emsisoft'
|
|
WaitOnProcess = 'a2cmd'
|
|
},
|
|
@{
|
|
Name = 'HitmanPro'
|
|
Category = 'antimalware'
|
|
Exe = "$Base\downloads\HitmanPro_x64.exe"
|
|
InstallerExe = $null
|
|
ScanArgs = @('/noinstall', '/scan', "/log=`"{LOG_ROOT}\HitmanPro_Scan_Log.txt`"", "/excludelist=`"$Base\whitelist.txt`"")
|
|
CleanArgs = @('/noinstall', '/clean', "/log=`"{LOG_ROOT}\HitmanPro_Scan_Log.txt`"", "/excludelist=`"$Base\whitelist.txt`"")
|
|
LogSrc = $null
|
|
TimeoutMin = 60
|
|
RandomizeExe = $false
|
|
PreCloseProcesses = @('chrome', 'firefox', 'msedge', 'brave', 'opera', 'iexplore', 'operagx', 'MicrosoftEdge')
|
|
PreCleanPaths = @('C:\ProgramData\HitmanPro', 'C:\ProgramData\HitmanPro.Alert', '%LOCALAPPDATA%\HitmanPro', '%LOCALAPPDATA%\HitmanPro.Alert')
|
|
PostCleanPaths = @('C:\ProgramData\HitmanPro', 'C:\ProgramData\HitmanPro.Alert', '%LOCALAPPDATA%\HitmanPro', '%LOCALAPPDATA%\HitmanPro.Alert')
|
|
ServiceNames = @()
|
|
HitmanProReset = $true
|
|
WhitelistArg = 'hitmanpro'
|
|
WaitOnProcess = 'HitmanPro_x64'
|
|
},
|
|
@{
|
|
Name = 'ESET'
|
|
Category = 'antimalware'
|
|
Exe = "$Base\downloads\esetonlinescanner.exe"
|
|
InstallerExe = $null
|
|
ScanArgs = @('/silent', '/unwanted')
|
|
CleanArgs = @('/silent', '/unwanted', '/clean')
|
|
LogSrc = $null
|
|
TimeoutMin = 240
|
|
RandomizeExe = $false
|
|
PreCloseProcesses = @()
|
|
PreCleanPaths = @()
|
|
PostCleanPaths = @()
|
|
ServiceNames = @('ekm', 'epfw', 'epfwwfp', 'EraAgentSvc')
|
|
HitmanProReset = $false
|
|
WhitelistArg = $null
|
|
WaitOnProcess = 'ESETOnlineScanner'
|
|
}
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Expand-TokenizedArgs {
|
|
param(
|
|
[string[]]$ArgList,
|
|
[string]$LogRoot,
|
|
[string]$ExeDir
|
|
)
|
|
$out = foreach ($a in $ArgList) {
|
|
$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 {}
|
|
$Process.WaitForExit(5000) | Out-Null
|
|
return $false
|
|
}
|
|
Start-Sleep -Seconds 5
|
|
}
|
|
# Flush the exit code — required before ExitCode property is readable
|
|
$Process.WaitForExit(5000) | Out-Null
|
|
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 Wait-ScannerCompletion {
|
|
param(
|
|
[System.Diagnostics.Process]$Process,
|
|
[string]$WaitOnProcess,
|
|
[string[]]$ServiceNames,
|
|
[int]$TimeoutSeconds
|
|
)
|
|
# First wait for the main process
|
|
$completed = Wait-ProcessWithTimeout -Process $Process -TimeoutSeconds $TimeoutSeconds
|
|
if (-not $completed) { return $false }
|
|
|
|
# Then wait for a named child process if specified
|
|
if ($WaitOnProcess) {
|
|
$deadline = (Get-Date).AddSeconds(60)
|
|
while ((Get-Date) -lt $deadline) {
|
|
$child = Get-Process -Name $WaitOnProcess -ErrorAction SilentlyContinue
|
|
if (-not $child) { break }
|
|
Start-Sleep -Seconds 3
|
|
}
|
|
}
|
|
|
|
# Then wait for services to stop
|
|
if ($ServiceNames -and $ServiceNames.Count -gt 0) {
|
|
Wait-ServicesToStop -ServiceNames $ServiceNames -TimeoutSeconds 120
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Invoke-RebootCleanupSetup {
|
|
param(
|
|
[string]$OriginalUser,
|
|
[string]$ScanId,
|
|
[string]$LogRoot
|
|
)
|
|
|
|
$tempUser = 'GuruRMM-Temp'
|
|
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
|
$specialKey= 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList'
|
|
|
|
Write-Host ''
|
|
Write-Host '------------------------------------------------------------' -ForegroundColor Yellow
|
|
Write-Host ' Reboot required — preparing cleanup session' -ForegroundColor Yellow
|
|
Write-Host '------------------------------------------------------------' -ForegroundColor Yellow
|
|
|
|
# Write state for the post-reboot script to read
|
|
@{
|
|
original_user = $OriginalUser
|
|
scan_id = $ScanId
|
|
log_root = $LogRoot
|
|
created_at = (Get-Date).ToUniversalTime().ToString('o')
|
|
} | ConvertTo-Json | Set-Content "$Base\cleanup-state.json" -Encoding UTF8
|
|
|
|
# Remove any leftover temp account from a previous run
|
|
Remove-LocalUser -Name $tempUser -ErrorAction SilentlyContinue
|
|
|
|
# Create the temp account (random password — Windows requires one for autologon)
|
|
$tempPass = [System.Guid]::NewGuid().ToString('N').Substring(0,16) + 'Aa1!'
|
|
$secPass = ConvertTo-SecureString $tempPass -AsPlainText -Force
|
|
New-LocalUser -Name $tempUser -Password $secPass -PasswordNeverExpires -UserMayNotChangePassword | Out-Null
|
|
Add-LocalGroupMember -Group 'Administrators' -Member $tempUser -ErrorAction SilentlyContinue
|
|
Write-Host " [OK] Created temp account: $tempUser" -ForegroundColor Green
|
|
|
|
# Hide from login screen
|
|
New-Item -Path $specialKey -Force | Out-Null
|
|
Set-ItemProperty -Path $specialKey -Name $tempUser -Value 0 -Type DWord
|
|
Write-Host " [OK] Account hidden from login screen" -ForegroundColor Green
|
|
|
|
# One-time autologon (AutoLogonCount=1 means Windows wipes the password entry after first use)
|
|
Set-ItemProperty -Path $wlKey -Name 'AutoAdminLogon' -Value '1'
|
|
Set-ItemProperty -Path $wlKey -Name 'DefaultUserName' -Value $tempUser
|
|
Set-ItemProperty -Path $wlKey -Name 'DefaultDomainName'-Value $env:COMPUTERNAME
|
|
Set-ItemProperty -Path $wlKey -Name 'DefaultPassword' -Value $tempPass
|
|
Set-ItemProperty -Path $wlKey -Name 'AutoLogonCount' -Value 1 -Type DWord
|
|
Write-Host " [OK] One-time autologon configured" -ForegroundColor Green
|
|
|
|
# Register cleanup script as logon task for GuruRMM-Temp
|
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
|
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$Base\Invoke-PostRebootCleanup.ps1`""
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $tempUser
|
|
$principal = New-ScheduledTaskPrincipal -UserId $tempUser -RunLevel Highest -LogonType Interactive
|
|
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 30) -MultipleInstances IgnoreNew
|
|
Register-ScheduledTask -TaskName 'GuruRMM-PostRebootCleanup' `
|
|
-Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
|
Write-Host " [OK] Post-reboot cleanup task registered" -ForegroundColor Green
|
|
|
|
Write-Host ''
|
|
Write-Host ' [INFO] Rebooting in 15 seconds. The machine will log in automatically,' -ForegroundColor Yellow
|
|
Write-Host ' complete cleanup, then return to the normal login screen.' -ForegroundColor Yellow
|
|
Write-Host ''
|
|
Start-Sleep -Seconds 15
|
|
Restart-Computer -Force
|
|
}
|
|
|
|
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) {
|
|
# 0=clean 1=cleaned(no reboot) 2=cleaned(reboot needed) 3=found/not cleaned (scan-only)
|
|
'AdwCleaner' { if ($ExitCode -in @(1,2,3)) { return 1 } }
|
|
# 0=clean 1=cleaned 2=cleaned(reboot needed)
|
|
'HitmanPro' { if ($ExitCode -in @(1,2)) { return 1 } }
|
|
# 0=clean 1=threats found/cleaned 2=found but not fully removed (reboot needed)
|
|
'Emsisoft' { if ($ExitCode -ge 1) { return 1 } }
|
|
# 0=clean 1=threats found 2=incomplete removal (reboot may help)
|
|
'ESET' { if ($ExitCode -in @(1,2)) { return 1 } }
|
|
'MSERT' { if ($ExitCode -ne 0) { return 1 } }
|
|
'TDSSKiller' { if ($ExitCode -eq 1) { return 1 } }
|
|
'Stinger' { if ($ExitCode -eq 13) { return 1 } }
|
|
# RKill: exit 1 = processes were killed, not a threat count
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function Get-ExitCodeReboot {
|
|
param([string]$ScannerName, [int]$ExitCode)
|
|
switch ($ScannerName) {
|
|
'AdwCleaner' { if ($ExitCode -eq 2) { return $true } }
|
|
'HitmanPro' { if ($ExitCode -eq 2) { return $true } }
|
|
'Emsisoft' { if ($ExitCode -eq 2) { return $true } }
|
|
'ESET' { if ($ExitCode -eq 2) { return $true } }
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Invoke-ForceRemoveList {
|
|
param([array]$RemoveList)
|
|
if (-not $RemoveList -or $RemoveList.Count -eq 0) { return }
|
|
Write-Host ''
|
|
Write-Host '------------------------------------------------------------' -ForegroundColor DarkGray
|
|
Write-Host ' Force-Remove Blacklist' -ForegroundColor Cyan
|
|
foreach ($entry in $RemoveList) {
|
|
$type = $entry.Type
|
|
$value = $entry.Value
|
|
try {
|
|
switch ($type) {
|
|
'Path' {
|
|
$expanded = [System.Environment]::ExpandEnvironmentVariables($value)
|
|
if (Test-Path $expanded) {
|
|
Remove-Item -Path $expanded -Recurse -Force -ErrorAction Stop
|
|
Write-Host " [OK] Removed path: $expanded" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [SKIP] Path not found: $expanded" -ForegroundColor Gray
|
|
}
|
|
}
|
|
'Registry' {
|
|
if (Test-Path $value) {
|
|
Remove-Item -Path $value -Recurse -Force -ErrorAction Stop
|
|
Write-Host " [OK] Removed registry key: $value" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [SKIP] Registry key not found: $value" -ForegroundColor Gray
|
|
}
|
|
}
|
|
'RegValue' {
|
|
if (Test-Path $entry.Key) {
|
|
Remove-ItemProperty -Path $entry.Key -Name $value -Force -ErrorAction Stop
|
|
Write-Host " [OK] Removed registry value: $($entry.Key)\$value" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [SKIP] Registry key not found: $($entry.Key)" -ForegroundColor Gray
|
|
}
|
|
}
|
|
'Service' {
|
|
$svc = Get-Service -Name $value -ErrorAction SilentlyContinue
|
|
if ($svc) {
|
|
Stop-Service -Name $value -Force -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 2
|
|
& sc.exe delete $value | Out-Null
|
|
Write-Host " [OK] Removed service: $value" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [SKIP] Service not found: $value" -ForegroundColor Gray
|
|
}
|
|
}
|
|
'Task' {
|
|
$task = Get-ScheduledTask -TaskName $value -ErrorAction SilentlyContinue
|
|
if ($task) {
|
|
Unregister-ScheduledTask -TaskName $value -Confirm:$false -ErrorAction Stop
|
|
Write-Host " [OK] Removed scheduled task: $value" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [SKIP] Scheduled task not found: $value" -ForegroundColor Gray
|
|
}
|
|
}
|
|
'Process' {
|
|
$procs = Get-Process -Name $value -ErrorAction SilentlyContinue
|
|
if ($procs) {
|
|
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
Write-Host " [OK] Killed process: $value ($($procs.Count) instance(s))" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [SKIP] Process not running: $value" -ForegroundColor Gray
|
|
}
|
|
}
|
|
default {
|
|
Write-Host " [WARNING] Unknown ForceRemove type '$type' for: $value" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Host " [ERROR] ForceRemove failed ($type): $value - $_" -ForegroundColor Red
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-RunningAsSystem {
|
|
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
return ($id.IsSystem)
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Initialization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$ScanStamp = "$env:COMPUTERNAME-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
|
$RunLogRoot = "C:\ScanLogs\$ScanStamp"
|
|
$ScanMode = if ($ScanOnly) { 'scan' } else { 'clean' }
|
|
|
|
# Auto-skip ESET when running as SYSTEM (no interactive desktop)
|
|
$RunningAsSystem = Test-RunningAsSystem
|
|
if ($RunningAsSystem -and -not $SkipEset) {
|
|
Write-Host '[INFO] Running as SYSTEM - ESET will be skipped (requires interactive desktop).' -ForegroundColor Cyan
|
|
$SkipEset = $true
|
|
}
|
|
|
|
# Create required directories
|
|
New-Item -ItemType Directory -Path $Base -Force | Out-Null
|
|
New-Item -ItemType Directory -Path "$Base\downloads" -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $RunLogRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path "$Base\reports" -Force | Out-Null
|
|
|
|
# Write whitelist file
|
|
$Whitelist | Set-Content -Path "$Base\whitelist.txt" -Encoding UTF8
|
|
Write-Host "[INFO] Whitelist written to $Base\whitelist.txt ($($Whitelist.Count) entries)" -ForegroundColor Cyan
|
|
|
|
Write-Host ''
|
|
Write-Host '=== GuruScan ===' -ForegroundColor Cyan
|
|
Write-Host " Machine : $env:COMPUTERNAME"
|
|
Write-Host " Scan ID : $ScanStamp"
|
|
Write-Host " Log root : $RunLogRoot"
|
|
Write-Host " Mode : $ScanMode"
|
|
Write-Host " As SYSTEM : $RunningAsSystem"
|
|
Write-Host ''
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Filter scanners
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$scannerList = $ScannerDefs
|
|
|
|
if ($SkipEset) {
|
|
$scannerList = $scannerList | Where-Object { $_.Name -ne 'ESET' }
|
|
}
|
|
|
|
if ($SkipScanners -and $SkipScanners.Count -gt 0) {
|
|
$scannerList = $scannerList | Where-Object { $SkipScanners -notcontains $_.Name }
|
|
}
|
|
|
|
if ($Scanners -and $Scanners.Count -gt 0) {
|
|
$scannerList = $scannerList | Where-Object { $Scanners -contains $_.Name }
|
|
if (-not $scannerList -or @($scannerList).Count -eq 0) {
|
|
Write-Host "[ERROR] No scanners matched the provided names: $($Scanners -join ', ')" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run scanners
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$results = [System.Collections.Generic.List[pscustomobject]]::new()
|
|
$startedAt = Get-Date
|
|
$totalThreats = 0
|
|
$rebootRequired = $false
|
|
|
|
foreach ($s in $scannerList) {
|
|
Write-Host '------------------------------------------------------------' -ForegroundColor DarkGray
|
|
Write-Host " Scanner : $($s.Name)" -ForegroundColor Cyan
|
|
Write-Host " Mode : $ScanMode"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Resolve EXE paths
|
|
# ------------------------------------------------------------------
|
|
$mainExePath = $s.Exe
|
|
$installerExePath = $s.InstallerExe
|
|
|
|
# For two-step scanners the main EXE won't exist until after install
|
|
$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 - expand {LOG_ROOT} tokens
|
|
# ------------------------------------------------------------------
|
|
$exeDir = Split-Path $mainExePath -Parent
|
|
|
|
if ($ScanOnly) {
|
|
$rawArgs = @($s.ScanArgs)
|
|
} else {
|
|
$rawArgs = @($s.CleanArgs)
|
|
}
|
|
|
|
$expandedArgs = Expand-TokenizedArgs -ArgList $rawArgs -LogRoot $RunLogRoot -ExeDir $exeDir
|
|
|
|
# ------------------------------------------------------------------
|
|
# HitmanPro trial registry pre-seed
|
|
# ------------------------------------------------------------------
|
|
if ($s.HitmanProReset -eq $true) {
|
|
Write-Host ' [INFO] Resetting HitmanPro trial registry...' -ForegroundColor Cyan
|
|
Invoke-HitmanProTrialReset
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Pre-clean paths
|
|
# ------------------------------------------------------------------
|
|
foreach ($p in $s.PreCleanPaths) {
|
|
Remove-PathSilent $p
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Determine effective timeout
|
|
# ------------------------------------------------------------------
|
|
$effectiveTimeoutMin = if ($TimeoutMin -gt 0) { $TimeoutMin } else { $s.TimeoutMin }
|
|
$timeoutSec = $effectiveTimeoutMin * 60
|
|
|
|
# ------------------------------------------------------------------
|
|
# Two-step install: Emsisoft pattern (installer + /update + scan)
|
|
# ------------------------------------------------------------------
|
|
if ($installerExePath) {
|
|
Write-Host " [INFO] Running installer: $installerExePath" -ForegroundColor Cyan
|
|
try {
|
|
$instProc = Start-Process -FilePath $installerExePath -ArgumentList '/S' -PassThru -WindowStyle Hidden
|
|
$instCompleted = Wait-ProcessWithTimeout -Process $instProc -TimeoutSeconds 300
|
|
# Close any Explorer folder window the NSIS installer opened (it opens the extract dir)
|
|
try {
|
|
(New-Object -ComObject Shell.Application).Windows() |
|
|
Where-Object { $_.LocationURL -like '*EmsisoftCmd*' -or $_.LocationName -like '*EmsisoftCmd*' } |
|
|
ForEach-Object { $_.Quit() }
|
|
} catch {}
|
|
if (-not $instCompleted) {
|
|
Write-Host ' [WARNING] Installer timed out after 5 min - skipping scanner' -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 not found 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 -WindowStyle Hidden
|
|
Wait-ProcessWithTimeout -Process $updateProc -TimeoutSeconds 300 | Out-Null
|
|
}
|
|
catch {
|
|
Write-Host " [WARNING] Update step failed: $_ - continuing with existing definitions" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Randomize EXE name (TDSSKiller pattern - bypass rootkit self-protection)
|
|
# ------------------------------------------------------------------
|
|
$randomizedExePath = $null
|
|
$launchExe = $mainExePath
|
|
|
|
if ($s.RandomizeExe -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] EXE randomized to: $randomizedExePath" -ForegroundColor Cyan
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Kill processes that would block this scanner (e.g. browsers before HitmanPro)
|
|
# ------------------------------------------------------------------
|
|
if ($s.PreCloseProcesses -and $s.PreCloseProcesses.Count -gt 0) {
|
|
foreach ($proc in $s.PreCloseProcesses) {
|
|
Get-Process -Name $proc -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Launch scanner
|
|
# ------------------------------------------------------------------
|
|
Write-Host " [....] Launching $($s.Name)..." -ForegroundColor Cyan
|
|
$scanStart = Get-Date
|
|
$proc = $null
|
|
$status = 'completed'
|
|
$exitCode = $null
|
|
|
|
try {
|
|
$startParams = @{
|
|
FilePath = $launchExe
|
|
PassThru = $true
|
|
NoNewWindow = [bool]$Headless
|
|
}
|
|
if ($expandedArgs -and $expandedArgs.Count -gt 0) {
|
|
$startParams['ArgumentList'] = $expandedArgs
|
|
}
|
|
|
|
$proc = Start-Process @startParams
|
|
|
|
$completed = Wait-ScannerCompletion -Process $proc -WaitOnProcess $s.WaitOnProcess -ServiceNames $s.ServiceNames -TimeoutSeconds $timeoutSec
|
|
|
|
if (-not $completed) {
|
|
$status = 'TIMED OUT'
|
|
Write-Host " [WARNING] $($s.Name) timed out after $effectiveTimeoutMin min" -ForegroundColor Yellow
|
|
} else {
|
|
$exitCode = $proc.ExitCode
|
|
}
|
|
}
|
|
catch {
|
|
$status = 'FAILED'
|
|
Write-Host " [ERROR] $($s.Name) failed to launch: $_" -ForegroundColor Red
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Calculate duration
|
|
# ------------------------------------------------------------------
|
|
$scanEnd = Get-Date
|
|
$durMin = [math]::Round(($scanEnd - $scanStart).TotalMinutes, 2)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Collect logs from scanner-specific log source directory/file
|
|
# ------------------------------------------------------------------
|
|
$logDestDir = Join-Path $RunLogRoot "$($s.Name)_Logs"
|
|
|
|
if ($s.LogSrc) {
|
|
$logSrcExpanded = @(Expand-TokenizedArgs -ArgList @($s.LogSrc) -LogRoot $RunLogRoot -ExeDir $exeDir)
|
|
if ($logSrcExpanded.Count -gt 0 -and $logSrcExpanded[0]) {
|
|
$logSrcPath = Expand-EnvPath $logSrcExpanded[0]
|
|
if (Test-Path $logSrcPath) {
|
|
New-Item -ItemType Directory -Path $logDestDir -Force | Out-Null
|
|
Copy-Item -Path $logSrcPath -Destination $logDestDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Host " [INFO] Logs copied to: $logDestDir" -ForegroundColor Cyan
|
|
} else {
|
|
Write-Host " [WARNING] Log source not found: $logSrcPath" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Post-clean paths
|
|
# ------------------------------------------------------------------
|
|
foreach ($p in $s.PostCleanPaths) {
|
|
Remove-PathSilent $p
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Clean up randomized EXE copy
|
|
# ------------------------------------------------------------------
|
|
if ($randomizedExePath -and (Test-Path $randomizedExePath)) {
|
|
Remove-Item $randomizedExePath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Threat and reboot detection from exit code
|
|
# ------------------------------------------------------------------
|
|
$threatsFound = 0
|
|
if ($null -ne $exitCode) {
|
|
$threatsFound = Get-ExitCodeThreats -ScannerName $s.Name -ExitCode $exitCode
|
|
if (Get-ExitCodeReboot -ScannerName $s.Name -ExitCode $exitCode) {
|
|
$rebootRequired = $true
|
|
Write-Host " [WARNING] $($s.Name) signals REBOOT REQUIRED to complete removal" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
$totalThreats += $threatsFound
|
|
|
|
# ------------------------------------------------------------------
|
|
# Status reporting
|
|
# ------------------------------------------------------------------
|
|
$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
|
|
})
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auto-remediate: re-run in clean mode if scan-only found threats
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if ($AutoRemediate -and $ScanOnly -and $totalThreats -gt 0) {
|
|
Write-Host ''
|
|
Write-Host ' [INFO] -AutoRemediate: threats found - re-running all scanners in clean mode...' -ForegroundColor Cyan
|
|
Write-Host ''
|
|
|
|
# Reset accumulators for the clean pass
|
|
$results.Clear()
|
|
$totalThreats = 0
|
|
$rebootRequired = $false
|
|
$ScanMode = 'clean'
|
|
|
|
$remList = if ($SkipEset) {
|
|
$ScannerDefs | Where-Object { $_.Name -ne 'ESET' }
|
|
} else {
|
|
$ScannerDefs
|
|
}
|
|
|
|
if ($Scanners -and $Scanners.Count -gt 0) {
|
|
$remList = $remList | Where-Object { $Scanners -contains $_.Name }
|
|
}
|
|
|
|
foreach ($s in $remList) {
|
|
Write-Host '------------------------------------------------------------' -ForegroundColor DarkGray
|
|
Write-Host " Scanner (clean) : $($s.Name)" -ForegroundColor Cyan
|
|
|
|
$mainExePath = $s.Exe
|
|
$installerExePath = $s.InstallerExe
|
|
$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
|
|
}
|
|
|
|
$exeDir = Split-Path $mainExePath -Parent
|
|
$expandedArgs = Expand-TokenizedArgs -ArgList @($s.CleanArgs) -LogRoot $RunLogRoot -ExeDir $exeDir
|
|
|
|
if ($s.HitmanProReset -eq $true) {
|
|
Write-Host ' [INFO] Resetting HitmanPro trial registry...' -ForegroundColor Cyan
|
|
Invoke-HitmanProTrialReset
|
|
}
|
|
|
|
foreach ($p in $s.PreCleanPaths) { Remove-PathSilent $p }
|
|
|
|
$effectiveTimeoutMin = if ($TimeoutMin -gt 0) { $TimeoutMin } else { $s.TimeoutMin }
|
|
$timeoutSec = $effectiveTimeoutMin * 60
|
|
|
|
if ($installerExePath) {
|
|
Write-Host " [INFO] Running installer: $installerExePath" -ForegroundColor Cyan
|
|
try {
|
|
$instProc = Start-Process -FilePath $installerExePath -ArgumentList '/S' -PassThru -WindowStyle Hidden
|
|
$instCompleted = Wait-ProcessWithTimeout -Process $instProc -TimeoutSeconds 300
|
|
try {
|
|
(New-Object -ComObject Shell.Application).Windows() |
|
|
Where-Object { $_.LocationURL -like '*EmsisoftCmd*' -or $_.LocationName -like '*EmsisoftCmd*' } |
|
|
ForEach-Object { $_.Quit() }
|
|
} catch {}
|
|
if (-not $instCompleted) {
|
|
Write-Host ' [WARNING] Installer timed out - skipping scanner' -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 not found 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 -WindowStyle Hidden
|
|
Wait-ProcessWithTimeout -Process $updateProc -TimeoutSeconds 300 | Out-Null
|
|
}
|
|
catch {
|
|
Write-Host " [WARNING] Update step failed: $_ - continuing with existing definitions" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
$randomizedExePath = $null
|
|
$launchExe = $mainExePath
|
|
|
|
if ($s.RandomizeExe -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] EXE randomized to: $randomizedExePath" -ForegroundColor Cyan
|
|
}
|
|
|
|
Write-Host " [....] Launching $($s.Name)..." -ForegroundColor Cyan
|
|
$scanStart = Get-Date
|
|
$status = 'completed'
|
|
$exitCode = $null
|
|
|
|
try {
|
|
$startParams = @{ FilePath = $launchExe; PassThru = $true; NoNewWindow = [bool]$Headless }
|
|
if ($expandedArgs -and $expandedArgs.Count -gt 0) {
|
|
$startParams['ArgumentList'] = $expandedArgs
|
|
}
|
|
$proc = Start-Process @startParams
|
|
$completed = Wait-ScannerCompletion -Process $proc -WaitOnProcess $s.WaitOnProcess -ServiceNames $s.ServiceNames -TimeoutSeconds $timeoutSec
|
|
if (-not $completed) {
|
|
$status = 'TIMED OUT'
|
|
Write-Host " [WARNING] $($s.Name) timed out after $effectiveTimeoutMin min" -ForegroundColor Yellow
|
|
} else {
|
|
$exitCode = $proc.ExitCode
|
|
}
|
|
}
|
|
catch {
|
|
$status = 'FAILED'
|
|
Write-Host " [ERROR] $($s.Name) failed to launch: $_" -ForegroundColor Red
|
|
}
|
|
|
|
$scanEnd = Get-Date
|
|
$durMin = [math]::Round(($scanEnd - $scanStart).TotalMinutes, 2)
|
|
$logDestDir = Join-Path $RunLogRoot "$($s.Name)_Clean_Logs"
|
|
|
|
if ($s.LogSrc) {
|
|
$logSrcExpanded = Expand-TokenizedArgs -ArgList @($s.LogSrc) -LogRoot $RunLogRoot -ExeDir $exeDir
|
|
if ($logSrcExpanded -and $logSrcExpanded.Count -gt 0 -and $logSrcExpanded[0]) {
|
|
$logSrcPath = Expand-EnvPath $logSrcExpanded[0]
|
|
if (Test-Path $logSrcPath) {
|
|
New-Item -ItemType Directory -Path $logDestDir -Force | Out-Null
|
|
Copy-Item -Path $logSrcPath -Destination $logDestDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Host " [INFO] Logs copied to: $logDestDir" -ForegroundColor Cyan
|
|
} else {
|
|
Write-Host " [WARNING] Log source not found: $logSrcPath" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($p in $s.PostCleanPaths) { Remove-PathSilent $p }
|
|
|
|
if ($randomizedExePath -and (Test-Path $randomizedExePath)) {
|
|
Remove-Item $randomizedExePath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
$threatsFound = 0
|
|
if ($null -ne $exitCode) {
|
|
$threatsFound = Get-ExitCodeThreats -ScannerName $s.Name -ExitCode $exitCode
|
|
if (Get-ExitCodeReboot -ScannerName $s.Name -ExitCode $exitCode) {
|
|
$rebootRequired = $true
|
|
Write-Host " [WARNING] $($s.Name) signals REBOOT REQUIRED to complete removal" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
$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
|
|
})
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Force-remove blacklist stage (runs after all scanners)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Invoke-ForceRemoveList -RemoveList $ForceRemove
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build 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
|
|
}
|
|
}
|
|
|
|
$resultsObj = [ordered]@{
|
|
scan_id = $ScanStamp
|
|
machine = $env:COMPUTERNAME
|
|
started_at = $startedAt.ToUniversalTime().ToString('o')
|
|
completed_at = $completedAt.ToUniversalTime().ToString('o')
|
|
total_threats = $totalThreats
|
|
reboot_required = $rebootRequired
|
|
scan_mode = $ScanMode
|
|
scanners = @($scannerResults)
|
|
}
|
|
|
|
$resultsJsonPath = Join-Path $RunLogRoot 'results.json'
|
|
$resultsObj | ConvertTo-Json -Depth 10 | Set-Content -Path $resultsJsonPath -Encoding UTF8
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Export CSV summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$csvPath = Join-Path $RunLogRoot '_summary.csv'
|
|
$results | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Archive all logs into a zip file for easy retrieval
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$reportsDir = "$Base\reports"
|
|
New-Item -ItemType Directory -Path $reportsDir -Force | Out-Null
|
|
|
|
$zipPath = "$reportsDir\$ScanStamp.zip"
|
|
try {
|
|
Compress-Archive -Path "$RunLogRoot\*" -DestinationPath $zipPath -Force
|
|
Write-Host "[INFO] Log archive: $zipPath" -ForegroundColor Cyan
|
|
}
|
|
catch {
|
|
Write-Host "[WARNING] Failed to create log archive: $_" -ForegroundColor Yellow
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Print summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Write-Host ''
|
|
Write-Host '============================================================' -ForegroundColor Cyan
|
|
Write-Host " SCAN COMPLETE - $ScanStamp" -ForegroundColor Cyan
|
|
Write-Host '============================================================' -ForegroundColor Cyan
|
|
Write-Host ''
|
|
$results | Format-Table -AutoSize
|
|
Write-Host ''
|
|
Write-Host " Total threats detected : $totalThreats"
|
|
Write-Host " Results JSON : $resultsJsonPath"
|
|
Write-Host " Summary CSV : $csvPath"
|
|
Write-Host " Log archive : $zipPath"
|
|
Write-Host ''
|
|
|
|
if ($totalThreats -gt 0) {
|
|
Write-Host " [WARNING] THREATS DETECTED - review logs in: $RunLogRoot" -ForegroundColor Yellow
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# If any scanner requires a reboot, set up the temp cleanup session and reboot
|
|
# ---------------------------------------------------------------------------
|
|
if ($rebootRequired) {
|
|
$consoleUser = ''
|
|
try {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
if ($identity -notmatch 'SYSTEM') {
|
|
# Running as a real user - whoami returns the actual SAM login name
|
|
$consoleUser = (& whoami).Trim()
|
|
} else {
|
|
# Running as SYSTEM (e.g. via RMM) - find the console session user via quser
|
|
$quserLines = & quser 2>$null
|
|
$consoleLine = $quserLines | Where-Object { $_ -match '\bconsole\b' }
|
|
if ($consoleLine -and $consoleLine -match '^\s*>?\s*(\S+)') {
|
|
$samName = $Matches[1]
|
|
$ci = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
|
|
$prefix = if ($ci -and $ci.PartOfDomain) { $ci.Domain } else { $env:COMPUTERNAME }
|
|
$consoleUser = "$prefix\$samName"
|
|
}
|
|
}
|
|
} catch {}
|
|
Invoke-RebootCleanupSetup -OriginalUser $consoleUser -ScanId $ScanStamp -LogRoot $RunLogRoot
|
|
# Invoke-RebootCleanupSetup does not return - it calls Restart-Computer
|
|
}
|
|
|
|
exit 0
|