#!/usr/bin/env bash
# guruscan-agent-test.sh - Deploy GuruScan to a Windows GuruRMM agent and run an
# end-to-end smoke test (full 3-engine chain, clean mode), pulling every log back
# for review.
#
# Phases:
#   prep    - upload module to C:\GuruScan, Defender-exclude tool dirs,
#             download RKill+Emsisoft, fetch HitmanPro, verify ready
#   scan    - dispatch Invoke-GuruScan.ps1 -Headless (clean mode) and poll
#   collect - pull results.json + per-scanner logs into the repo
#   all     - prep then scan then collect
#
# Usage:
#   bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>
#
# Mirrors the RMM plumbing in run-onboarding-diagnostic.sh (vault auth -> JWT ->
# chunked base64 upload -> dispatch -> poll). Detection testing uses the real
# malware-samples set via the scan-one / verify-each phases. Prep's EICAR seed is
# DISABLED by default (kept for future scanners) -- enable with SEED_EICAR=1.

set -u

TARGET="${1:-}"
PHASE="${2:-all}"
SCANNER_ARG="${3:-}"

# EICAR test-file seeding in the prep phase. DISABLED by default -- kept (not
# deleted) so it can be switched back on to smoke-test detection as new scanners
# are added to the chain. Enable per-run with:  SEED_EICAR=1 bash guruscan-agent-test.sh ...
SEED_EICAR="${SEED_EICAR:-0}"

if [ -z "$TARGET" ]; then
    echo "[ERROR] Usage: bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>" >&2
    exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
VAULT="$REPO_ROOT/.claude/scripts/vault.sh"
ALERT="$REPO_ROOT/.claude/scripts/post-bot-alert.sh"
GS_DIR="$REPO_ROOT/projects/msp-tools/guru-scan"
RESULTS_ROOT="$GS_DIR/test-results"
RMM="http://172.16.3.30:3001"
HITMANPRO_URL="https://dl.surfright.nl/HitmanPro_x64.exe"

_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "guruscan-test" "$@" >/dev/null 2>&1 || true; }
post_alert() { [ -f "$ALERT" ] && bash "$ALERT" "$1" >/dev/null 2>&1 || true; }

for tool in jq curl base64 split; do
    command -v "$tool" >/dev/null 2>&1 || { echo "[ERROR] Required tool not found: $tool" >&2; exit 1; }
done

# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
RMM_EMAIL="$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-email 2>/dev/null)"
RMM_PASS="$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-password 2>/dev/null)"
if [ -z "$RMM_EMAIL" ] || [ "$RMM_EMAIL" = "null" ] || [ -z "$RMM_PASS" ]; then
    echo "[ERROR] Could not read GuruRMM credentials from vault" >&2
    _logerr "vault read of GuruRMM creds failed"
    exit 1
fi
TOKEN="$(curl -s -m 30 -X POST "$RMM/api/auth/login" -H "Content-Type: application/json" \
    --data-binary "$(jq -nc --arg e "$RMM_EMAIL" --arg p "$RMM_PASS" '{email:$e,password:$p}')" | jq -r '.token // empty')"
if [ -z "$TOKEN" ]; then
    echo "[ERROR] RMM login failed" >&2; _logerr "RMM login failed"; exit 1
fi
echo "[OK] Authenticated to GuruRMM"

# ---------------------------------------------------------------------------
# Resolve agent (uuid -> exact host -> partial host)
# ---------------------------------------------------------------------------
AGENTS="$(curl -s -m 30 "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")"
echo "$AGENTS" | jq -e 'type=="array"' >/dev/null 2>&1 || { echo "[ERROR] Could not list agents" >&2; exit 1; }

if echo "$TARGET" | grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
    AGENT="$(echo "$AGENTS" | jq --arg id "$TARGET" '[.[]|select(.id==$id)]|.[0]//empty')"
else
    AGENT="$(echo "$AGENTS" | jq --arg h "$TARGET" '[.[]|select((.hostname|ascii_downcase)==($h|ascii_downcase))]|.[0]//empty')"
    if [ -z "$AGENT" ] || [ "$AGENT" = "null" ]; then
        MATCHES="$(echo "$AGENTS" | jq --arg h "$TARGET" '[.[]|select(.hostname|ascii_downcase|contains($h|ascii_downcase))]')"
        COUNT="$(echo "$MATCHES" | jq 'length')"
        [ "$COUNT" = "1" ] && AGENT="$(echo "$MATCHES" | jq '.[0]')"
        if [ "$COUNT" != "1" ]; then
            echo "[ERROR] $COUNT agents match '$TARGET'. Be more specific." >&2
            echo "$MATCHES" | jq -r '.[]|"  \(.hostname) (\(.os_type)) id=\(.id)"' >&2
            exit 1
        fi
    fi
