#!/bin/bash # VT-Sentinel monitoring cron wrapper (incremental) # Checks both VT-Sentinel file activity AND plugin updates set -e SCRIPT_DIR="$(dirname "$0")" LOG_FILE="/tmp/openclaw/openclaw.log" STATE_DIR="/home/node/.openclaw/workspace-security/memory" SEEN_FILE="$STATE_DIR/seen-plugin-updates.txt" mkdir -p "$STATE_DIR" touch "$SEEN_FILE" OUTPUT="" # --- VT-Sentinel activity --- REPORT=$("$SCRIPT_DIR/check.sh" 2>/dev/null) TOTAL=$(echo "$REPORT" | jq '.totalEvents') ALERTS=$(echo "$REPORT" | jq '.alerts') ACTION_EVENTS=$(echo "$REPORT" | jq '[.summary.uploads, .summary.upload_complete, .summary.upload_failed, .summary.cache_hits, .summary.verdicts.clean, .summary.verdicts.malicious, .summary.verdicts.suspicious, .summary.quarantined, .summary.blocked] | add') if [ "$ACTION_EVENTS" -gt 0 ]; then OUTPUT+="šŸ›”ļø VT-Sentinel Activity Report\n===============================\n\n" OUTPUT+="šŸ“Š Summary: ${ACTION_EVENTS} file events\n" OUTPUT+=$(echo "$REPORT" | jq -r ' .summary | (if .uploads > 0 then " ā¬†ļø Uploads initiated: \(.uploads)" else empty end), (if .upload_complete > 0 then " āœ… Uploads completed: \(.upload_complete)" else empty end), (if .upload_failed > 0 then " āŒ Upload failures: \(.upload_failed)" else empty end), (if .cache_hits > 0 then " šŸ’¾ Cache hits: \(.cache_hits)" else empty end), (if .verdicts.clean > 0 then " āœ… Clean verdicts: \(.verdicts.clean)" else empty end), (if .verdicts.malicious > 0 then " 🚨 MALICIOUS: \(.verdicts.malicious)" else empty end), (if .verdicts.suspicious > 0 then " āš ļø SUSPICIOUS: \(.verdicts.suspicious)" else empty end), (if .quarantined > 0 then " šŸ“¦ Quarantined: \(.quarantined)" else empty end), (if .blocked > 0 then " 🚫 Blocked: \(.blocked)" else empty end) ') OUTPUT+="\n\nšŸ“‹ Event Details:\n" OUTPUT+=$(echo "$REPORT" | jq -r '.events[] | select(.category != "lifecycle" and .category != "config") | " [\(.timestamp)] \(.category): \(.message)"') if [ "$ALERTS" = "true" ]; then OUTPUT+="\n\nāš ļø SECURITY ALERT: Review malicious/suspicious/blocked events above!" fi fi # --- VT scan thread creation for new uploads --- PENDING_FILE="$STATE_DIR/pending-scans.json" [ ! -f "$PENDING_FILE" ] || [ ! -s "$PENDING_FILE" ] && echo '[]' > "$PENDING_FILE" SEEN_SCANS="$STATE_DIR/seen-scan-hashes.txt" touch "$SEEN_SCANS" UPLOADS=$(echo "$REPORT" | jq -r '[.events[] | select(.category == "upload")] | .[]' 2>/dev/null) COMPLETES=$(echo "$REPORT" | jq -r '[.events[] | select(.category == "upload_complete")] | .[].message' 2>/dev/null) if [ -n "$UPLOADS" ]; then # Extract filenames + risk categories from upload events echo "$REPORT" | jq -r '.events[] | select(.category == "upload") | .message' | while IFS= read -r msg; do RISK_CAT=$(echo "$msg" | grep -oP 'Unknown \K[A-Z_]+' || echo "UNKNOWN") FNAME=$(echo "$msg" | grep -oP 'file \K[^,]+' || echo "unknown") [ -z "$FNAME" ] || [ "$FNAME" = "unknown" ] && continue # Check if already tracked grep -qF "$FNAME" "$SEEN_SCANS" 2>/dev/null && continue # Find corresponding transaction ID from upload_complete events HASH="" if [ -n "$COMPLETES" ]; then # Get first unmatched transaction ID, decode to extract hash TXN_ID=$(echo "$COMPLETES" | grep -oP '\(\K[A-Za-z0-9+/=]+(?=\))' | head -1) if [ -n "$TXN_ID" ]; then HASH=$(echo "$TXN_ID" | base64 -d 2>/dev/null | cut -d: -f1) fi fi # Create thread RESULT=$("$SCRIPT_DIR/scan-thread.sh" "$FNAME" "$RISK_CAT" "${HASH:-pending}" 2>/dev/null) THREAD_ID=$(echo "$RESULT" | jq -r '.threadId // empty') if [ -n "$THREAD_ID" ] && [ -n "$HASH" ]; then # Add to pending scans PENDING=$(cat "$PENDING_FILE") PENDING=$(echo "$PENDING" | jq --arg h "$HASH" --arg f "$FNAME" --arg t "$THREAD_ID" --arg r "$RISK_CAT" \ '. += [{"hash":$h,"filename":$f,"threadId":$t,"riskCategory":$r}]') echo "$PENDING" > "$PENDING_FILE" OUTPUT+="\nšŸ›”ļø Created scan thread: [$RISK_CAT] $FNAME (hash: ${HASH:0:12}...)\n" fi echo "$FNAME" >> "$SEEN_SCANS" done fi # --- Follow up on pending scans --- PENDING_COUNT=$(jq 'length' "$PENDING_FILE" 2>/dev/null || echo 0) if [ "$PENDING_COUNT" -gt 0 ]; then FOLLOWUP=$("$SCRIPT_DIR/followup.sh" 2>/dev/null) if [ -n "$FOLLOWUP" ]; then OUTPUT+="\n$FOLLOWUP\n" fi fi # --- Plugin update check (creates Discord forum threads) --- # Filter: only plugins subsystem entries, strip ANSI + multibyte artifacts, deduplicate PLUGIN_UPDATES=$(tail -c 5000000 "$LOG_FILE" 2>/dev/null \ | grep '"subsystem.*plugins"' \ | grep -oP 'Update available: [^"\\]+' \ | sed 's/\x1b\[[0-9;]*m//g; s/\[3[0-9]m//g' \ | tr -d '\r' \ | sort -u) if [ -n "$PLUGIN_UPDATES" ]; then while IFS= read -r line; do [ -z "$line" ] && continue grep -qF "$line" "$SEEN_FILE" 2>/dev/null && continue # Parse: "Update available: 0.4.0 → 0.6.0. Run: openclaw plugins install plugin-name" OLD_VER=$(echo "$line" | grep -oP 'Update available: \K[0-9.]+') NEW_VER=$(echo "$line" | grep -oP '→ \K[0-9.]+') INSTALL_CMD=$(echo "$line" | grep -oP 'Run: \K.*') PLUGIN_NAME=$(echo "$INSTALL_CMD" | grep -oP 'install \K\S+') if [ -n "$PLUGIN_NAME" ] && [ -n "$OLD_VER" ] && [ -n "$NEW_VER" ]; then RESULT=$("$SCRIPT_DIR/plugin-update-thread.sh" "$PLUGIN_NAME" "$OLD_VER" "$NEW_VER" "$INSTALL_CMD" 2>/dev/null) if echo "$RESULT" | jq -e '.ok == true' >/dev/null 2>&1; then OUTPUT+="\nšŸ“¦ Created thread for plugin update: ${PLUGIN_NAME} ${OLD_VER} → ${NEW_VER}\n" else OUTPUT+="\nšŸ“¦ Plugin update: ${PLUGIN_NAME} ${OLD_VER} → ${NEW_VER} (thread creation failed)\n" fi fi echo "$line" >> "$SEEN_FILE" done <<< "$PLUGIN_UPDATES" fi # --- Output --- if [ -z "$OUTPUT" ]; then echo "NO_REPLY" else echo -e "$OUTPUT" fi