openclaw-sentinel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,371 @@
1
+ #!/bin/bash
2
+ #
3
+ # setup-daemon.sh — Install osqueryd as a system daemon for OpenClaw Sentinel
4
+ #
5
+ # Usage: sudo ./scripts/setup-daemon.sh [--uninstall]
6
+ #
7
+ # Supports macOS (launchd) and Linux (systemd).
8
+ # Starts osqueryd on boot with Sentinel's config.
9
+ #
10
+
11
+ set -euo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ REPO_DIR="$(dirname "$SCRIPT_DIR")"
15
+
16
+ # Colors
17
+ RED='\033[0;31m'
18
+ GREEN='\033[0;32m'
19
+ YELLOW='\033[1;33m'
20
+ NC='\033[0m'
21
+
22
+ info() { echo -e "${GREEN}✓${NC} $1"; }
23
+ warn() { echo -e "${YELLOW}⚠${NC} $1"; }
24
+ error() { echo -e "${RED}✗${NC} $1"; }
25
+
26
+ # ── Detect OS ──
27
+ OS="$(uname -s)"
28
+ case "$OS" in
29
+ Darwin) INIT_SYSTEM="launchd" ;;
30
+ Linux)
31
+ if command -v systemctl &>/dev/null; then
32
+ INIT_SYSTEM="systemd"
33
+ else
34
+ error "Linux detected but systemd not found. Only systemd is supported."
35
+ exit 1
36
+ fi
37
+ ;;
38
+ *)
39
+ error "Unsupported OS: $OS"
40
+ exit 1
41
+ ;;
42
+ esac
43
+
44
+ # ── Common: find osqueryd ──
45
+ find_osqueryd() {
46
+ local candidates=(
47
+ /opt/osquery/lib/osquery.app/Contents/MacOS/osqueryd # macOS .pkg
48
+ /usr/local/bin/osqueryd
49
+ /opt/homebrew/bin/osqueryd
50
+ /usr/bin/osqueryd
51
+ /usr/sbin/osqueryd
52
+ )
53
+ for candidate in "${candidates[@]}"; do
54
+ if [[ -x "$candidate" ]]; then
55
+ echo "$candidate"
56
+ return
57
+ fi
58
+ done
59
+ }
60
+
61
+ # ── Common: find user and sentinel dir ──
62
+ find_sentinel_dir() {
63
+ local real_user="${SUDO_USER:-$(logname 2>/dev/null || echo '')}"
64
+ if [[ -z "$real_user" ]]; then
65
+ error "Cannot determine the real user. Run with: sudo $0"
66
+ exit 1
67
+ fi
68
+
69
+ local real_home
70
+ if [[ "$OS" == "Darwin" ]]; then
71
+ real_home=$(dscl . -read "/Users/$real_user" NFSHomeDirectory | awk '{print $2}')
72
+ else
73
+ real_home=$(getent passwd "$real_user" | cut -d: -f6)
74
+ fi
75
+
76
+ echo "$real_user" "$real_home" "${real_home}/.openclaw/sentinel"
77
+ }
78
+
79
+ # ── Common: create dirs and config ──
80
+ setup_sentinel_dir() {
81
+ local sentinel_dir="$1"
82
+
83
+ mkdir -p "$sentinel_dir/config"
84
+ mkdir -p "$sentinel_dir/db"
85
+ mkdir -p "$sentinel_dir/logs/osquery"
86
+
87
+ local config_file="$sentinel_dir/config/osquery.conf"
88
+ if [[ ! -f "$config_file" ]]; then
89
+ warn "No osquery config found — generating default"
90
+
91
+ # Try to use the Node.js config generator (single source of truth)
92
+ if command -v node &>/dev/null && [[ -f "$REPO_DIR/dist/osquery.js" ]]; then
93
+ node -e "
94
+ import('file://$REPO_DIR/dist/osquery.js').then(m => {
95
+ const config = m.generateOsqueryConfig({});
96
+ process.stdout.write(JSON.stringify(config, null, 2));
97
+ });
98
+ " > "$config_file" 2>/dev/null && {
99
+ info "Generated $config_file (from plugin source)"
100
+ return
101
+ }
102
+ fi
103
+
104
+ # Fallback: inline config
105
+ cat > "$config_file" << 'OSQUERY_CONF'
106
+ {
107
+ "options": {
108
+ "logger_plugin": "filesystem",
109
+ "disable_events": "false",
110
+ "events_expiry": "3600",
111
+ "events_max": "100000"
112
+ },
113
+ "schedule": {
114
+ "process_events": {
115
+ "query": "SELECT pid, path, cmdline, uid, euid, username, signing_id, team_id, platform_binary, event_type, time FROM es_process_events WHERE event_type = 'exec';",
116
+ "interval": 30,
117
+ "description": "Process execution events from Endpoint Security"
118
+ },
119
+ "logged_in_users": {
120
+ "query": "SELECT type, user, host, time, pid FROM logged_in_users;",
121
+ "interval": 60,
122
+ "description": "Currently logged-in users"
123
+ },
124
+ "listening_ports": {
125
+ "query": "SELECT lp.port, lp.address, lp.protocol, p.name, p.path, p.cmdline FROM listening_ports lp JOIN processes p ON lp.pid = p.pid WHERE lp.port > 0;",
126
+ "interval": 120,
127
+ "description": "Listening network ports with process info"
128
+ },
129
+ "failed_auth": {
130
+ "query": "SELECT time, message FROM asl WHERE facility = 'auth' AND level <= 3 AND (message LIKE '%authentication error%' OR message LIKE '%Failed password%' OR message LIKE '%Invalid user%') ORDER BY time DESC LIMIT 50;",
131
+ "interval": 60,
132
+ "description": "Failed authentication attempts"
133
+ },
134
+ "launch_daemons": {
135
+ "query": "SELECT name, path, program, program_arguments, run_at_load FROM launchd WHERE path LIKE '/Library/LaunchDaemons/%' OR path LIKE '/Library/LaunchAgents/%';",
136
+ "interval": 300,
137
+ "description": "LaunchDaemons and LaunchAgents"
138
+ },
139
+ "shell_history": {
140
+ "query": "SELECT uid, command, time FROM shell_history WHERE command LIKE '%sudo%' OR command LIKE '%chmod%' OR command LIKE '%chown%' ORDER BY time DESC LIMIT 20;",
141
+ "interval": 60,
142
+ "description": "Shell commands involving privilege changes"
143
+ },
144
+ "ssh_keys": {
145
+ "query": "SELECT uid, path, encrypted FROM user_ssh_keys;",
146
+ "interval": 300,
147
+ "description": "SSH keys on the system"
148
+ },
149
+ "open_sockets": {
150
+ "query": "SELECT p.name, p.path, pos.remote_address, pos.remote_port, pos.local_port, pos.protocol FROM process_open_sockets pos JOIN processes p ON pos.pid = p.pid WHERE pos.remote_address != '' AND pos.remote_address != '127.0.0.1' AND pos.remote_address != '::1' AND pos.remote_address != '0.0.0.0' LIMIT 50;",
151
+ "interval": 120,
152
+ "description": "Outbound network connections"
153
+ }
154
+ },
155
+ "decorators": {
156
+ "load": ["SELECT hostname FROM system_info;"]
157
+ }
158
+ }
159
+ OSQUERY_CONF
160
+ info "Generated $config_file (inline fallback)"
161
+ fi
162
+ }
163
+
164
+ # ── Common: kill existing manual osqueryd ──
165
+ kill_existing() {
166
+ local sentinel_dir="$1"
167
+ if [[ -f "$sentinel_dir/osqueryd.pid" ]]; then
168
+ local old_pid
169
+ old_pid=$(cat "$sentinel_dir/osqueryd.pid" 2>/dev/null || echo "")
170
+ if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
171
+ warn "Killing existing osqueryd (pid $old_pid)..."
172
+ kill "$old_pid" 2>/dev/null || true
173
+ sleep 1
174
+ fi
175
+ fi
176
+ }
177
+
178
+ # ════════════════════════════════════════════
179
+ # macOS (launchd)
180
+ # ════════════════════════════════════════════
181
+
182
+ PLIST_NAME="com.openclaw.osqueryd"
183
+ PLIST_DEST="/Library/LaunchDaemons/${PLIST_NAME}.plist"
184
+ PLIST_TEMPLATE="${REPO_DIR}/launchd/${PLIST_NAME}.plist"
185
+
186
+ launchd_uninstall() {
187
+ echo "Uninstalling osqueryd daemon (launchd)..."
188
+ if launchctl list "$PLIST_NAME" &>/dev/null; then
189
+ launchctl unload "$PLIST_DEST" 2>/dev/null || true
190
+ info "Daemon stopped"
191
+ fi
192
+ if [[ -f "$PLIST_DEST" ]]; then
193
+ rm "$PLIST_DEST"
194
+ info "Removed $PLIST_DEST"
195
+ fi
196
+ echo "Done."
197
+ }
198
+
199
+ launchd_install() {
200
+ local osqueryd="$1" sentinel_dir="$2"
201
+
202
+ if [[ ! -f "$PLIST_TEMPLATE" ]]; then
203
+ error "Plist template not found: $PLIST_TEMPLATE"
204
+ exit 1
205
+ fi
206
+
207
+ # Stop existing
208
+ if launchctl list "$PLIST_NAME" &>/dev/null; then
209
+ warn "Stopping existing daemon..."
210
+ launchctl unload "$PLIST_DEST" 2>/dev/null || true
211
+ fi
212
+
213
+ # Install plist
214
+ sed \
215
+ -e "s|__OSQUERYD_PATH__|${osqueryd}|g" \
216
+ -e "s|__SENTINEL_DIR__|${sentinel_dir}|g" \
217
+ "$PLIST_TEMPLATE" > "$PLIST_DEST"
218
+
219
+ chmod 644 "$PLIST_DEST"
220
+ chown root:wheel "$PLIST_DEST"
221
+ info "Installed $PLIST_DEST"
222
+
223
+ # Load
224
+ launchctl load "$PLIST_DEST"
225
+ sleep 2
226
+
227
+ if launchctl list "$PLIST_NAME" &>/dev/null; then
228
+ info "osqueryd daemon is running (launchd)"
229
+ else
230
+ error "Daemon failed to start. Check: $sentinel_dir/logs/osqueryd-stderr.log"
231
+ exit 1
232
+ fi
233
+
234
+ echo ""
235
+ echo -e "${GREEN}Done!${NC} osqueryd is running as a launchd daemon."
236
+ echo ""
237
+ echo "Important: Grant Full Disk Access to osqueryd for Endpoint Security:"
238
+ echo " System Settings → Privacy & Security → Full Disk Access"
239
+ echo " Add: $osqueryd"
240
+ echo ""
241
+ echo "Commands:"
242
+ echo " Status: sudo launchctl list $PLIST_NAME"
243
+ echo " Stop: sudo launchctl unload $PLIST_DEST"
244
+ echo " Start: sudo launchctl load $PLIST_DEST"
245
+ echo " Uninstall: sudo $0 --uninstall"
246
+ echo " Logs: tail -f $sentinel_dir/logs/osquery/osqueryd.results.log"
247
+ }
248
+
249
+ # ════════════════════════════════════════════
250
+ # Linux (systemd)
251
+ # ════════════════════════════════════════════
252
+
253
+ SYSTEMD_UNIT="openclaw-osqueryd"
254
+ SYSTEMD_DEST="/etc/systemd/system/${SYSTEMD_UNIT}.service"
255
+ SYSTEMD_TEMPLATE="${REPO_DIR}/systemd/${SYSTEMD_UNIT}.service"
256
+
257
+ systemd_uninstall() {
258
+ echo "Uninstalling osqueryd daemon (systemd)..."
259
+ if systemctl is-active "$SYSTEMD_UNIT" &>/dev/null; then
260
+ systemctl stop "$SYSTEMD_UNIT"
261
+ info "Daemon stopped"
262
+ fi
263
+ if systemctl is-enabled "$SYSTEMD_UNIT" &>/dev/null; then
264
+ systemctl disable "$SYSTEMD_UNIT"
265
+ info "Daemon disabled"
266
+ fi
267
+ if [[ -f "$SYSTEMD_DEST" ]]; then
268
+ rm "$SYSTEMD_DEST"
269
+ systemctl daemon-reload
270
+ info "Removed $SYSTEMD_DEST"
271
+ fi
272
+ echo "Done."
273
+ }
274
+
275
+ systemd_install() {
276
+ local osqueryd="$1" sentinel_dir="$2"
277
+
278
+ if [[ ! -f "$SYSTEMD_TEMPLATE" ]]; then
279
+ error "Systemd template not found: $SYSTEMD_TEMPLATE"
280
+ exit 1
281
+ fi
282
+
283
+ # Stop existing
284
+ if systemctl is-active "$SYSTEMD_UNIT" &>/dev/null; then
285
+ warn "Stopping existing daemon..."
286
+ systemctl stop "$SYSTEMD_UNIT"
287
+ fi
288
+
289
+ # Install unit
290
+ sed \
291
+ -e "s|__OSQUERYD_PATH__|${osqueryd}|g" \
292
+ -e "s|__SENTINEL_DIR__|${sentinel_dir}|g" \
293
+ "$SYSTEMD_TEMPLATE" > "$SYSTEMD_DEST"
294
+
295
+ chmod 644 "$SYSTEMD_DEST"
296
+ info "Installed $SYSTEMD_DEST"
297
+
298
+ systemctl daemon-reload
299
+ systemctl enable "$SYSTEMD_UNIT"
300
+ systemctl start "$SYSTEMD_UNIT"
301
+
302
+ sleep 2
303
+
304
+ if systemctl is-active "$SYSTEMD_UNIT" &>/dev/null; then
305
+ info "osqueryd daemon is running (systemd)"
306
+ else
307
+ error "Daemon failed to start. Check: journalctl -u $SYSTEMD_UNIT"
308
+ exit 1
309
+ fi
310
+
311
+ echo ""
312
+ echo -e "${GREEN}Done!${NC} osqueryd is running as a systemd service."
313
+ echo ""
314
+ echo "Commands:"
315
+ echo " Status: sudo systemctl status $SYSTEMD_UNIT"
316
+ echo " Stop: sudo systemctl stop $SYSTEMD_UNIT"
317
+ echo " Start: sudo systemctl start $SYSTEMD_UNIT"
318
+ echo " Logs: journalctl -u $SYSTEMD_UNIT -f"
319
+ echo " Uninstall: sudo $0 --uninstall"
320
+ }
321
+
322
+ # ════════════════════════════════════════════
323
+ # Main
324
+ # ════════════════════════════════════════════
325
+
326
+ # Handle --uninstall
327
+ if [[ "${1:-}" == "--uninstall" ]]; then
328
+ if [[ $EUID -ne 0 ]]; then
329
+ error "This script must be run as root (sudo)"
330
+ exit 1
331
+ fi
332
+ case "$INIT_SYSTEM" in
333
+ launchd) launchd_uninstall ;;
334
+ systemd) systemd_uninstall ;;
335
+ esac
336
+ exit 0
337
+ fi
338
+
339
+ # Preflight
340
+ if [[ $EUID -ne 0 ]]; then
341
+ error "This script must be run as root (sudo)"
342
+ echo " Usage: sudo $0"
343
+ exit 1
344
+ fi
345
+
346
+ # Find osqueryd
347
+ OSQUERYD=$(find_osqueryd)
348
+ if [[ -z "$OSQUERYD" ]]; then
349
+ error "osqueryd not found."
350
+ echo " Install from: https://osquery.io/downloads"
351
+ echo " macOS: download the .pkg installer"
352
+ echo " Linux: see https://osquery.io/downloads/official"
353
+ exit 1
354
+ fi
355
+ info "Found osqueryd: $OSQUERYD"
356
+
357
+ # Find sentinel dir
358
+ read -r REAL_USER REAL_HOME SENTINEL_DIR <<< "$(find_sentinel_dir)"
359
+ info "User: $REAL_USER"
360
+ info "Sentinel dir: $SENTINEL_DIR"
361
+ info "Init system: $INIT_SYSTEM"
362
+
363
+ # Setup
364
+ setup_sentinel_dir "$SENTINEL_DIR"
365
+ kill_existing "$SENTINEL_DIR"
366
+
367
+ # Install
368
+ case "$INIT_SYSTEM" in
369
+ launchd) launchd_install "$OSQUERYD" "$SENTINEL_DIR" ;;
370
+ systemd) systemd_install "$OSQUERYD" "$SENTINEL_DIR" ;;
371
+ esac
@@ -0,0 +1,142 @@
1
+ # Sentinel — Endpoint Security Monitoring
2
+
3
+ You have access to real-time endpoint security monitoring via three tools powered by [osquery](https://osquery.io).
4
+
5
+ ## Tools
6
+
7
+ ### `sentinel_status`
8
+ Check if monitoring is active, how many events have been detected, and the current baseline (known hosts/ports). Call this first when investigating security concerns.
9
+
10
+ ### `sentinel_events`
11
+ Get recent security events. Filter by severity (`critical`, `high`, `medium`, `low`, `info`) or category (`process`, `network`, `file`, `auth`, `privilege`). Use this to review what Sentinel has flagged.
12
+
13
+ ### `sentinel_query`
14
+ Run ad-hoc osquery SQL for deeper investigation. osquery exposes 200+ virtual tables backed by live OS APIs — every query returns real-time system state, not cached data.
15
+
16
+ **Blocked tables** (security risk): `carves`, `curl`, `curl_certificate`
17
+
18
+ ## When to use these tools
19
+
20
+ - User asks about security, open ports, running processes, SSH connections, or system health
21
+ - During heartbeat security checks
22
+ - Investigating suspicious activity or alerts
23
+ - Auditing system configuration (firewall, SSH keys, launch daemons)
24
+
25
+ ## Common investigation queries
26
+
27
+ ### System overview
28
+ ```sql
29
+ -- Who's logged in right now?
30
+ SELECT type, user, host, time, pid FROM logged_in_users;
31
+
32
+ -- What's listening on the network?
33
+ SELECT lp.port, lp.protocol, lp.address, p.name, p.path
34
+ FROM listening_ports lp JOIN processes p ON lp.pid = p.pid
35
+ WHERE lp.port > 0 ORDER BY lp.port;
36
+
37
+ -- What processes are running as root?
38
+ SELECT pid, name, path, cmdline FROM processes WHERE uid = 0 ORDER BY start_time DESC LIMIT 30;
39
+ ```
40
+
41
+ ### SSH & authentication
42
+ ```sql
43
+ -- SSH keys on this machine
44
+ SELECT uid, path, encrypted FROM user_ssh_keys;
45
+
46
+ -- Authorized keys (who can SSH in?)
47
+ SELECT * FROM authorized_keys;
48
+
49
+ -- SSH config
50
+ SELECT * FROM ssh_configs;
51
+ ```
52
+
53
+ ### Persistence mechanisms
54
+ ```sql
55
+ -- Launch daemons and agents (macOS)
56
+ SELECT name, path, program, program_arguments, run_at_load
57
+ FROM launchd
58
+ WHERE path LIKE '/Library/LaunchDaemons/%' OR path LIKE '/Library/LaunchAgents/%'
59
+ OR path LIKE '%/Library/LaunchAgents/%';
60
+
61
+ -- Cron jobs
62
+ SELECT * FROM crontab;
63
+
64
+ -- Startup items (Linux)
65
+ SELECT name, path, source FROM startup_items;
66
+ ```
67
+
68
+ ### Process investigation
69
+ ```sql
70
+ -- Processes with open network connections
71
+ SELECT p.name, p.path, pos.remote_address, pos.remote_port, pos.local_port
72
+ FROM process_open_sockets pos JOIN processes p ON pos.pid = p.pid
73
+ WHERE pos.remote_address != '' AND pos.remote_address != '127.0.0.1'
74
+ AND pos.remote_address != '::1' AND pos.remote_address != '0.0.0.0'
75
+ ORDER BY p.name;
76
+
77
+ -- Find a specific process
78
+ SELECT pid, name, path, cmdline, uid, parent FROM processes WHERE name LIKE '%suspicious%';
79
+
80
+ -- Process tree (who spawned what)
81
+ SELECT p.pid, p.name, p.path, p.cmdline, pp.name AS parent_name
82
+ FROM processes p LEFT JOIN processes pp ON p.parent = pp.pid
83
+ WHERE p.uid = 0 ORDER BY p.start_time DESC LIMIT 20;
84
+ ```
85
+
86
+ ### File integrity & system config
87
+ ```sql
88
+ -- Check a specific file's hash
89
+ SELECT path, sha256 FROM hash WHERE path = '/etc/hosts';
90
+
91
+ -- Firewall status (macOS)
92
+ SELECT * FROM alf;
93
+ SELECT * FROM alf_exceptions;
94
+
95
+ -- Disk encryption
96
+ SELECT * FROM disk_encryption;
97
+
98
+ -- System info
99
+ SELECT hostname, cpu_type, hardware_model, physical_memory FROM system_info;
100
+ ```
101
+
102
+ ### Network investigation
103
+ ```sql
104
+ -- DNS resolvers
105
+ SELECT * FROM dns_resolvers;
106
+
107
+ -- ARP table (who's on the local network)
108
+ SELECT address, mac, interface FROM arp_cache;
109
+
110
+ -- Interfaces
111
+ SELECT interface, address, mask, type FROM interface_addresses WHERE address != '';
112
+
113
+ -- Routes
114
+ SELECT destination, gateway, interface FROM routes WHERE destination != '::1';
115
+ ```
116
+
117
+ ## Interpreting results
118
+
119
+ - **Unsigned binaries** from `/tmp`, `/var/tmp`, or user home dirs are suspicious
120
+ - **Root processes** you don't recognize warrant investigation
121
+ - **Listening ports** on `0.0.0.0` (all interfaces) are externally accessible — only expected for intentional services
122
+ - **SSH logins** from IPs outside your Tailscale range (`100.64.0.0/10`) or known hosts are flagged
123
+ - **New LaunchDaemons/LaunchAgents** could be persistence mechanisms
124
+ - **Failed auth attempts** — 3+ in a minute suggests targeted access attempts, 10+ suggests brute force
125
+
126
+ ## Alert severity levels
127
+
128
+ | Severity | Meaning | Examples |
129
+ |----------|---------|---------|
130
+ | 🚨 critical | Immediate threat | Brute force attack, critical file modified (/etc/sudoers) |
131
+ | 🔴 high | Likely malicious | Unsigned binary, privilege escalation, unknown SSH login, persistence change |
132
+ | 🟡 medium | Unusual activity | New listening port, single failed login, suspicious command |
133
+ | 🔵 low | Informational | Minor config changes |
134
+ | ℹ️ info | Baseline data | Normal system activity |
135
+
136
+ ## Tips
137
+
138
+ - Always check `sentinel_status` first to confirm monitoring is active
139
+ - Use `sentinel_events` before running ad-hoc queries — Sentinel may have already flagged the issue
140
+ - For recurring checks, suggest the user add items to their HEARTBEAT.md
141
+ - When reporting findings, distinguish between **confirmed threats** and **unusual but potentially benign** activity
142
+ - If osqueryd is not running, suggest: `sudo ./scripts/setup-daemon.sh` from the plugin directory
@@ -0,0 +1,27 @@
1
+ [Unit]
2
+ Description=osqueryd daemon for OpenClaw Sentinel
3
+ Documentation=https://github.com/sunil-sadasivan/openclaw-sentinel
4
+ After=network.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ ExecStart=__OSQUERYD_PATH__ \
9
+ --config_path=__SENTINEL_DIR__/config/osquery.conf \
10
+ --database_path=__SENTINEL_DIR__/db \
11
+ --logger_path=__SENTINEL_DIR__/logs/osquery \
12
+ --pidfile=__SENTINEL_DIR__/osqueryd.pid \
13
+ --logger_plugin=filesystem \
14
+ --disable_events=false \
15
+ --events_expiry=3600 \
16
+ --events_max=100000 \
17
+ --logger_rotate=true \
18
+ --logger_max_log_size=52428800 \
19
+ --force
20
+ Restart=on-failure
21
+ RestartSec=10
22
+ StandardOutput=journal
23
+ StandardError=journal
24
+ SyslogIdentifier=openclaw-osqueryd
25
+
26
+ [Install]
27
+ WantedBy=multi-user.target