fi
[ -z "$AGENT" ] || [ "$AGENT" = "null" ] && { echo "[ERROR] No agent matching '$TARGET'." >&2; exit 1; }

AGENT_ID="$(echo "$AGENT" | jq -r '.id')"
AGENT_HOST="$(echo "$AGENT" | jq -r '.hostname')"
AGENT_OS="$(echo "$AGENT" | jq -r '.os_type')"
AGENT_STATUS="$(echo "$AGENT" | jq -r '.status // "unknown"')"
echo "[OK] Agent: $AGENT_HOST ($AGENT_OS) status=$AGENT_STATUS id=$AGENT_ID"
[ "$AGENT_OS" != "windows" ] && { echo "[ERROR] Windows-only. os_type=$AGENT_OS" >&2; exit 1; }

WORK_DIR="$(mktemp -d 2>/dev/null || echo "${TMPDIR:-/tmp}/guruscan-test-$$")"
mkdir -p "$WORK_DIR"
trap 'rm -rf "$WORK_DIR" 2>/dev/null || true' EXIT

# ---------------------------------------------------------------------------
# dispatch_one <script-file> <timeout_seconds> <max_polls> -> echoes result JSON
# ---------------------------------------------------------------------------
dispatch_one() {
    local script_file="$1" to="$2" max_polls="${3:-72}"
    local payload_file resp cmd_id status result count
    payload_file="$WORK_DIR/payload.json"
    jq -nc --rawfile cmd "$script_file" --argjson to "$to" \
        '{command_type:"powershell", command:$cmd, timeout_seconds:$to}' > "$payload_file"
    resp="$(curl -s -m 30 -X POST "$RMM/api/agents/$AGENT_ID/command" \
        -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
        --data-binary "@$payload_file")"
    cmd_id="$(echo "$resp" | jq -r '.command_id // empty')"
    [ -z "$cmd_id" ] && { echo "[ERROR] Dispatch failed: $resp" >&2; return 1; }
    count=0
    while [ $count -lt "$max_polls" ]; do
        result="$(curl -s -m 30 "$RMM/api/commands/$cmd_id" -H "Authorization: Bearer $TOKEN")"
        status="$(echo "$result" | jq -r '.status // empty')"
        case "$status" in
            completed|failed|cancelled|interrupted)
                printf '%s' "$cmd_id" > "$WORK_DIR/last_cmd_id"
                echo "$result"; return 0 ;;
            *) count=$((count+1)); sleep 5 ;;
        esac
    done
    echo "[ERROR] Command $cmd_id did not finish (last=$status)" >&2; return 1
}

# run_ps <script-file> <timeout> <max_polls> <label> -> prints status, returns nonzero on failure
run_ps() {
    local sf="$1" to="$2" mp="$3" label="$4" res st ec out err
    res="$(dispatch_one "$sf" "$to" "$mp")" || { echo "[ERROR] $label: dispatch failed" >&2; return 1; }
    st="$(echo "$res" | jq -r '.status')"; ec="$(echo "$res" | jq -r '.exit_code // "null"')"
    out="$(echo "$res" | jq -r '.stdout // ""')"; err="$(echo "$res" | jq -r '.stderr // ""')"
    echo "--- $label: status=$st exit=$ec ---"
    [ -n "$out" ] && printf '%s\n' "$out"
    [ -n "$err" ] && { echo "  [stderr]"; printf '%s\n' "$err" | head -30; }
    printf '%s' "$res" > "$WORK_DIR/last_result.json"
    [ "$st" = "completed" ] || return 1
    return 0
}

