Files
claudetools/projects/msp-tools/guru-scan/Invoke-GuruScan.ps1
Howard Enos 40e090c95a 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>
2026-05-27 00:13:16 -07:00

74 lines
2.8 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.
Scanner definitions are read from scanners.json in the same directory.
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 add
it back to scanners.json.
.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 scanners.json exactly.
.PARAMETER TimeoutMin
Override the per-scanner timeout (in minutes) for all scanners.
.PARAMETER SkipScanners
Skip one or more named scanners by name. Names must match the Name field
in scanners.json exactly. Useful for excluding a single scanner without
respecifying the entire list.
.PARAMETER Headless
Suppress scanner windows (used when dispatching via RMM).
.EXAMPLE
.\Invoke-GuruScan.ps1
.\Invoke-GuruScan.ps1 -ScanOnly -AutoRemediate
.\Invoke-GuruScan.ps1 -SkipScanners Emsisoft
.\Invoke-GuruScan.ps1 -Headless
#>
[CmdletBinding()]
param(
[switch]$ScanOnly,
[switch]$AutoRemediate,
[string[]]$Scanners,
[int]$TimeoutMin = 0,
[string[]]$SkipScanners = @(),
[switch]$Headless
)
$moduleManifest = Join-Path $PSScriptRoot 'GuruScan.psd1'
if (-not (Test-Path $moduleManifest)) {
Write-Host "[ERROR] GuruScan module not found: $moduleManifest" -ForegroundColor Red
exit 1
}
Import-Module $moduleManifest -Force
$scanStart = Get-Date
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"
}
}