- workspace: capmetro-monitor, github-notifications, model-selector - workspace-security: vt-monitor, monitor-unauthorized - workspace-home: cron-manager, monitor-unauthorized - extensions: vt-sentinel (VT-Sentinel plugin) Includes sync.sh for pull/push, README, AGENTS.md, .gitignore.
210 lines
6.7 KiB
Bash
Executable File
210 lines
6.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# Incremental unauthorized connection checker
|
|
# Reads the dedicated unauthorized-connections.log (populated by log-splitter.sh),
|
|
# compares against seen-connections.json and authorized-ips.json,
|
|
# and outputs structured JSON categorizing IPs as new or returning.
|
|
#
|
|
# Usage: bash check.sh
|
|
#
|
|
# Output JSON:
|
|
# {
|
|
# "timestamp": "2026-02-16T12:00:00Z",
|
|
# "new_ips": [ { "ip": "...", "first_seen": "...", "reason": "...", ... } ],
|
|
# "returning_ips": [ { "ip": "...", "new_attempts": 3, "latest": "...", ... } ],
|
|
# "total_events": 12,
|
|
# "whitelisted_skipped": 2
|
|
# }
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
|
|
UNAUTH_LOG="/tmp/openclaw/unauthorized-connections.log"
|
|
STATE_DIR="/home/node/.openclaw/workspace-security/memory"
|
|
OFFSET_FILE="$STATE_DIR/unauth-check-offset"
|
|
SEEN_FILE="$SKILL_DIR/seen-connections.json"
|
|
AUTH_FILE="$SKILL_DIR/authorized-ips.json"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Initialize state files if missing
|
|
[ ! -s "$SEEN_FILE" ] && echo '{}' > "$SEEN_FILE"
|
|
[ ! -s "$AUTH_FILE" ] && echo '{"whitelist":["127.0.0.1","::1","localhost"]}' > "$AUTH_FILE"
|
|
|
|
# Check log exists
|
|
if [ ! -f "$UNAUTH_LOG" ]; then
|
|
echo '{"timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","new_ips":[],"returning_ips":[],"total_events":0,"whitelisted_skipped":0}'
|
|
exit 0
|
|
fi
|
|
|
|
FILE_SIZE=$(stat -c%s "$UNAUTH_LOG")
|
|
|
|
# Read offset
|
|
LAST_OFFSET=0
|
|
if [ -f "$OFFSET_FILE" ]; then
|
|
LAST_OFFSET=$(cat "$OFFSET_FILE")
|
|
fi
|
|
|
|
# Handle log rotation / truncation
|
|
if [ "$LAST_OFFSET" -gt "$FILE_SIZE" ]; then
|
|
LAST_OFFSET=0
|
|
fi
|
|
|
|
BYTES_NEW=$((FILE_SIZE - LAST_OFFSET))
|
|
|
|
if [ "$BYTES_NEW" -le 0 ]; then
|
|
echo "$FILE_SIZE" > "$OFFSET_FILE"
|
|
echo '{"timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","new_ips":[],"returning_ips":[],"total_events":0,"whitelisted_skipped":0}'
|
|
exit 0
|
|
fi
|
|
|
|
# Load whitelist
|
|
WHITELIST=$(jq -r '.whitelist[]' "$AUTH_FILE" 2>/dev/null | tr '\n' '|' | sed 's/|$//')
|
|
|
|
is_whitelisted() {
|
|
local ip="$1"
|
|
[[ "$ip" == "127.0.0.1" || "$ip" == "::1" || "$ip" == "localhost" ]] && return 0
|
|
[ -n "$WHITELIST" ] && echo "$ip" | grep -qE "^($WHITELIST)$" && return 0
|
|
return 1
|
|
}
|
|
|
|
# Parse new log entries into per-IP aggregated data
|
|
TMPFILE=$(mktemp)
|
|
trap "rm -f $TMPFILE" EXIT
|
|
|
|
tail -c +"$((LAST_OFFSET + 1))" "$UNAUTH_LOG" > "$TMPFILE"
|
|
|
|
TOTAL_EVENTS=0
|
|
WHITELISTED=0
|
|
declare -A IP_EVENTS # ip -> JSON array of events
|
|
declare -A IP_LATEST # ip -> latest timestamp
|
|
declare -A IP_COUNT # ip -> count of events in this batch
|
|
|
|
while IFS= read -r line; do
|
|
[ -z "$line" ] && continue
|
|
TOTAL_EVENTS=$((TOTAL_EVENTS + 1))
|
|
|
|
# Extract fields from JSON log line
|
|
PARSED=$(echo "$line" | jq -r '
|
|
[
|
|
.time // "",
|
|
(.["1"].forwardedFor // ""),
|
|
(.["1"].authReason // .["1"].reason // .["1"].cause // ""),
|
|
(.["1"].origin // ""),
|
|
(.["1"].userAgent // ""),
|
|
((.["2"] // "") | tostring | (capture("remote=(?<r>[0-9.]+)") | .r) // "")
|
|
] | @tsv
|
|
' 2>/dev/null) || continue
|
|
|
|
IFS=$'\t' read -r ts ip reason origin ua remote <<< "$PARSED"
|
|
[ -z "$ip" ] && continue
|
|
|
|
if is_whitelisted "$ip"; then
|
|
WHITELISTED=$((WHITELISTED + 1))
|
|
continue
|
|
fi
|
|
|
|
# Build event JSON
|
|
EVENT=$(jq -cn \
|
|
--arg ts "$ts" \
|
|
--arg ip "$ip" \
|
|
--arg reason "$reason" \
|
|
--arg origin "$origin" \
|
|
--arg ua "$ua" \
|
|
--arg remote "$remote" \
|
|
'{timestamp:$ts, ip:$ip, reason:$reason, origin:$origin, user_agent:$ua, remote:$remote}')
|
|
|
|
# Aggregate by IP
|
|
if [ -z "${IP_EVENTS[$ip]+x}" ]; then
|
|
IP_EVENTS[$ip]="$EVENT"
|
|
IP_COUNT[$ip]=1
|
|
else
|
|
IP_EVENTS[$ip]="${IP_EVENTS[$ip]}"$'\n'"$EVENT"
|
|
IP_COUNT[$ip]=$(( ${IP_COUNT[$ip]} + 1 ))
|
|
fi
|
|
IP_LATEST[$ip]="$ts"
|
|
|
|
done < "$TMPFILE"
|
|
|
|
# Categorize IPs as new or returning
|
|
NEW_IPS="[]"
|
|
RETURNING_IPS="[]"
|
|
|
|
for ip in "${!IP_EVENTS[@]}"; do
|
|
COUNT=${IP_COUNT[$ip]}
|
|
LATEST=${IP_LATEST[$ip]}
|
|
|
|
# Get first event for details
|
|
FIRST_EVENT=$(echo "${IP_EVENTS[$ip]}" | head -1)
|
|
REASON=$(echo "$FIRST_EVENT" | jq -r '.reason')
|
|
ORIGIN=$(echo "$FIRST_EVENT" | jq -r '.origin')
|
|
UA=$(echo "$FIRST_EVENT" | jq -r '.user_agent')
|
|
REMOTE=$(echo "$FIRST_EVENT" | jq -r '.remote')
|
|
|
|
# Check if IP was previously seen
|
|
if jq -e --arg ip "$ip" 'has($ip)' "$SEEN_FILE" >/dev/null 2>&1; then
|
|
# Returning IP — update seen record, add to returning list
|
|
PREV_COUNT=$(jq -r --arg ip "$ip" '.[$ip].total_attempts // 0' "$SEEN_FILE")
|
|
NEW_TOTAL=$((PREV_COUNT + COUNT))
|
|
|
|
STMP=$(mktemp)
|
|
jq --arg ip "$ip" \
|
|
--arg latest "$LATEST" \
|
|
--argjson count "$NEW_TOTAL" \
|
|
--argjson batch "$COUNT" \
|
|
'.[$ip].last_seen = $latest | .[$ip].total_attempts = $count | .[$ip].attempts_this_batch = $batch' \
|
|
"$SEEN_FILE" > "$STMP" && mv "$STMP" "$SEEN_FILE"
|
|
|
|
RETURNING_IPS=$(echo "$RETURNING_IPS" | jq \
|
|
--arg ip "$ip" \
|
|
--arg latest "$LATEST" \
|
|
--argjson new_attempts "$COUNT" \
|
|
--argjson total "$NEW_TOTAL" \
|
|
--arg reason "$REASON" \
|
|
--arg origin "$ORIGIN" \
|
|
--arg ua "$UA" \
|
|
--arg remote "$REMOTE" \
|
|
'. += [{ip:$ip, latest:$latest, new_attempts:$new_attempts, total_attempts:$total, reason:$reason, origin:$origin, user_agent:$ua, remote:$remote}]')
|
|
else
|
|
# New IP — record it, add to new list
|
|
STMP=$(mktemp)
|
|
jq --arg ip "$ip" \
|
|
--arg ts "$LATEST" \
|
|
--arg reason "$REASON" \
|
|
--arg origin "$ORIGIN" \
|
|
--arg ua "$UA" \
|
|
--arg remote "$REMOTE" \
|
|
--argjson count "$COUNT" \
|
|
'. + {($ip): {first_seen: $ts, last_seen: $ts, reason: $reason, origin: $origin, user_agent: $ua, remote: $remote, total_attempts: $count}}' \
|
|
"$SEEN_FILE" > "$STMP" && mv "$STMP" "$SEEN_FILE"
|
|
|
|
NEW_IPS=$(echo "$NEW_IPS" | jq \
|
|
--arg ip "$ip" \
|
|
--arg first_seen "$LATEST" \
|
|
--argjson attempts "$COUNT" \
|
|
--arg reason "$REASON" \
|
|
--arg origin "$ORIGIN" \
|
|
--arg ua "$UA" \
|
|
--arg remote "$REMOTE" \
|
|
'. += [{ip:$ip, first_seen:$first_seen, attempts:$attempts, reason:$reason, origin:$origin, user_agent:$ua, remote:$remote}]')
|
|
fi
|
|
done
|
|
|
|
# Save new offset
|
|
echo "$FILE_SIZE" > "$OFFSET_FILE"
|
|
|
|
# Output structured result
|
|
jq -n \
|
|
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
--argjson new_ips "$NEW_IPS" \
|
|
--argjson returning_ips "$RETURNING_IPS" \
|
|
--argjson total "$TOTAL_EVENTS" \
|
|
--argjson whitelisted "$WHITELISTED" \
|
|
'{
|
|
timestamp: $ts,
|
|
new_ips: $new_ips,
|
|
returning_ips: $returning_ips,
|
|
total_events: $total,
|
|
whitelisted_skipped: $whitelisted
|
|
}'
|