# upload_file <local> <remote_win_path>  (chunked base64, decode on endpoint)
upload_file() {
    local local_path="$1" remote="$2" b64="$WORK_DIR/up.b64" chunk_dir="$WORK_DIR/chunks"
    local remote_b64="${remote}.b64"
    [ -f "$local_path" ] || { echo "[ERROR] missing local file: $local_path" >&2; return 1; }
    rm -rf "$chunk_dir"; mkdir -p "$chunk_dir"
    if base64 -w0 "$local_path" > "$b64" 2>/dev/null; then :
    elif base64 -i "$local_path" > "$b64" 2>/dev/null; then :
    else base64 < "$local_path" | tr -d '\n' > "$b64"; fi
    split -b 24000 "$b64" "$chunk_dir/chunk_"
    local chunks idx=0 nch sf data
    chunks=$(ls -1 "$chunk_dir"/chunk_* | sort)
    nch=$(echo "$chunks" | wc -l | tr -d ' ')
    for ch in $chunks; do
        idx=$((idx+1)); data="$(cat "$ch")"; sf="$WORK_DIR/chunkcmd.ps1"
        if [ "$idx" -eq 1 ]; then
            cat > "$sf" <<PS
\$ErrorActionPreference='Stop'
\$d=Split-Path "$remote" -Parent
if(-not (Test-Path \$d)){New-Item -ItemType Directory -Path \$d -Force|Out-Null}
[System.IO.File]::WriteAllText("$remote_b64","$data")
Write-Output "CHUNK $idx OK"
PS
        else
            cat > "$sf" <<PS
\$ErrorActionPreference='Stop'
[System.IO.File]::AppendAllText("$remote_b64","$data")
Write-Output "CHUNK $idx OK"
PS
        fi
        local r; r="$(dispatch_one "$sf" 60)" || { echo "[ERROR] $(basename "$remote") chunk $idx dispatch failed" >&2; return 1; }
        [ "$(echo "$r" | jq -r '.status')" = "completed" ] || { echo "[ERROR] $(basename "$remote") chunk $idx failed" >&2; return 1; }
    done
    # decode
    local df="$WORK_DIR/decode.ps1"
    cat > "$df" <<PS
\$ErrorActionPreference='Stop'
\$b64=(Get-Content "$remote_b64" -Raw) -replace '\s',''
[System.IO.File]::WriteAllBytes("$remote",[System.Convert]::FromBase64String(\$b64))
Remove-Item "$remote_b64" -Force -ErrorAction SilentlyContinue
\$len=(Get-Item "$remote").Length
Write-Output ("WROTE $remote (\$len bytes)")
PS
    local r; r="$(dispatch_one "$df" 60)" || { echo "[ERROR] decode dispatch failed for $remote" >&2; return 1; }
    [ "$(echo "$r" | jq -r '.status')" = "completed" ] || { echo "[ERROR] decode failed for $remote: $(echo "$r"|jq -r '.stderr'|head -c160)" >&2; return 1; }
    echo "[OK] uploaded $(basename "$remote") ($nch chunk(s)) -> $(echo "$r"|jq -r '.stdout'|tr -d '\r')"
}

# ---------------------------------------------------------------------------
# DETACHED, NO-CAP execution. A scanner must never be killed by an RMM command
# timeout - large drives can scan for hours. So we launch GuruScan as a
# scheduled task with ExecutionTimeLimit=0 (unlimited); the launch command
# returns immediately, the scan runs to completion on its own, and we poll a
# disk done-marker with NO overall cap.
# ---------------------------------------------------------------------------
# gs_launch_detached "<invoke-guruscan-args>" <tag>
gs_launch_detached() {
    local gsargs="$1" tag="$2"
    local wl="$WORK_DIR/wrapper_$tag.ps1"
    cat > "$wl" <<PS
\$ErrorActionPreference='Continue'
\$marker='C:\\GuruScan\\_done_${tag}.txt'
\$log='C:\\GuruScan\\_log_${tag}.txt'
Remove-Item \$marker,\$log -Force -ErrorAction SilentlyContinue
try { & C:\\GuruScan\\Invoke-GuruScan.ps1 ${gsargs} *> \$log; \$rc=\$LASTEXITCODE }
catch { \$rc=-1; \$_ | Out-String | Add-Content \$log }
"DONE rc=\$rc at \$(Get-Date -Format o)" | Set-Content \$marker
PS
    upload_file "$wl" "C:\\GuruScan\\_detached_${tag}.ps1" || return 1
    local sf="$WORK_DIR/launch_$tag.ps1"
    cat > "$sf" <<PS
\$ErrorActionPreference='Stop'
\$tn='GuruScan-${tag}'
try{ Unregister-ScheduledTask -TaskName \$tn -Confirm:\$false -ErrorAction SilentlyContinue }catch{}
\$a=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\\GuruScan\\_detached_${tag}.ps1'
\$pr=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
\$st=New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName \$tn -Action \$a -Principal \$pr -Settings \$st -Force | Out-Null
Start-ScheduledTask -TaskName \$tn
Start-Sleep -Seconds 2
Write-Output ('LAUNCHED ' + \$tn + ' state=' + ((Get-ScheduledTask -TaskName \$tn).State))
PS
    run_ps "$sf" 90 24 "launch-$tag" || return 1
}

