From 5ede4fee266a5def4570712e49ea58a414f3fd04 Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Sun, 21 Jun 2026 10:43:16 -0700 Subject: [PATCH] sync: auto-sync from HOWARD-HOME at 2026-06-21 10:42:33 Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-21 10:42:33 --- .claude/scripts/guruscan-agent-test.sh | 368 ++++++++++++++++++ .claude/skills/bitdefender/scripts/gz.py | 201 +++++++++- .../skills/bitdefender/scripts/gz_client.py | 121 ++++++ .../skills/bitdefender/scripts/selftest.py | 19 + errorlog.md | 128 ++++++ 5 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 .claude/scripts/guruscan-agent-test.sh diff --git a/.claude/scripts/guruscan-agent-test.sh b/.claude/scripts/guruscan-agent-test.sh new file mode 100644 index 00000000..07907994 --- /dev/null +++ b/.claude/scripts/guruscan-agent-test.sh @@ -0,0 +1,368 @@ +#!/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