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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenClaw Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # 🛡️ OpenClaw Sentinel
2
+
3
+ OpenClaw agents run with elevated privileges on your machine — shell access, file operations, network connections. Sentinel continuously monitors for unauthorized access, suspicious processes, privilege escalation, and system anomalies, alerting you in real-time through any OpenClaw channel.
4
+
5
+ A security monitoring plugin for [OpenClaw](https://github.com/openclaw/openclaw), powered by [osquery](https://osquery.io).
6
+
7
+ ## What it does
8
+
9
+ Sentinel watches your machine for suspicious activity and alerts you in real-time:
10
+
11
+ - **🔍 Process monitoring** — unsigned binaries, privilege escalation, suspicious commands
12
+ - **🔐 SSH monitoring** — logins from unknown hosts, brute force attempts
13
+ - **🌐 Network monitoring** — new listening ports, unexpected services
14
+ - **📁 File integrity** — changes to critical system files, new persistence mechanisms (LaunchDaemons, cron)
15
+ - **🚨 Smart alerting** — learns your baseline (known hosts, ports) and only alerts on anomalies
16
+
17
+ ## Architecture
18
+
19
+ ```
20
+ osqueryd (root daemon)
21
+ ↓ writes JSON results
22
+ ~/.openclaw/sentinel/logs/osquery/osqueryd.results.log
23
+ ↓ tailed by
24
+ Sentinel watcher (fs.watch + poll fallback)
25
+ ↓ parsed results
26
+ Analyzer (detection rules)
27
+ ↓ high/critical events
28
+ OpenClaw → Signal/Slack/Telegram alert
29
+ ```
30
+
31
+ Sentinel **does not** run osqueryd itself (it requires root). You start osqueryd separately via `sudo` or `launchd`, and Sentinel tails its result logs.
32
+
33
+ ## Prerequisites
34
+
35
+ - **macOS** (Apple Silicon or Intel) or **Linux** (systemd-based)
36
+ - [osquery](https://osquery.io) installed
37
+ - [OpenClaw](https://github.com/openclaw/openclaw) running
38
+
39
+ ### Install osquery
40
+
41
+ **macOS (Homebrew):**
42
+ ```bash
43
+ brew install --cask osquery
44
+ ```
45
+
46
+ **macOS (manual):**
47
+ ```bash
48
+ # Download the official .pkg from https://osquery.io/downloads
49
+ ```
50
+
51
+ > **Note:** osquery needs **Full Disk Access** on macOS for the Endpoint Security framework. Grant it to `/opt/osquery/lib/osquery.app/Contents/MacOS/osqueryd` in System Settings → Privacy & Security → Full Disk Access.
52
+
53
+ **Linux (Debian/Ubuntu):**
54
+ ```bash
55
+ wget -qO - https://pkg.osquery.io/deb/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/osquery-archive-keyring.gpg
56
+ echo "deb [signed-by=/usr/share/keyrings/osquery-archive-keyring.gpg] https://pkg.osquery.io/deb deb main" | sudo tee /etc/apt/sources.list.d/osquery.list
57
+ sudo apt-get update && sudo apt-get install osquery
58
+ ```
59
+
60
+ **Linux (RHEL/CentOS):**
61
+ ```bash
62
+ curl -L https://pkg.osquery.io/rpm/GPG | sudo tee /etc/pki/rpm-gpg/RPM-GPG-KEY-osquery
63
+ sudo yum-config-manager --add-repo https://pkg.osquery.io/rpm/osquery-s3-rpm.repo
64
+ sudo yum install osquery
65
+ ```
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ openclaw plugins install /path/to/openclaw-sentinel
71
+ openclaw gateway restart
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ Add to your `~/.openclaw/openclaw.json` under `plugins.entries`:
77
+
78
+ ```json
79
+ {
80
+ "plugins": {
81
+ "entries": {
82
+ "sentinel": {
83
+ "enabled": true,
84
+ "config": {
85
+ "osqueryPath": "/opt/osquery/lib/osquery.app/Contents/MacOS/osqueryi",
86
+ "logPath": "~/.openclaw/sentinel",
87
+ "alertChannel": "signal",
88
+ "alertTo": "+1234567890",
89
+ "alertSeverity": "high"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### Config options
98
+
99
+ | Option | Type | Default | Description |
100
+ |--------|------|---------|-------------|
101
+ | `osqueryPath` | string | auto-detect | Path to `osqueryi` binary |
102
+ | `logPath` | string | `~/.openclaw/sentinel` | Directory for sentinel data and osquery logs |
103
+ | `alertChannel` | string | — | Channel for alerts (`signal`, `slack`, `telegram`, etc.) |
104
+ | `alertTo` | string | — | Alert target (phone number, channel ID, etc.) |
105
+ | `alertSeverity` | string | `high` | Minimum severity to alert: `critical`, `high`, `medium`, `low`, `info` |
106
+ | `trustedSigningIds` | string[] | `[]` | Code signing IDs to skip (e.g. `com.apple`) |
107
+ | `trustedPaths` | string[] | `[]` | Binary paths to skip (e.g. `/usr/bin`, `/opt/homebrew/bin`) |
108
+ | `watchPaths` | string[] | `[]` | File paths to monitor for integrity changes |
109
+ | `enableProcessMonitor` | boolean | `true` | Monitor process execution events |
110
+ | `enableFileIntegrity` | boolean | `true` | Monitor file integrity events |
111
+ | `enableNetworkMonitor` | boolean | `true` | Monitor network connections |
112
+ | `pollIntervalMs` | number | `30000` | Fallback poll interval (ms) if fs.watch misses events |
113
+
114
+ ## Starting osqueryd
115
+
116
+ Sentinel watches osqueryd's output — you need to start osqueryd separately. The included setup script handles everything.
117
+
118
+ ### Automated setup (recommended)
119
+
120
+ ```bash
121
+ sudo ./scripts/setup-daemon.sh
122
+ ```
123
+
124
+ The script auto-detects your OS and will:
125
+ 1. Find your osqueryd binary
126
+ 2. Create the sentinel directory structure (`~/.openclaw/sentinel/`)
127
+ 3. Generate a default osquery config if none exists
128
+ 4. Install a system daemon:
129
+ - **macOS**: LaunchDaemon (`/Library/LaunchDaemons/com.openclaw.osqueryd.plist`)
130
+ - **Linux**: systemd unit (`/etc/systemd/system/openclaw-osqueryd.service`)
131
+ 5. Start osqueryd — auto-starts on boot and restarts on crash
132
+
133
+ ```bash
134
+ # macOS
135
+ sudo launchctl list com.openclaw.osqueryd
136
+
137
+ # Linux
138
+ sudo systemctl status openclaw-osqueryd
139
+
140
+ # Uninstall (both)
141
+ sudo ./scripts/setup-daemon.sh --uninstall
142
+ ```
143
+
144
+ ### Manual start (for testing)
145
+
146
+ ```bash
147
+ SENTINEL_DIR=~/.openclaw/sentinel
148
+
149
+ sudo osqueryd \
150
+ --config_path=$SENTINEL_DIR/config/osquery.conf \
151
+ --database_path=$SENTINEL_DIR/db \
152
+ --logger_path=$SENTINEL_DIR/logs/osquery \
153
+ --pidfile=$SENTINEL_DIR/osqueryd.pid \
154
+ --logger_plugin=filesystem \
155
+ --disable_events=false \
156
+ --events_expiry=3600 \
157
+ --daemonize \
158
+ --force
159
+ ```
160
+
161
+ ### Full Disk Access
162
+
163
+ For Endpoint Security framework support (process events, file events), grant Full Disk Access:
164
+
165
+ **System Settings → Privacy & Security → Full Disk Access → Add osqueryd**
166
+
167
+ The path is typically `/opt/osquery/lib/osquery.app/Contents/MacOS/osqueryd`.
168
+
169
+ ## Agent tools
170
+
171
+ Sentinel registers three tools your OpenClaw agent can use:
172
+
173
+ ### `sentinel_status`
174
+
175
+ Get monitoring status — daemon state, event counts, known baseline.
176
+
177
+ ### `sentinel_query`
178
+
179
+ Run ad-hoc osquery SQL for security investigation:
180
+
181
+ ```
182
+ "Show me all listening ports"
183
+ → sentinel_query: SELECT * FROM listening_ports WHERE port > 0;
184
+
185
+ "What processes are running as root?"
186
+ → sentinel_query: SELECT name, path, cmdline FROM processes WHERE uid = 0;
187
+
188
+ "Any SSH keys on this machine?"
189
+ → sentinel_query: SELECT * FROM user_ssh_keys;
190
+ ```
191
+
192
+ ### `sentinel_events`
193
+
194
+ Get recent security events, filterable by severity or category:
195
+
196
+ ```
197
+ "Show me critical events"
198
+ → sentinel_events: { severity: "critical" }
199
+
200
+ "Any SSH-related events?"
201
+ → sentinel_events: { category: "ssh_login" }
202
+ ```
203
+
204
+ ## Usage examples
205
+
206
+ Just ask your agent in natural language through any OpenClaw channel (Signal, Slack, Discord, etc.):
207
+
208
+ **System overview:**
209
+ > "How's my machine looking security-wise?"
210
+ > "Any security alerts today?"
211
+ > "What's the sentinel status?"
212
+
213
+ **Network investigation:**
214
+ > "What ports are open on this machine?"
215
+ > "Show me all outbound connections"
216
+ > "Is anything phoning home to an IP I don't recognize?"
217
+ > "What's listening on port 5432?"
218
+
219
+ **Process investigation:**
220
+ > "What's running as root right now?"
221
+ > "Any unsigned binaries running?"
222
+ > "Show me recently started processes"
223
+ > "What launched in the last hour?"
224
+
225
+ **SSH & access:**
226
+ > "Who's logged into this machine?"
227
+ > "Any failed SSH attempts?"
228
+ > "Has anyone tried to brute force SSH?"
229
+ > "Show me all SSH keys on the system"
230
+
231
+ **Persistence & malware hunting:**
232
+ > "Are there any new LaunchDaemons I should know about?"
233
+ > "Show me all cron jobs"
234
+ > "Any changes to /etc/hosts or sudoers?"
235
+ > "What browser extensions are installed?"
236
+
237
+ **Forensics:**
238
+ > "What happened on this machine between 2am and 5am?"
239
+ > "Show me all shell history with sudo commands"
240
+ > "Which processes have the most open file descriptors?"
241
+ > "What DNS queries were made in the last hour?"
242
+
243
+ The agent translates these into osquery SQL, runs them through `sentinel_query`, and explains the results in plain English.
244
+
245
+ ## Detection rules
246
+
247
+ | Category | Severity | Trigger |
248
+ |----------|----------|---------|
249
+ | Unsigned binary | high | Process executed without valid code signature |
250
+ | Privilege escalation | critical | `sudo`, `su`, `doas` with unexpected targets |
251
+ | Suspicious command | high | `curl \| sh`, `base64 -d`, `nc -l`, reverse shells |
252
+ | Unknown SSH login | high | SSH from IP not in baseline |
253
+ | SSH brute force | critical | 5+ failed auth attempts in short window |
254
+ | New listening port | medium | Port not seen during baseline scan |
255
+ | File integrity | high | Changes to watched paths |
256
+ | Persistence | high | New LaunchDaemon, LaunchAgent, or cron entry |
257
+
258
+ ## How baseline works
259
+
260
+ On startup, Sentinel snapshots:
261
+ - All currently logged-in remote hosts → **known hosts**
262
+ - All currently listening ports → **known ports**
263
+
264
+ Future events are compared against this baseline. Only anomalies trigger alerts. The baseline refreshes each time the gateway restarts.
265
+
266
+ ## Example alerts
267
+
268
+ ```
269
+ 🚨 SECURITY ALERT
270
+ Severity: HIGH
271
+ Category: ssh_login
272
+ Time: 2026-02-21 10:15:00
273
+
274
+ Unknown SSH login from 203.0.113.42
275
+ User: root | TTY: ttys003
276
+
277
+ This host is not in the known baseline.
278
+ ```
279
+
280
+ ```
281
+ 🔴 SECURITY ALERT
282
+ Severity: CRITICAL
283
+ Category: privilege_escalation
284
+ Time: 2026-02-21 14:30:00
285
+
286
+ Privilege escalation detected
287
+ User: www → root | PID: 54321
288
+ Command: sudo /bin/bash
289
+ ```
290
+
291
+ ## Development
292
+
293
+ ```bash
294
+ git clone https://github.com/sunil-sadasivan/openclaw-sentinel.git
295
+ cd openclaw-sentinel
296
+ npm install
297
+ npm run build # Compile TypeScript
298
+ npm run dev # Watch mode
299
+
300
+ # Install locally for testing
301
+ openclaw plugins install .
302
+ openclaw gateway restart
303
+ ```
304
+
305
+ ## Project structure
306
+
307
+ ```
308
+ src/
309
+ ├── index.ts # Plugin entry point — tool registration, watcher startup
310
+ ├── config.ts # SentinelConfig interface, defaults, SecurityEvent types
311
+ ├── osquery.ts # osquery binary discovery, SQL execution, config generation
312
+ ├── analyzer.ts # Detection rules — processes, SSH, ports, files, persistence
313
+ └── watcher.ts # Event-driven log tailer (fs.watch + poll fallback)
314
+ ```
315
+
316
+ ## License
317
+
318
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { shouldAlert, meetsThreshold, createAlertState } from "../alerts.js";
4
+ function makeEvent(title, severity = "high") {
5
+ return {
6
+ id: "test",
7
+ timestamp: Date.now(),
8
+ severity,
9
+ category: "process",
10
+ title,
11
+ description: "test",
12
+ details: {},
13
+ hostname: "test",
14
+ };
15
+ }
16
+ describe("shouldAlert", () => {
17
+ it("allows first alert", () => {
18
+ const state = createAlertState();
19
+ assert.equal(shouldAlert(makeEvent("Test"), state), true);
20
+ });
21
+ it("deduplicates same title within 5 minutes", () => {
22
+ const state = createAlertState();
23
+ const now = Date.now();
24
+ assert.equal(shouldAlert(makeEvent("Same Event"), state, now), true);
25
+ assert.equal(shouldAlert(makeEvent("Same Event"), state, now + 1000), false);
26
+ assert.equal(shouldAlert(makeEvent("Same Event"), state, now + 60_000), false);
27
+ assert.equal(shouldAlert(makeEvent("Same Event"), state, now + 299_000), false);
28
+ });
29
+ it("allows same title after 5 minute window (entries expire from rate limit window)", () => {
30
+ const state = createAlertState();
31
+ const now = Date.now();
32
+ shouldAlert(makeEvent("Recurring"), state, now);
33
+ // After 5+ minutes AND after the 1-min rate limit window cleans up
34
+ assert.equal(shouldAlert(makeEvent("Recurring"), state, now + 301_000), true);
35
+ });
36
+ it("allows different titles", () => {
37
+ const state = createAlertState();
38
+ const now = Date.now();
39
+ assert.equal(shouldAlert(makeEvent("Event A"), state, now), true);
40
+ assert.equal(shouldAlert(makeEvent("Event B"), state, now), true);
41
+ assert.equal(shouldAlert(makeEvent("Event C"), state, now), true);
42
+ });
43
+ it("rate limits at 10 per minute", () => {
44
+ const state = createAlertState();
45
+ const now = Date.now();
46
+ for (let i = 0; i < 10; i++) {
47
+ assert.equal(shouldAlert(makeEvent(`Event ${i}`), state, now), true);
48
+ }
49
+ // 11th should be blocked
50
+ assert.equal(shouldAlert(makeEvent("Event 10"), state, now), false);
51
+ });
52
+ it("allows alerts again after rate limit window expires", () => {
53
+ const state = createAlertState();
54
+ const now = Date.now();
55
+ for (let i = 0; i < 10; i++) {
56
+ shouldAlert(makeEvent(`Event ${i}`), state, now);
57
+ }
58
+ // After 1 minute, rate limit resets
59
+ assert.equal(shouldAlert(makeEvent("New Event"), state, now + 61_000), true);
60
+ });
61
+ });
62
+ describe("meetsThreshold", () => {
63
+ it("critical meets all thresholds", () => {
64
+ assert.equal(meetsThreshold("critical", "info"), true);
65
+ assert.equal(meetsThreshold("critical", "low"), true);
66
+ assert.equal(meetsThreshold("critical", "medium"), true);
67
+ assert.equal(meetsThreshold("critical", "high"), true);
68
+ assert.equal(meetsThreshold("critical", "critical"), true);
69
+ });
70
+ it("info only meets info threshold", () => {
71
+ assert.equal(meetsThreshold("info", "info"), true);
72
+ assert.equal(meetsThreshold("info", "low"), false);
73
+ assert.equal(meetsThreshold("info", "medium"), false);
74
+ assert.equal(meetsThreshold("info", "high"), false);
75
+ });
76
+ it("high meets high and below", () => {
77
+ assert.equal(meetsThreshold("high", "high"), true);
78
+ assert.equal(meetsThreshold("high", "medium"), true);
79
+ assert.equal(meetsThreshold("high", "critical"), false);
80
+ });
81
+ it("defaults to high when invalid severity given", () => {
82
+ assert.equal(meetsThreshold("critical", "invalid"), true);
83
+ assert.equal(meetsThreshold("high", "invalid"), true);
84
+ assert.equal(meetsThreshold("medium", "invalid"), false);
85
+ });
86
+ });
@@ -0,0 +1 @@
1
+ export {};