# gs_wait_detached <tag> <label>  -- polls for the done-marker; NO cap (24h safety net)
gs_wait_detached() {
    local tag="$1" label="$2" i=0 maxi=2880
    local sf="$WORK_DIR/poll_$tag.ps1"
    cat > "$sf" <<PS
\$ErrorActionPreference='Continue'
\$marker='C:\\GuruScan\\_done_${tag}.txt'
if(Test-Path \$marker){ Write-Output ('MARKER ' + ((Get-Content \$marker -Raw) -replace '\s+',' ').Trim()) }
else {
  \$act=@(); foreach(\$n in @('a2cmd','HitmanPro_x64','rkill')){ \$p=Get-Process -Name \$n -ErrorAction SilentlyContinue; if(\$p){ \$act += (\$n+' CPU='+[math]::Round((\$p|Select-Object -First 1).CPU,0)+'s') } }
  if(\$act.Count){ Write-Output ('RUNNING ' + (\$act -join ', ')) } else { Write-Output 'RUNNING (between scanners / starting)' }
}
PS
    echo "[INFO] waiting for '$label' to finish (NO cap; checking every 30s)..."
    while [ $i -lt $maxi ]; do
        run_ps "$sf" 30 8 "poll-$tag" >/dev/null 2>&1 || true
        local out; out="$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null | tr -d '\r')"
        if printf '%s' "$out" | grep -q '^MARKER '; then
            echo "[OK] '$label' finished -> $(printf '%s' "$out" | grep '^MARKER ')"
            return 0
        fi
        i=$((i+1))
        if [ $((i % 4)) -eq 0 ]; then echo "  [$label] $((i/2)) min elapsed: $(printf '%s' "$out" | grep -E '^(RUNNING|MARKER)' | head -1)"; fi
        sleep 30
    done
    echo "[WARN] '$label' exceeded 24h safety net"; return 1
}

# fetch the captured console log of a detached run (contains GURUSCAN_RESULT_JSON)
gs_fetch_detached_log() {
    local tag="$1"
    local sf="$WORK_DIR/getlog_$tag.ps1"
    cat > "$sf" <<PS
\$p='C:\\GuruScan\\_log_${tag}.txt'
if(Test-Path \$p){ Get-Content \$p -Raw } else { Write-Output 'NO-LOG' }
PS
    run_ps "$sf" 60 24 "fetchlog-$tag" >/dev/null 2>&1 || true
    jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null
}

