#!/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, EICAR-seeded, clean mode), pulling # every log back for review. # # Phases: # prep - upload module to C:\GuruScan, Defender-exclude tool/test dirs, # download RKill+Emsisoft, fetch HitmanPro, seed EICAR, 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 # # Mirrors the RMM plumbing in run-onboarding-diagnostic.sh (vault auth -> JWT -> # chunked base64 upload -> dispatch -> poll). EICAR is the standard harmless AV # test file; it is assembled on the endpoint (never written to this host) and # dropped only into a Defender-excluded folder so GuruScan's own engines are what # detect it. set -u TARGET="${1:-}" PHASE="${2:-all}" if [ -z "$TARGET" ]; then echo "[ERROR] Usage: bash guruscan-agent-test.sh " >&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 -> 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