feat(guru-scan): fix exit code capture, add GURUSCAN_RESULT_JSON reporting, pre-scan hardening
Exit code fix: add $proc.Handle caching after Start-Process -PassThru to prevent
the handle from being released before ExitCode is readable (known PS5.1 bug).
GuruRMM reporting: launcher now finds results.json after each scan and emits
GURUSCAN_RESULT_JSON:<compressed> to stdout. Agent CommandResult captures it;
server stores it in commands.stdout for retrieval via GET /api/commands/:id.
Pre-scan hardening:
- Pre-flight EXE check: warns about missing scanner binaries before run starts
- Windows Defender exclusions added for scanner/log paths before scan, removed after
AdwCleaner: add /path {LOG_ROOT} arg so logs write directly to scan log root;
update log_src to {LOG_ROOT}\Logs to match.
HitmanPro: add /quiet to scan and clean args to suppress GUI in headless runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# GuruScan.psm1
|
# GuruScan.psm1
|
||||||
# Multi-engine malware scan orchestrator for GuruRMM.
|
# Multi-engine malware scan orchestrator for GuruRMM.
|
||||||
# PowerShell 5.1 compatible — no ternary, no ??, no ?. operators.
|
# PowerShell 5.1 compatible -- no ternary, no ??, no ?. operators.
|
||||||
#
|
#
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -73,7 +73,7 @@ function Wait-ProcessWithTimeout {
|
|||||||
.OUTPUTS
|
.OUTPUTS
|
||||||
$true if the process exited cleanly within the timeout, $false if it was killed.
|
$true if the process exited cleanly within the timeout, $false if it was killed.
|
||||||
.NOTES
|
.NOTES
|
||||||
Calls WaitForExit(5000) before returning to flush the exit code — required
|
Calls WaitForExit(5000) before returning to flush the exit code -- required
|
||||||
before the ExitCode property is reliably readable on fast-exit processes.
|
before the ExitCode property is reliably readable on fast-exit processes.
|
||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
@@ -89,7 +89,7 @@ function Wait-ProcessWithTimeout {
|
|||||||
}
|
}
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
}
|
}
|
||||||
# Flush exit code — required before ExitCode property is readable
|
# Flush exit code -- required before ExitCode property is readable
|
||||||
$Process.WaitForExit(5000) | Out-Null
|
$Process.WaitForExit(5000) | Out-Null
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ function Get-ExitCodeThreats {
|
|||||||
'MSERT' { if ($ExitCode -ne 0) { return 1 } }
|
'MSERT' { if ($ExitCode -ne 0) { return 1 } }
|
||||||
'TDSSKiller' { if ($ExitCode -eq 1) { return 1 } }
|
'TDSSKiller' { if ($ExitCode -eq 1) { return 1 } }
|
||||||
'Stinger' { if ($ExitCode -eq 13) { return 1 } }
|
'Stinger' { if ($ExitCode -eq 13) { return 1 } }
|
||||||
# RKill: exit 1 = processes were killed — not a threat count
|
# RKill: exit 1 = processes were killed -- not a threat count
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ function Invoke-ScanPass {
|
|||||||
Write-Host " Scanner : $($s.Name)" -ForegroundColor Cyan
|
Write-Host " Scanner : $($s.Name)" -ForegroundColor Cyan
|
||||||
Write-Host " Mode : $ScanMode"
|
Write-Host " Mode : $ScanMode"
|
||||||
|
|
||||||
# Normalize property names — JSON objects use snake_case, PS hashtables use PascalCase
|
# Normalize property names -- JSON objects use snake_case, PS hashtables use PascalCase
|
||||||
$sName = if ($s.PSObject.Properties['Name']) { $s.Name } else { $s.name }
|
$sName = if ($s.PSObject.Properties['Name']) { $s.Name } else { $s.name }
|
||||||
$sExe = if ($s.PSObject.Properties['Exe']) { $s.Exe } else { $s.exe }
|
$sExe = if ($s.PSObject.Properties['Exe']) { $s.Exe } else { $s.exe }
|
||||||
$sInstallerExe = if ($s.PSObject.Properties['InstallerExe']) { $s.InstallerExe } else { $s.installer_exe }
|
$sInstallerExe = if ($s.PSObject.Properties['InstallerExe']) { $s.InstallerExe } else { $s.installer_exe }
|
||||||
@@ -416,8 +416,10 @@ function Invoke-ScanPass {
|
|||||||
$sWaitOnProcess = if ($s.PSObject.Properties['WaitOnProcess']) { $s.WaitOnProcess } else { $s.wait_on_process }
|
$sWaitOnProcess = if ($s.PSObject.Properties['WaitOnProcess']) { $s.WaitOnProcess } else { $s.wait_on_process }
|
||||||
$sInstallerArgs = if ($s.PSObject.Properties['InstallerArgs']) { $s.InstallerArgs } else { $s.installer_args }
|
$sInstallerArgs = if ($s.PSObject.Properties['InstallerArgs']) { $s.InstallerArgs } else { $s.installer_args }
|
||||||
$sRunUpdateAfterInstall = if ($s.PSObject.Properties['RunUpdateAfterInstall']) { $s.RunUpdateAfterInstall } else { $s.run_update_after_install }
|
$sRunUpdateAfterInstall = if ($s.PSObject.Properties['RunUpdateAfterInstall']) { $s.RunUpdateAfterInstall } else { $s.run_update_after_install }
|
||||||
|
$sSession0Compatible = if ($s.PSObject.Properties['Session0Compatible']) { $s.Session0Compatible } else { $s.session0_compatible }
|
||||||
if ($null -eq $sInstallerArgs) { $sInstallerArgs = @('/S') }
|
if ($null -eq $sInstallerArgs) { $sInstallerArgs = @('/S') }
|
||||||
if ($null -eq $sRunUpdateAfterInstall) { $sRunUpdateAfterInstall = $false }
|
if ($null -eq $sRunUpdateAfterInstall) { $sRunUpdateAfterInstall = $false }
|
||||||
|
if ($null -eq $sSession0Compatible) { $sSession0Compatible = $true }
|
||||||
|
|
||||||
# Coerce null JSON values to empty arrays / nulls
|
# Coerce null JSON values to empty arrays / nulls
|
||||||
if ($null -eq $sInstallerExe) { $sInstallerExe = $null }
|
if ($null -eq $sInstallerExe) { $sInstallerExe = $null }
|
||||||
@@ -428,6 +430,20 @@ function Invoke-ScanPass {
|
|||||||
if ($null -eq $sPostClean) { $sPostClean = @() }
|
if ($null -eq $sPostClean) { $sPostClean = @() }
|
||||||
if ($null -eq $sServiceNames) { $sServiceNames = @() }
|
if ($null -eq $sServiceNames) { $sServiceNames = @() }
|
||||||
|
|
||||||
|
# Skip GUI-only scanners when running as SYSTEM in Session 0 (no interactive desktop)
|
||||||
|
if (-not $sSession0Compatible -and (Test-RunningAsSystem)) {
|
||||||
|
Write-Host " [WARNING] $sName requires an interactive user session -- skipping (running as SYSTEM)" -ForegroundColor Yellow
|
||||||
|
$results.Add([pscustomobject]@{
|
||||||
|
Scanner = $sName
|
||||||
|
Status = 'SKIPPED (requires user session)'
|
||||||
|
ExitCode = $null
|
||||||
|
ThreatsFound = 0
|
||||||
|
Duration = '0 min'
|
||||||
|
LogPath = ''
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
$mainExePath = $sExe
|
$mainExePath = $sExe
|
||||||
$installerExePath = $sInstallerExe
|
$installerExePath = $sInstallerExe
|
||||||
$exeToCheck = if ($installerExePath) { $installerExePath } else { $mainExePath }
|
$exeToCheck = if ($installerExePath) { $installerExePath } else { $mainExePath }
|
||||||
@@ -524,6 +540,7 @@ function Invoke-ScanPass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$proc = Start-Process @startParams
|
$proc = Start-Process @startParams
|
||||||
|
$null = $proc.Handle # force handle caching -- Start-Process -PassThru can lose the handle before exit, making ExitCode return null
|
||||||
|
|
||||||
$serviceArr = @()
|
$serviceArr = @()
|
||||||
if ($sServiceNames -and $sServiceNames.Count -gt 0) {
|
if ($sServiceNames -and $sServiceNames.Count -gt 0) {
|
||||||
@@ -539,6 +556,7 @@ function Invoke-ScanPass {
|
|||||||
$status = 'TIMED OUT'
|
$status = 'TIMED OUT'
|
||||||
Write-Host " [WARNING] $sName timed out after $effectiveTimeoutMin min" -ForegroundColor Yellow
|
Write-Host " [WARNING] $sName timed out after $effectiveTimeoutMin min" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
|
$proc.WaitForExit() | Out-Null # no-arg form guarantees ExitCode is readable
|
||||||
$exitCode = $proc.ExitCode
|
$exitCode = $proc.ExitCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,7 +637,7 @@ function Register-ScannerCleanupTask {
|
|||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host '------------------------------------------------------------' -ForegroundColor Yellow
|
Write-Host '------------------------------------------------------------' -ForegroundColor Yellow
|
||||||
Write-Host ' Reboot recommended — registering post-reboot cleanup task' -ForegroundColor Yellow
|
Write-Host ' Reboot recommended -- registering post-reboot cleanup task' -ForegroundColor Yellow
|
||||||
Write-Host '------------------------------------------------------------' -ForegroundColor Yellow
|
Write-Host '------------------------------------------------------------' -ForegroundColor Yellow
|
||||||
|
|
||||||
# Write state for the cleanup script to read
|
# Write state for the cleanup script to read
|
||||||
@@ -629,45 +647,14 @@ function Register-ScannerCleanupTask {
|
|||||||
created_at = (Get-Date).ToUniversalTime().ToString('o')
|
created_at = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
} | ConvertTo-Json | Set-Content "$script:Base\cleanup-state.json" -Encoding UTF8
|
} | ConvertTo-Json | Set-Content "$script:Base\cleanup-state.json" -Encoding UTF8
|
||||||
|
|
||||||
# Write the cleanup script that the scheduled task will run
|
# Copy the static cleanup script from the module directory to C:\GuruScan\
|
||||||
$cleanupScript = @'
|
$cleanupSrc = Join-Path $script:ModuleRoot 'Invoke-ScannerCleanup.ps1'
|
||||||
$Base = 'C:\GuruScan'
|
if (Test-Path $cleanupSrc) {
|
||||||
$state = @{ scan_id = ''; log_root = '' }
|
Copy-Item -Path $cleanupSrc -Destination "$script:Base\Invoke-ScannerCleanup.ps1" -Force
|
||||||
$stateFile = "$Base\cleanup-state.json"
|
} else {
|
||||||
|
Write-Host " [WARNING] Invoke-ScannerCleanup.ps1 not found at $cleanupSrc -- cleanup task will not run." -ForegroundColor Yellow
|
||||||
if (Test-Path $stateFile) {
|
return
|
||||||
try { $state = Get-Content $stateFile -Raw | ConvertFrom-Json } catch {}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Remove scanner installation files
|
|
||||||
$scannerPaths = @(
|
|
||||||
'C:\EmsisoftCmd',
|
|
||||||
'C:\AdwCleaner',
|
|
||||||
'C:\ProgramData\HitmanPro',
|
|
||||||
'C:\ProgramData\HitmanPro.Alert'
|
|
||||||
)
|
|
||||||
foreach ($p in $scannerPaths) {
|
|
||||||
Remove-Item -Path $p -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove scanner download EXEs
|
|
||||||
Remove-Item -Path "$Base\downloads" -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# Flag logs for GuruRMM to pull
|
|
||||||
@{
|
|
||||||
scan_id = $state.scan_id
|
|
||||||
log_root = $state.log_root
|
|
||||||
zip_path = "$Base\reports\$($state.scan_id).zip"
|
|
||||||
cleaned_at = (Get-Date).ToUniversalTime().ToString('o')
|
|
||||||
} | ConvertTo-Json | Set-Content "$Base\logs-ready.json" -Encoding UTF8
|
|
||||||
|
|
||||||
# Remove state file
|
|
||||||
Remove-Item -Path $stateFile -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# Unregister this task
|
|
||||||
Unregister-ScheduledTask -TaskName 'GuruRMM-ScannerCleanup' -Confirm:$false -ErrorAction SilentlyContinue
|
|
||||||
'@
|
|
||||||
$cleanupScript | Set-Content "$script:Base\Invoke-ScannerCleanup.ps1" -Encoding UTF8
|
|
||||||
|
|
||||||
# Register as SYSTEM logon task with 30-minute delay
|
# Register as SYSTEM logon task with 30-minute delay
|
||||||
try {
|
try {
|
||||||
@@ -776,11 +763,11 @@ function Invoke-GuruScan {
|
|||||||
[string]$OutputSink = 'Disk'
|
[string]$OutputSink = 'Disk'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Whitelist — written to C:\GuruScan\whitelist.txt before any scanner runs.
|
# Whitelist -- written to C:\GuruScan\whitelist.txt before any scanner runs.
|
||||||
# Emsisoft and HitmanPro honour this; RKill and AdwCleaner do not.
|
# Emsisoft and HitmanPro honour this; RKill and AdwCleaner do not.
|
||||||
$whitelist = @('C:\GuruScan')
|
$whitelist = @('C:\GuruScan')
|
||||||
|
|
||||||
# ForceRemove blacklist — items removed after all scanners complete.
|
# ForceRemove blacklist -- items removed after all scanners complete.
|
||||||
$forceRemove = @()
|
$forceRemove = @()
|
||||||
|
|
||||||
$scanStamp = "$env:COMPUTERNAME-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
$scanStamp = "$env:COMPUTERNAME-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
||||||
@@ -836,6 +823,31 @@ function Invoke-GuruScan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Pre-flight: warn about missing scanner EXEs so missing scanners are
|
||||||
|
# visible up front rather than silently skipped mid-run.
|
||||||
|
$missingExes = @()
|
||||||
|
foreach ($s in $scannerList) {
|
||||||
|
$sName = if ($s.PSObject.Properties['Name']) { $s.Name } else { $s.name }
|
||||||
|
$sExe = if ($s.PSObject.Properties['Exe']) { $s.Exe } else { $s.exe }
|
||||||
|
$sInstExe = if ($s.PSObject.Properties['InstallerExe']) { $s.InstallerExe } else { $s.installer_exe }
|
||||||
|
if (-not (Test-Path $sExe) -and (-not $sInstExe -or -not (Test-Path $sInstExe))) {
|
||||||
|
$missingExes += $sName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($missingExes.Count -gt 0) {
|
||||||
|
Write-Host "[WARNING] Missing scanner EXEs -- these will be skipped: $($missingExes -join ', ')" -ForegroundColor Yellow
|
||||||
|
Write-Host " Run .\Download-Scanners.ps1 to download them." -ForegroundColor Yellow
|
||||||
|
Write-Host ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add Windows Defender exclusions for scanner paths so Defender does not
|
||||||
|
# quarantine scanner EXEs or log files mid-run.
|
||||||
|
$defenderExclusions = @($script:Base, $script:LogRoot, 'C:\EmsisoftCmd', 'C:\AdwCleaner')
|
||||||
|
try {
|
||||||
|
Add-MpPreference -ExclusionPath $defenderExclusions -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "[INFO] Windows Defender exclusions added for scanner paths" -ForegroundColor Cyan
|
||||||
|
} catch {}
|
||||||
|
|
||||||
# First pass
|
# First pass
|
||||||
$startedAt = Get-Date
|
$startedAt = Get-Date
|
||||||
$passParams = @{
|
$passParams = @{
|
||||||
@@ -964,7 +976,14 @@ function Invoke-GuruScan {
|
|||||||
Register-ScannerCleanupTask -ScanId $scanStamp -LogRoot $runLogRoot
|
Register-ScannerCleanupTask -ScanId $scanStamp -LogRoot $runLogRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
# Return result object (always returned; caller uses it for RMM sink)
|
# Remove the Defender exclusions added at scan start.
|
||||||
|
try {
|
||||||
|
Remove-MpPreference -ExclusionPath $defenderExclusions -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "[INFO] Windows Defender exclusions removed" -ForegroundColor Cyan
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
# Return result object (RMM sink) or suppress it (Disk sink lets the
|
||||||
|
# launcher read results.json from disk for structured reporting).
|
||||||
$resultRecord = [pscustomobject]$resultsObj
|
$resultRecord = [pscustomobject]$resultsObj
|
||||||
if ($OutputSink -eq 'RMM') {
|
if ($OutputSink -eq 'RMM') {
|
||||||
return $resultRecord
|
return $resultRecord
|
||||||
@@ -1093,10 +1112,10 @@ function Invoke-Remediation {
|
|||||||
|
|
||||||
if ($totalThreats -gt 0) {
|
if ($totalThreats -gt 0) {
|
||||||
Write-Host " [WARNING] $totalThreats threat indicator(s) still detected after clean pass." -ForegroundColor Yellow
|
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 " 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
|
Write-Host " they can be fully removed. Reboot and re-scan." -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host " [OK] Clean pass complete — no threats detected in output." -ForegroundColor Green
|
Write-Host " [OK] Clean pass complete -- no threats detected in output." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@@ -1271,7 +1290,7 @@ function Get-ScanSummary {
|
|||||||
Write-Host " Full logs: $(Split-Path $ResultsFile)" -ForegroundColor Gray
|
Write-Host " Full logs: $(Split-Path $ResultsFile)" -ForegroundColor Gray
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
# Ollama AI analysis (optional — requires -AI switch)
|
# Ollama AI analysis (optional -- requires -AI switch)
|
||||||
if ($AI) {
|
if ($AI) {
|
||||||
Write-Host "================================================================" -ForegroundColor Cyan
|
Write-Host "================================================================" -ForegroundColor Cyan
|
||||||
Write-Host " AI Analysis (Ollama)" -ForegroundColor Cyan
|
Write-Host " AI Analysis (Ollama)" -ForegroundColor Cyan
|
||||||
@@ -1311,7 +1330,7 @@ $logText
|
|||||||
|
|
||||||
Task: Extract a concise list of specific threat names, file paths, registry keys, or other findings from the logs.
|
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."
|
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.
|
Do not add commentary -- only the list.
|
||||||
"@
|
"@
|
||||||
|
|
||||||
$threatDetails = Invoke-Ollama -Prompt $threatPrompt -Model $OllamaModel -BaseUrl $OllamaUrl
|
$threatDetails = Invoke-Ollama -Prompt $threatPrompt -Model $OllamaModel -BaseUrl $OllamaUrl
|
||||||
@@ -1320,7 +1339,7 @@ Do not add commentary — only the list.
|
|||||||
Write-Host " Identified Threats / Findings:" -ForegroundColor Yellow
|
Write-Host " Identified Threats / Findings:" -ForegroundColor Yellow
|
||||||
$threatDetails -split "`n" | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
|
$threatDetails -split "`n" | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
|
||||||
} else {
|
} else {
|
||||||
Write-Host " [WARNING] Ollama unavailable — skipping threat extraction." -ForegroundColor Yellow
|
Write-Host " [WARNING] Ollama unavailable -- skipping threat extraction." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
# Step 2: Prioritized remediation recommendations
|
# Step 2: Prioritized remediation recommendations
|
||||||
@@ -1345,7 +1364,7 @@ Plain numbered list, no markdown headers, no padding text. Be specific.
|
|||||||
Write-Host " Remediation Checklist:" -ForegroundColor Cyan
|
Write-Host " Remediation Checklist:" -ForegroundColor Cyan
|
||||||
$recommendations -split "`n" | ForEach-Object { Write-Host " $_" -ForegroundColor Cyan }
|
$recommendations -split "`n" | ForEach-Object { Write-Host " $_" -ForegroundColor Cyan }
|
||||||
} else {
|
} else {
|
||||||
Write-Host " [WARNING] Ollama unavailable — skipping recommendations." -ForegroundColor Yellow
|
Write-Host " [WARNING] Ollama unavailable -- skipping recommendations." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@@ -1363,7 +1382,7 @@ function Invoke-PostRebootCleanup {
|
|||||||
Normally this runs automatically via the GuruRMM-ScannerCleanup scheduled task.
|
Normally this runs automatically via the GuruRMM-ScannerCleanup scheduled task.
|
||||||
.PARAMETER StateFile
|
.PARAMETER StateFile
|
||||||
Path to cleanup-state.json. Defaults to C:\GuruScan\cleanup-state.json.
|
Path to cleanup-state.json. Defaults to C:\GuruScan\cleanup-state.json.
|
||||||
(Parameter kept for backward compatibility — the cleanup script reads it directly.)
|
(Parameter kept for backward compatibility -- the cleanup script reads it directly.)
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
|
|||||||
@@ -52,4 +52,22 @@ if (-not (Test-Path $moduleManifest)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Import-Module $moduleManifest -Force
|
Import-Module $moduleManifest -Force
|
||||||
|
$scanStart = Get-Date
|
||||||
Invoke-GuruScan @PSBoundParameters -OutputSink Disk
|
Invoke-GuruScan @PSBoundParameters -OutputSink Disk
|
||||||
|
|
||||||
|
# Emit structured JSON to stdout for GuruRMM CommandResult capture.
|
||||||
|
# Read from results.json written during this run (newer than $scanStart).
|
||||||
|
$resultsFile = Get-ChildItem -Path 'C:\ScanLogs' -Recurse -Filter 'results.json' -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.LastWriteTime -gt $scanStart } |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($resultsFile) {
|
||||||
|
$json = Get-Content -Path $resultsFile.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||||
|
if ($json) {
|
||||||
|
# Compress to single line so the agent's stdout parser sees it as one line
|
||||||
|
$compressed = ($json | ConvertFrom-Json | ConvertTo-Json -Depth 10 -Compress)
|
||||||
|
Write-Output ''
|
||||||
|
Write-Output "GURUSCAN_RESULT_JSON:$compressed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Scanners run in this order. Each stage hands off to the next regardless of findi
|
|||||||
| # | Scanner | Category | Notes |
|
| # | Scanner | Category | Notes |
|
||||||
|---|---------|----------|-------|
|
|---|---------|----------|-------|
|
||||||
| 1 | **RKill** | process-killer | Kills malware-related processes before scanners run. Exit 1 = processes were killed (not a threat indicator). |
|
| 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. |
|
| 2 | **AdwCleaner** | adware | Removes adware, PUPs, and browser hijackers. **Requires an interactive user session** (GUI app; no headless/SYSTEM mode). Skipped automatically when running as SYSTEM with no desktop. To include AdwCleaner, dispatch via GuruRMM with `context: user_session`. |
|
||||||
| 3 | **Emsisoft Command Line Scanner** | antimalware | Two-step: NSIS installer extracts to `C:\EmsisoftCmd\`, then `/update` fetches latest definitions, then scans. |
|
| 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`. |
|
| 4 | **HitmanPro** | antimalware | Cloud-assisted second-opinion scanner. Trial registry is reset before each run via `Invoke-HitmanProTrialReset`. |
|
||||||
|
|
||||||
@@ -75,6 +75,11 @@ To run cleanup immediately without waiting (e.g. if the task was missed):
|
|||||||
|
|
||||||
- `-Headless` passes `NoNewWindow` to all scanner launches, suppressing UI windows.
|
- `-Headless` passes `NoNewWindow` to all scanner launches, suppressing UI windows.
|
||||||
Use this when dispatching from an RMM agent that has no interactive desktop.
|
Use this when dispatching from an RMM agent that has no interactive desktop.
|
||||||
|
- Scanners with `session0_compatible: false` in `scanners.json` are automatically skipped
|
||||||
|
when the module detects it is running as SYSTEM (Session 0). Currently: **AdwCleaner**.
|
||||||
|
The result record shows `SKIPPED (requires user session)` rather than a failure.
|
||||||
|
- To run AdwCleaner via GuruRMM, dispatch with `context: user_session` so it runs in
|
||||||
|
the active user's desktop session (requires a logged-in user on the target machine).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -156,6 +161,7 @@ guru-scan\
|
|||||||
Invoke-Remediation.ps1 # Thin launcher -> Invoke-Remediation
|
Invoke-Remediation.ps1 # Thin launcher -> Invoke-Remediation
|
||||||
Get-ScanSummary.ps1 # Thin launcher -> Get-ScanSummary
|
Get-ScanSummary.ps1 # Thin launcher -> Get-ScanSummary
|
||||||
Invoke-PostRebootCleanup.ps1 # Thin launcher -> Invoke-PostRebootCleanup (manual cleanup trigger)
|
Invoke-PostRebootCleanup.ps1 # Thin launcher -> Invoke-PostRebootCleanup (manual cleanup trigger)
|
||||||
|
Invoke-ScannerCleanup.ps1 # Post-reboot cleanup script; copied to C:\GuruScan\ when reboot is needed
|
||||||
Download-Scanners.ps1 # Downloads scanner EXEs from scanners.json URLs
|
Download-Scanners.ps1 # Downloads scanner EXEs from scanners.json URLs
|
||||||
downloads\ # Scanner EXEs (gitignored)
|
downloads\ # Scanner EXEs (gitignored)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"service_names": [],
|
"service_names": [],
|
||||||
"hitmanpro_trial_reset": false,
|
"hitmanpro_trial_reset": false,
|
||||||
"whitelist_arg": null,
|
"whitelist_arg": null,
|
||||||
"wait_on_process": null
|
"wait_on_process": null,
|
||||||
|
"session0_compatible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "AdwCleaner",
|
"name": "AdwCleaner",
|
||||||
@@ -33,18 +34,19 @@
|
|||||||
"download_url": "https://adwcleaner.malwarebytes.com/adwcleaner?channel=release",
|
"download_url": "https://adwcleaner.malwarebytes.com/adwcleaner?channel=release",
|
||||||
"manual_download": true,
|
"manual_download": true,
|
||||||
"manual_download_note": "Malwarebytes blocks automated downloads; download manually from https://www.malwarebytes.com/adwcleaner",
|
"manual_download_note": "Malwarebytes blocks automated downloads; download manually from https://www.malwarebytes.com/adwcleaner",
|
||||||
"scan_args": ["/eula", "/scan", "/noreboot"],
|
"scan_args": ["/eula", "/scan", "/noreboot", "/path", "{LOG_ROOT}"],
|
||||||
"clean_args": ["/eula", "/clean", "/noreboot"],
|
"clean_args": ["/eula", "/clean", "/noreboot", "/path", "{LOG_ROOT}"],
|
||||||
"log_src": "C:\\AdwCleaner\\Logs",
|
"log_src": "{LOG_ROOT}\\Logs",
|
||||||
"timeout_min": 60,
|
"timeout_min": 60,
|
||||||
"randomize_exe": false,
|
"randomize_exe": false,
|
||||||
"pre_close_processes": [],
|
"pre_close_processes": [],
|
||||||
"pre_clean_paths": ["C:\\AdwCleaner"],
|
"pre_clean_paths": ["C:\\AdwCleaner"],
|
||||||
"post_clean_paths": ["C:\\AdwCleaner"],
|
"post_clean_paths": [],
|
||||||
"service_names": ["AdwCleanerSvc"],
|
"service_names": ["AdwCleanerSvc"],
|
||||||
"hitmanpro_trial_reset": false,
|
"hitmanpro_trial_reset": false,
|
||||||
"whitelist_arg": null,
|
"whitelist_arg": null,
|
||||||
"wait_on_process": "AdwCleaner"
|
"wait_on_process": "AdwCleaner",
|
||||||
|
"session0_compatible": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Emsisoft",
|
"name": "Emsisoft",
|
||||||
@@ -94,7 +96,8 @@
|
|||||||
"service_names": [],
|
"service_names": [],
|
||||||
"hitmanpro_trial_reset": false,
|
"hitmanpro_trial_reset": false,
|
||||||
"whitelist_arg": "emsisoft",
|
"whitelist_arg": "emsisoft",
|
||||||
"wait_on_process": "a2cmd"
|
"wait_on_process": "a2cmd",
|
||||||
|
"session0_compatible": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "HitmanPro",
|
"name": "HitmanPro",
|
||||||
@@ -109,12 +112,14 @@
|
|||||||
"scan_args": [
|
"scan_args": [
|
||||||
"/noinstall",
|
"/noinstall",
|
||||||
"/scan",
|
"/scan",
|
||||||
|
"/quiet",
|
||||||
"/log=\"{LOG_ROOT}\\HitmanPro_Scan_Log.txt\"",
|
"/log=\"{LOG_ROOT}\\HitmanPro_Scan_Log.txt\"",
|
||||||
"/excludelist=\"C:\\GuruScan\\whitelist.txt\""
|
"/excludelist=\"C:\\GuruScan\\whitelist.txt\""
|
||||||
],
|
],
|
||||||
"clean_args": [
|
"clean_args": [
|
||||||
"/noinstall",
|
"/noinstall",
|
||||||
"/clean",
|
"/clean",
|
||||||
|
"/quiet",
|
||||||
"/log=\"{LOG_ROOT}\\HitmanPro_Scan_Log.txt\"",
|
"/log=\"{LOG_ROOT}\\HitmanPro_Scan_Log.txt\"",
|
||||||
"/excludelist=\"C:\\GuruScan\\whitelist.txt\""
|
"/excludelist=\"C:\\GuruScan\\whitelist.txt\""
|
||||||
],
|
],
|
||||||
@@ -137,7 +142,8 @@
|
|||||||
"service_names": [],
|
"service_names": [],
|
||||||
"hitmanpro_trial_reset": true,
|
"hitmanpro_trial_reset": true,
|
||||||
"whitelist_arg": "hitmanpro",
|
"whitelist_arg": "hitmanpro",
|
||||||
"wait_on_process": "HitmanPro_x64"
|
"wait_on_process": "HitmanPro_x64",
|
||||||
|
"session0_compatible": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user