# ===========================================================================
phase_prep() {
    echo ""; echo "=== PHASE: prep ==="
    # 1) upload module files
    local files="GuruScan.psm1 GuruScan.psd1 scanners.json Invoke-GuruScan.ps1 Invoke-ScannerCleanup.ps1 Download-Scanners.ps1"
    for f in $files; do
        upload_file "$GS_DIR/$f" "C:\\GuruScan\\$f" || { _logerr "upload failed" --context "file=$f host=$AGENT_HOST"; return 1; }
    done

    # 2) Defender exclusions for tool + downloads dirs (so Defender does not nuke
    #    the scanner EXEs before GuruScan's engines run).
    local sf="$WORK_DIR/defender.ps1"
    cat > "$sf" <<'PS'
$ErrorActionPreference='Continue'
foreach($p in @('C:\GuruScan','C:\GuruScan\downloads','C:\EmsisoftCmd')){
  try { Add-MpPreference -ExclusionPath $p -ErrorAction Stop; Write-Output "EXCLUDED $p" }
  catch { Write-Output ("EXCLUDE-SKIP " + $p + " : " + $_.Exception.Message) }
}
PS
    run_ps "$sf" 60 24 "defender-exclusions" || echo "[WARN] Defender exclusion step had issues (continuing)"

    # 3) download RKill + Emsisoft via the module's downloader
    local sf2="$WORK_DIR/dl.ps1"
    cat > "$sf2" <<'PS'
$ErrorActionPreference='Continue'
& C:\GuruScan\Download-Scanners.ps1
PS
    run_ps "$sf2" 600 130 "download-rkill-emsisoft" || echo "[WARN] scanner download reported issues (HitmanPro is expected MANUAL)"

    # 4) fetch HitmanPro directly to downloads\
    local sf3="$WORK_DIR/hmp.ps1"
    cat > "$sf3" <<PS
\$ErrorActionPreference='Continue'
\$ProgressPreference='SilentlyContinue'
\$dst='C:\\GuruScan\\downloads\\HitmanPro_x64.exe'
try {
  [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor 3072
  \$wc=New-Object System.Net.WebClient
  \$wc.Headers.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
  \$wc.DownloadFile('$HITMANPRO_URL',\$dst)
  \$len=(Get-Item \$dst).Length
  Write-Output ("HITMANPRO OK \$len bytes")
} catch { Write-Output ("HITMANPRO FAIL: " + \$_.Exception.Message) }
PS
    run_ps "$sf3" 300 70 "download-hitmanpro" || echo "[WARN] HitmanPro download dispatch issue"

    # 5) [OPTIONAL] seed EICAR into a Defender-excluded test folder (assembled on
    #    the endpoint). DISABLED unless SEED_EICAR=1 -- retained so detection can be
    #    smoke-tested as new scanners are added to the chain.
    if [ "$SEED_EICAR" = "1" ]; then
        local sfe="$WORK_DIR/eicar.ps1"
        cat > "$sfe" <<'PS'
$ErrorActionPreference='Continue'
$dir='C:\GuruScanTest'
if(-not (Test-Path $dir)){New-Item -ItemType Directory -Path $dir -Force|Out-Null}
try { Add-MpPreference -ExclusionPath $dir -ErrorAction Stop } catch {}
# Standard EICAR test signature, assembled from fragments so it is never stored
# contiguously anywhere except the on-disk test file.
$e = 'X5O!P%@AP[4\PZX54(P^)7CC)7}' + '$EICAR' + '-STANDARD-ANTIVIRUS-' + 'TEST-FILE!$H+H*'
$f = Join-Path $dir 'eicar_test.com'
Set-Content -Path $f -Value $e -Encoding ASCII -NoNewline
Start-Sleep -Seconds 2
if(Test-Path $f){
  $sz=(Get-Item $f).Length
  Write-Output ("EICAR seeded: $f ($sz bytes) - survived (Defender exclusion working)")
} else {
  Write-Output "EICAR MISSING after write - Defender (or other AV) grabbed it despite exclusion"
}
PS
        run_ps "$sfe" 60 24 "seed-eicar" || { _logerr "EICAR seed failed" --context "host=$AGENT_HOST"; return 1; }
    else
        echo "[INFO] EICAR seeding disabled (set SEED_EICAR=1 to enable)"
    fi

    # 6) readiness check - confirm all three scanner binaries + module present
    local sf5="$WORK_DIR/ready.ps1"
    cat > "$sf5" <<'PS'
$ErrorActionPreference='Continue'
$need=@{
  'RKill'     ='C:\GuruScan\downloads\rkill.exe';
  'Emsisoft'  ='C:\GuruScan\downloads\EmsisoftCommandlineScanner64.exe';
  'HitmanPro' ='C:\GuruScan\downloads\HitmanPro_x64.exe';
  'Module'    ='C:\GuruScan\GuruScan.psm1'
}
$ok=$true
foreach($k in $need.Keys){
  if(Test-Path $need[$k]){ Write-Output ("READY  {0,-10} {1} ({2} bytes)" -f $k,$need[$k],(Get-Item $need[$k]).Length) }
  else { Write-Output ("MISSING {0,-10} {1}" -f $k,$need[$k]); $ok=$false }
}
if($ok){Write-Output 'ALL-READY'}else{Write-Output 'NOT-READY'}
PS
    run_ps "$sf5" 60 24 "readiness" || return 1
    if grep -q 'ALL-READY' "$WORK_DIR/last_result.json" 2>/dev/null || \
       echo "$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null)" | grep -q 'ALL-READY'; then
        echo "[OK] prep complete - endpoint is READY for scan"
        post_alert "[RMM] GuruScan test: prepped $AGENT_HOST (module+3 scanners staged)"
    else
        echo "[WARN] prep finished but not all components READY - review output above before scanning"
    fi
}

# ===========================================================================
phase_scan() {
    echo ""; echo "=== PHASE: scan (clean mode, full chain, headless, DETACHED - NO CAP) ==="
    gs_launch_detached "-Headless" "scan" || { _logerr "detached scan launch failed" --context "host=$AGENT_HOST"; echo "[ERROR] launch failed"; return 1; }
    gs_wait_detached "scan" "full-scan" || true
    local out; out="$(gs_fetch_detached_log "scan")"
    echo ""
    printf '%s\n' "$out" | grep 'GURUSCAN_RESULT_JSON:' | sed 's/^.*GURUSCAN_RESULT_JSON://' | jq '.' 2>/dev/null \
        || echo "[WARN] no GURUSCAN_RESULT_JSON in detached log"
    post_alert "[RMM] GuruScan test: detached scan finished on $AGENT_HOST"
}

# ===========================================================================
phase_collect() {
    echo ""; echo "=== PHASE: collect (pull results.json + logs) ==="
    local stamp; stamp="$(date -u +%Y%m%dT%H%M%S)"
    local outdir="$RESULTS_ROOT/${AGENT_HOST}-${stamp}"
    mkdir -p "$outdir"

    # newest scan log dir + results.json + per-scanner logs, emitted as base64 blobs
    local sf="$WORK_DIR/collect.ps1"
    cat > "$sf" <<'PS'
$ErrorActionPreference='Continue'
$root='C:\ScanLogs'
if(-not (Test-Path $root)){ Write-Output 'NO-SCANLOGS'; return }
$dir=Get-ChildItem $root -Directory | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if(-not $dir){ Write-Output 'NO-SCAN-DIR'; return }
Write-Output ("SCANDIR=" + $dir.FullName)
# EICAR sanity: is the planted test file still on disk after a clean-mode scan?
$eicar='C:\GuruScanTest\eicar_test.com'
if(Test-Path $eicar){ Write-Output ("EICAR-STILL-PRESENT (" + (Get-Item $eicar).Length + " bytes) - NOT quarantined") }
else { Write-Output 'EICAR-GONE - a scanner removed/quarantined it' }
# whitelist that was handed to the engines
if(Test-Path 'C:\GuruScan\whitelist.txt'){ Write-Output '===FILE===whitelist.txt==='; Write-Output ([Convert]::ToBase64String([IO.File]::ReadAllBytes('C:\GuruScan\whitelist.txt'))) }
# recurse: pull every log file under the scan dir (logs live in *_Logs subdirs), cap each at 512KB
Get-ChildItem $dir.FullName -File -Recurse | ForEach-Object {
  $bytes=[IO.File]::ReadAllBytes($_.FullName)
  if($bytes.Length -gt 524288){ $bytes=$bytes[0..524287] }
  $rel=$_.FullName.Substring($dir.FullName.Length).TrimStart('\') -replace '\\','__'
  Write-Output ("===FILE===" + $rel + "===")
  Write-Output ([Convert]::ToBase64String($bytes))
}
Write-Output "===END==="
PS
    run_ps "$sf" 180 72 "collect-logs" || { echo "[ERROR] collect failed"; return 1; }
    local out; out="$(jq -r '.stdout' "$WORK_DIR/last_result.json")"
    printf '%s' "$out" > "$WORK_DIR/collect.out"

    # split the marker-delimited base64 blobs into files
    awk -v outdir="$WORK_DIR/blobs" '
        BEGIN{ system("mkdir -p \"" outdir "\""); name="" }
        /^===FILE===/ { n=$0; sub(/^===FILE===/,"",n); sub(/===$/,"",n); name=n; next }
        /^===END===/ { name=""; next }
        name!="" { print > (outdir "/" name ".b64") }
    ' "$WORK_DIR/collect.out"

    if [ -d "$WORK_DIR/blobs" ]; then
        for bf in "$WORK_DIR/blobs"/*.b64; do
            [ -e "$bf" ] || continue
            local name; name="$(basename "$bf" .b64)"
            tr -d '\r\n' < "$bf" | base64 -d > "$outdir/$name" 2>/dev/null \
                && echo "[OK] pulled $name ($(wc -c < "$outdir/$name") bytes)" \
                || echo "[WARN] could not decode $name"
        done
    fi

    # save raw stdout (incl. SCANDIR line) too
    printf '%s\n' "$out" | grep -E '^(SCANDIR=|NO-)' > "$outdir/_collect-meta.txt" 2>/dev/null || true

    echo ""
    if [ -f "$outdir/results.json" ]; then
        echo "=== results.json ==="
        jq '.' "$outdir/results.json" 2>/dev/null || cat "$outdir/results.json"
    else
        echo "[WARN] results.json not among pulled files - check $outdir"
    fi
    echo ""
    echo "[OK] logs saved to: $outdir"
}

# ===========================================================================
# verify-each: re-seed a fresh EICAR before EACH engine and run that engine
# ALONE in clean mode, so the first engine's quarantine can't mask the others.
# Reports a per-engine detect+remove matrix. RKill is run but is a process
# killer (not a file scanner), so it is expected NOT to touch the file.
# Seeds in several locations to give targeted scanners (HitmanPro) a fair shot.
# ===========================================================================
VE_DIRS=('C:\GuruScanTest' 'C:\Users\Public\Desktop' 'C:\Windows\Temp')
VE_FILES=('C:\GuruScanTest\eicar_test.com' 'C:\Users\Public\Desktop\eicar_test.com' 'C:\Windows\Temp\eicar_test.com')

ve_seed_ps() {  # emits PS that (re)creates a fresh EICAR in every VE_DIRS location
    cat <<'PS'
$ErrorActionPreference='Continue'
$e='X5O!P%@AP[4\PZX54(P^)7CC)7}' + '$EICAR' + '-STANDARD-ANTIVIRUS-' + 'TEST-FILE!$H+H*'
foreach($d in @('C:\GuruScanTest','C:\Users\Public\Desktop','C:\Windows\Temp')){
  if(-not (Test-Path $d)){ New-Item -ItemType Directory -Path $d -Force | Out-Null }
  Set-Content -Path (Join-Path $d 'eicar_test.com') -Value $e -Encoding ASCII -NoNewline
}
$n=@(Get-ChildItem 'C:\GuruScanTest\eicar_test.com','C:\Users\Public\Desktop\eicar_test.com','C:\Windows\Temp\eicar_test.com' -ErrorAction SilentlyContinue).Count
Write-Output ("SEEDED $n/3 EICAR copies")
PS
}

phase_verify_each() {
    echo ""; echo "=== PHASE: verify-each (per-engine re-seed, clean mode) ==="

    # Defender exclusions for all seed locations (so only GuruScan's engines act)
    local sfx="$WORK_DIR/ve_excl.ps1"
    cat > "$sfx" <<'PS'
$ErrorActionPreference='Continue'
foreach($p in @('C:\GuruScanTest','C:\Users\Public\Desktop','C:\Windows\Temp','C:\GuruScan','C:\GuruScan\downloads','C:\EmsisoftCmd')){
  try{ Add-MpPreference -ExclusionPath $p -ErrorAction Stop; Write-Output "EXCLUDED $p" }catch{ Write-Output ("EXCL-SKIP "+$p) }
}
PS
    run_ps "$sfx" 60 24 "ve-defender-exclusions" || echo "[WARN] exclusion step issues"

    local engines="RKill HitmanPro Emsisoft"
    local matrix=""
    for eng in $engines; do
        echo ""; echo "--- verify engine: $eng ---"
        # 1) re-seed fresh EICAR everywhere
        ve_seed_ps > "$WORK_DIR/ve_seed.ps1"
        run_ps "$WORK_DIR/ve_seed.ps1" 60 24 "seed-for-$eng" || { echo "[ERROR] seed failed for $eng"; return 1; }

        # 2) run ONLY this engine in clean mode, DETACHED with NO cap (Emsisoft
        #    updates+scans all of C:\ - can take far longer than any RMM timeout)
        gs_launch_detached "-Scanners $eng -Headless" "ve_$eng" || { echo "[ERROR] launch failed for $eng"; return 1; }
        gs_wait_detached "ve_$eng" "run-$eng" || true

        # 3) check which seeded copies survived + read that run's result json
        cat > "$WORK_DIR/ve_check.ps1" <<'PS'
$ErrorActionPreference='Continue'
foreach($f in @('C:\GuruScanTest\eicar_test.com','C:\Users\Public\Desktop\eicar_test.com','C:\Windows\Temp\eicar_test.com')){
  if(Test-Path $f){ Write-Output ("PRESENT $f") } else { Write-Output ("GONE $f") }
}
$d=Get-ChildItem C:\ScanLogs -Directory -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if($d){
  try{ $r=Get-Content (Join-Path $d.FullName 'results.json') -Raw | ConvertFrom-Json
    Write-Output ("RESULT total_threats=" + $r.total_threats + " | " + (($r.scanners | ForEach-Object { $_.name + ' threats=' + $_.threats_found + ' exit=' + $_.exit_code }) -join ' ; ')) }catch{}
}
PS
        run_ps "$WORK_DIR/ve_check.ps1" 60 24 "check-$eng" || true
        local out gone present
        out="$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null)"
        gone="$(printf '%s' "$out" | grep -c '^GONE ')"
        present="$(printf '%s' "$out" | grep -c '^PRESENT ')"
        local verdict
        if [ "$eng" = "RKill" ]; then verdict="n/a (process killer, not a file scanner)"
        elif [ "$gone" -gt 0 ]; then verdict="DETECTED+REMOVED ($gone/3 copies removed)"
        else verdict="MISSED (0/3 copies removed)"; fi
        matrix="${matrix}\n  ${eng}: ${verdict}"
        echo "  -> $eng: $verdict"
    done

    # cleanup: remove seeded files + the exclusions we added
    cat > "$WORK_DIR/ve_clean.ps1" <<'PS'
$ErrorActionPreference='Continue'
foreach($f in @('C:\GuruScanTest\eicar_test.com','C:\Users\Public\Desktop\eicar_test.com','C:\Windows\Temp\eicar_test.com')){ Remove-Item $f -Force -ErrorAction SilentlyContinue }
Remove-Item 'C:\GuruScanTest' -Recurse -Force -ErrorAction SilentlyContinue
foreach($p in @('C:\GuruScanTest','C:\Users\Public\Desktop','C:\Windows\Temp','C:\GuruScan','C:\GuruScan\downloads','C:\EmsisoftCmd')){ try{ Remove-MpPreference -ExclusionPath $p -ErrorAction Stop }catch{} }
Write-Output ("REMAINING-EXCLUSIONS: " + (((Get-MpPreference).ExclusionPath) -join '; '))
PS
    run_ps "$WORK_DIR/ve_clean.ps1" 60 24 "ve-cleanup" || true

    echo ""
    echo "=========================================================="
    echo " VERIFY-EACH RESULT (per-engine, independent re-seed)"
    echo -e "$matrix"
    echo "=========================================================="
    post_alert "[RMM] GuruScan verify-each on $AGENT_HOST complete - see per-engine detect/remove matrix"
}

# ===========================================================================
# scan-one <Scanner>: fully automated single-scanner test. ONE command does it
# all, hands-off: restore the malware-lab samples, clear stale cleanup state,
# run the scanner DETACHED + NO-CAP via the same path production uses, then
# collect and report (detections, removal, results.json, reboot-cleanup task).
# Mirrors how we proved Emsisoft -- no manual stitching.
# ===========================================================================
phase_scan_one() {
    local scanner="${SCANNER_ARG:-HitmanPro}"
    echo ""; echo "=== PHASE: scan-one ($scanner) - automated, detached, no-cap ==="

    # Setup: free the mutex (kill any prior GuruScan run + scanner procs), clear
    # stale cleanup task/state, and restore the malware lab samples.
    local sf="$WORK_DIR/one_setup.ps1"
    cat > "$sf" <<'PS'
$ErrorActionPreference='Continue'
Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskName -like 'GuruScan-*' } | ForEach-Object { try{ Stop-ScheduledTask -TaskName $_.TaskName -EA SilentlyContinue }catch{}; try{ Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -EA SilentlyContinue }catch{} }
foreach($n in @('a2cmd','HitmanPro_x64','rkill','EmsisoftCommandlineScanner64')){ Get-Process -Name $n -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue }
Start-Sleep 3
Get-ScheduledTask -TaskName 'GuruRMM-ScannerCleanup' -EA SilentlyContinue | Unregister-ScheduledTask -Confirm:$false -EA SilentlyContinue
Remove-Item 'C:\GuruScan\cleanup-state.json' -Force -EA SilentlyContinue
# Re-ensure scanners exist (a prior reboot-cleanup may have wiped C:\GuruScan\downloads).
if(-not (Test-Path 'C:\GuruScan\downloads\EmsisoftCommandlineScanner64.exe') -or -not (Test-Path 'C:\GuruScan\downloads\rkill.exe')){
  & C:\GuruScan\Download-Scanners.ps1 *> $null
}
if(-not (Test-Path 'C:\GuruScan\downloads\HitmanPro_x64.exe')){
  try{ [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor 3072; (New-Object Net.WebClient).DownloadFile('https://dl.surfright.nl/HitmanPro_x64.exe','C:\GuruScan\downloads\HitmanPro_x64.exe') }catch{}
}
$zip = Get-ChildItem 'C:\Users\Owner\Downloads' -Filter '*.zip' -EA SilentlyContinue | Where-Object { $_.Name -match 'malware' } | Select-Object -First 1
if($zip){ Expand-Archive -Path $zip.FullName -DestinationPath 'C:\Users\Owner\Desktop' -Force -EA SilentlyContinue }
$n=(Get-ChildItem 'C:\Users\Owner\Desktop\malware-samples-master' -Recurse -File -EA SilentlyContinue).Count
Set-Content 'C:\GuruScan\_one_before.txt' $n
Write-Output ("setup done - scanners ensured; malware samples present: $n")
PS
    run_ps "$sf" 600 130 "setup" || { echo "[ERROR] setup failed"; return 1; }

    # Run the scanner the production way: detached scheduled task, unlimited time.
    gs_launch_detached "-Scanners $scanner -Headless" "one" || { echo "[ERROR] launch failed"; return 1; }
    gs_wait_detached "one" "$scanner" || true

    # Report the outcome (parsed from what GuruScan actually wrote).
    local rf="$WORK_DIR/one_report.ps1"
    cat > "$rf" <<'PS'
$ErrorActionPreference='Continue'
$before=Get-Content 'C:\GuruScan\_one_before.txt' -EA SilentlyContinue
$after=(Get-ChildItem 'C:\Users\Owner\Desktop\malware-samples-master' -Recurse -File -EA SilentlyContinue).Count
Write-Output ("samples: before=$before after=$after  REMOVED=" + ([int]$before-[int]$after))
$d=Get-ChildItem 'C:\ScanLogs' -Directory -EA SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if($d -and (Test-Path (Join-Path $d.FullName 'results.json'))){ $r=Get-Content (Join-Path $d.FullName 'results.json') -Raw|ConvertFrom-Json
  Write-Output ('results.json -> total_threats=' + $r.total_threats + ' reboot_required=' + $r.reboot_required)
  $r.scanners|ForEach-Object{ Write-Output ('  ' + $_.name + ': exit=' + $_.exit_code + ' threats=' + $_.threats_found) } }
$ct=Get-ScheduledTask -TaskName 'GuruRMM-ScannerCleanup' -EA SilentlyContinue
if($ct){ Write-Output ('reboot-cleanup task -> REGISTERED (state=' + $ct.State + ', logon-delay=' + $ct.Triggers[0].Delay + ')') } else { Write-Output 'reboot-cleanup task -> NOT registered' }
PS
    run_ps "$rf" 60 24 "report" || true
    post_alert "[RMM] GuruScan automated scan-one ($scanner) complete on $AGENT_HOST"
}

case "$PHASE" in
    prep)        phase_prep ;;
    scan)        phase_scan ;;
    scan-one)    phase_scan_one ;;
    collect)     phase_collect ;;
    verify-each) phase_verify_each ;;
    all)         phase_prep && phase_scan && phase_collect ;;
    *) echo "[ERROR] Unknown phase '$PHASE' (prep|scan|scan-one <Scanner>|collect|verify-each|all)" >&2; exit 1 ;;
esac
