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

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

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

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

295 lines
12 KiB
PowerShell

<#
.SYNOPSIS
Parses a GuruScan results.json into a human-readable report.
.DESCRIPTION
Locates a results.json (defaults to the most recent scan in C:\ScanLogs\),
prints a formatted summary with per-scanner results, threat counts, and
a remediation recommendation if threats were found.
Use -AI to send log content to a local Ollama model for threat analysis
and AI-generated remediation recommendations.
.PARAMETER ResultsFile
Full path to a specific results.json. If omitted, the latest file in
C:\ScanLogs\ is used automatically.
.PARAMETER ShowAll
Show every scanner including those that completed clean (no threats).
.PARAMETER AI
Send scan logs to local Ollama (http://localhost:11434) for AI-powered
threat analysis and prioritized remediation recommendations.
.PARAMETER OllamaUrl
Ollama base URL. Defaults to http://localhost:11434.
.PARAMETER OllamaModel
Ollama model to use. Defaults to qwen3.6:latest.
.EXAMPLE
.\Get-ScanSummary.ps1
.\Get-ScanSummary.ps1 -AI
.\Get-ScanSummary.ps1 -ResultsFile "C:\ScanLogs\DESKTOP-20260523-143000\results.json" -AI
#>
[CmdletBinding()]
param(
[string]$ResultsFile = '',
[switch]$ShowAll,
[switch]$AI,
[string]$OllamaUrl = 'http://localhost:11434',
[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
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