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>
295 lines
12 KiB
PowerShell
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
|