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:
2026-02-16 15:32:44 +00:00
commit 3b7d6bb67c
37 changed files with 3358 additions and 0 deletions

View 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
}'