#!/bin/bash # Follow up on pending VT scans — poll VT API, update Discord threads # Usage: followup.sh set -e STATE_DIR="/home/node/.openclaw/workspace-security/memory" PENDING_FILE="$STATE_DIR/pending-scans.json" CHANNEL_ID="1470849667737714851" # Initialize if missing if [ ! -f "$PENDING_FILE" ] || [ ! -s "$PENDING_FILE" ]; then echo '[]' > "$PENDING_FILE" exit 0 fi PENDING=$(cat "$PENDING_FILE") COUNT=$(echo "$PENDING" | jq 'length') [ "$COUNT" -eq 0 ] && exit 0 TOKEN=$(printenv DISCORD_BOT_TOKEN) VT_KEY=$(printenv VIRUSTOTAL_API_KEY) if [ -z "$TOKEN" ] || [ -z "$VT_KEY" ]; then echo "Missing DISCORD_BOT_TOKEN or VIRUSTOTAL_API_KEY" >&2 exit 1 fi UPDATED="[]" RESOLVED=0 for i in $(seq 0 $((COUNT - 1))); do ENTRY=$(echo "$PENDING" | jq ".[$i]") HASH=$(echo "$ENTRY" | jq -r '.hash') THREAD_ID=$(echo "$ENTRY" | jq -r '.threadId') FILENAME=$(echo "$ENTRY" | jq -r '.filename') RISK_CAT=$(echo "$ENTRY" | jq -r '.riskCategory') # Query VT API VT_RESULT=$(curl -s --max-time 10 -H "x-apikey: $VT_KEY" \ "https://www.virustotal.com/api/v3/files/$HASH" 2>/dev/null) STATS=$(echo "$VT_RESULT" | jq '.data.attributes.last_analysis_stats // empty' 2>/dev/null) if [ -z "$STATS" ] || [ "$STATS" = "null" ]; then # Still pending or not found — keep in queue UPDATED=$(echo "$UPDATED" | jq --argjson entry "$ENTRY" '. += [$entry]') continue fi # Extract verdict MALICIOUS=$(echo "$STATS" | jq '.malicious // 0') SUSPICIOUS=$(echo "$STATS" | jq '.suspicious // 0') UNDETECTED=$(echo "$STATS" | jq '.undetected // 0') TOTAL=$((MALICIOUS + SUSPICIOUS + UNDETECTED)) TYPE_DESC=$(echo "$VT_RESULT" | jq -r '.data.attributes.type_description // "Unknown"') SHA256=$(echo "$VT_RESULT" | jq -r '.data.attributes.sha256 // "unknown"') VT_LINK="https://www.virustotal.com/gui/file/$SHA256" # Determine verdict if [ "$MALICIOUS" -gt 0 ]; then VERDICT="🚨 MALICIOUS" EMOJI="🚨" VERDICT_SHORT="MALICIOUS ($MALICIOUS/$TOTAL)" elif [ "$SUSPICIOUS" -gt 0 ]; then VERDICT="āš ļø SUSPICIOUS" EMOJI="āš ļø" VERDICT_SHORT="SUSPICIOUS ($SUSPICIOUS/$TOTAL)" else VERDICT="āœ… CLEAN" EMOJI="āœ…" VERDICT_SHORT="CLEAN (0/$TOTAL)" fi # Post verdict to thread MSG=$(printf '%s **Analysis Complete — %s**\n\n**File:** `%s`\n**Type:** %s\n**SHA-256:** `%s`\n\n**Results:**\n• Malicious: %s\n• Suspicious: %s\n• Undetected: %s engines\n\n**VT Link:** %s\n\n**Verdict:** %s' \ "$EMOJI" "$VERDICT_SHORT" "$FILENAME" "$TYPE_DESC" "$SHA256" \ "$MALICIOUS" "$SUSPICIOUS" "$UNDETECTED" "$VT_LINK" "$VERDICT") curl -s -X POST \ -H "Authorization: Bot $TOKEN" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg content "$MSG" '{content: $content}')" \ "https://discord.com/api/v10/channels/$THREAD_ID/messages" > /dev/null # Update thread title NEW_TITLE=$(printf '[%s] %s — %s' "$RISK_CAT" "$FILENAME" "$VERDICT") curl -s -X PATCH \ -H "Authorization: Bot $TOKEN" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg name "$NEW_TITLE" '{name: $name}')" \ "https://discord.com/api/v10/channels/$THREAD_ID" > /dev/null RESOLVED=$((RESOLVED + 1)) echo "āœ… Resolved: $FILENAME → $VERDICT_SHORT" # VT rate limit: 4 req/min on free tier, be conservative sleep 1 done # Save remaining pending scans echo "$UPDATED" > "$PENDING_FILE" if [ "$RESOLVED" -gt 0 ]; then REMAINING=$(echo "$UPDATED" | jq 'length') echo "Resolved $RESOLVED scan(s), $REMAINING still pending" fi