Initial commit: custom OpenClaw skills from docker-test
- 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.
This commit is contained in:
209
workspace-security/monitor-unauthorized/scripts/check.sh
Executable file
209
workspace-security/monitor-unauthorized/scripts/check.sh
Executable file
@@ -0,0 +1,209 @@
|
||||
#!/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
|
||||
}'
|
||||
Reference in New Issue
Block a user