openclaw-sentinel 0.2.1 → 0.3.1

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.
@@ -25,4 +25,4 @@ export declare function analyzeFileEvents(rows: Record<string, string>[]): Secur
25
25
  /**
26
26
  * Format a security event for human-readable alerting.
27
27
  */
28
- export declare function formatAlert(evt: SecurityEvent): string;
28
+ export declare function formatAlert(evt: SecurityEvent, assessment?: string | null): string;
package/dist/analyzer.js CHANGED
@@ -48,11 +48,15 @@ export function analyzeProcessEvents(rows, config) {
48
48
  events.push(event("high", "privilege", "Privilege escalation detected", `Process escalated to root: ${path}\nUser: ${username} (uid=${uid}, euid=0)\nCommand: ${cmdline}`, { path, cmdline, uid, euid, username, signingId }));
49
49
  continue;
50
50
  }
51
- // MEDIUM: Suspicious command patterns
51
+ // Skip commands matching trusted patterns (e.g. OpenClaw heartbeat scripts)
52
+ const trustedCmdPatterns = (config.trustedCommandPatterns ?? []).map((p) => new RegExp(p, "i"));
53
+ if (trustedCmdPatterns.some((p) => p.test(cmdline)))
54
+ continue;
55
+ // Suspicious command patterns — only flag actually dangerous imports/tools
52
56
  const suspiciousPatterns = [
53
57
  /curl.*\|.*sh/i,
54
58
  /wget.*\|.*sh/i,
55
- /python.*-c.*import/i,
59
+ /python.*-c.*import\s+(os|subprocess|socket|pty|shutil|ctypes)/i,
56
60
  /base64.*decode/i,
57
61
  /nc\s+-l/i,
58
62
  /ncat.*-l/i,
@@ -61,7 +65,17 @@ export function analyzeProcessEvents(rows, config) {
61
65
  /mkfifo/i,
62
66
  ];
63
67
  if (suspiciousPatterns.some((p) => p.test(cmdline))) {
64
- events.push(event("high", "process", "Suspicious command detected", `Potentially malicious command: ${cmdline}\nProcess: ${path}\nUser: ${username}`, { path, cmdline, uid, username }));
68
+ // Detect likely OpenClaw-spawned commands: safe python one-liners,
69
+ // curl to known APIs, etc. run by the host user. Downgrade to low.
70
+ const safeAgentPatterns = [
71
+ /python.*-c.*import\s+(json|sys|csv|re|datetime|warnings|urllib|http|pathlib|hashlib|hmac|base64|time|math|collections|itertools|functools|textwrap|string|io|copy)/i,
72
+ /python.*-c.*from\s+(google|googleapiclient|oauth2client|service_account)/i,
73
+ /curl.*-[sS].*(-H\s+["']Authorization|api\.|sentry\.io|helpscout\.net|trusthub\.twilio|ngpvan\.com|github\.com|slack\.com)/i,
74
+ /bq\s+(query|ls|show|head|mk)/i,
75
+ /gh\s+(api|pr|issue|run)/i,
76
+ ];
77
+ const isLikelyAgent = safeAgentPatterns.some((p) => p.test(cmdline));
78
+ events.push(event(isLikelyAgent ? "low" : "high", "process", "Suspicious command detected", `Potentially malicious command: ${cmdline}\nProcess: ${path}\nUser: ${username}`, { path, cmdline, uid, username, likelyAgent: isLikelyAgent }));
65
79
  }
66
80
  }
67
81
  return events;
@@ -167,7 +181,7 @@ export function analyzeFileEvents(rows) {
167
181
  /**
168
182
  * Format a security event for human-readable alerting.
169
183
  */
170
- export function formatAlert(evt) {
184
+ export function formatAlert(evt, assessment) {
171
185
  const severityEmoji = {
172
186
  critical: "🚨",
173
187
  high: "🔴",
@@ -177,11 +191,15 @@ export function formatAlert(evt) {
177
191
  };
178
192
  const emoji = severityEmoji[evt.severity];
179
193
  const time = new Date(evt.timestamp).toLocaleTimeString();
180
- return [
194
+ const lines = [
181
195
  `${emoji} **SENTINEL: ${evt.title}**`,
182
196
  `Severity: ${evt.severity.toUpperCase()} | ${evt.category}`,
183
197
  `Host: ${evt.hostname} | ${time}`,
184
198
  "",
185
199
  evt.description,
186
- ].join("\n");
200
+ ];
201
+ if (assessment) {
202
+ lines.push("", `🦞 ${assessment}`);
203
+ }
204
+ return lines.join("\n");
187
205
  }
package/dist/config.d.ts CHANGED
@@ -17,7 +17,10 @@ export interface SentinelConfig {
17
17
  enableNetworkMonitor?: boolean;
18
18
  trustedSigningIds?: string[];
19
19
  trustedPaths?: string[];
20
+ trustedCommandPatterns?: string[];
20
21
  watchPaths?: string[];
22
+ /** Add a one-line LLM assessment to alerts before sending (requires openclaw CLI) */
23
+ llmAlertAssessment?: boolean;
21
24
  }
22
25
  export declare const DEFAULT_CONFIG: Required<Pick<SentinelConfig, "pollIntervalMs" | "enableProcessMonitor" | "enableFileIntegrity" | "enableNetworkMonitor" | "trustedSigningIds" | "trustedPaths" | "watchPaths">>;
23
26
  /** A security event detected by Sentinel */
package/dist/index.js CHANGED
@@ -93,6 +93,51 @@ async function checkDaemon(sentinelDir) {
93
93
  return false;
94
94
  }
95
95
  }
96
+ /**
97
+ * Use the LLM (via openclaw CLI) to generate a one-line human-readable
98
+ * assessment of a security event. Returns the assessment string, or null
99
+ * on failure.
100
+ */
101
+ async function llmAssessEvent(evt) {
102
+ const details = typeof evt.details === "string" ? evt.details : JSON.stringify(evt.details);
103
+ const prompt = `You are a security-savvy AI agent named Claw monitoring your human's machine. A security event was detected:
104
+
105
+ Title: ${evt.title}
106
+ Severity: ${evt.severity}
107
+ Category: ${evt.category}
108
+ Description: ${evt.description}
109
+ Details: ${details}
110
+
111
+ Context: This machine runs OpenClaw (an AI assistant platform) which frequently spawns commands via heartbeats, cron jobs, and agent tasks — python one-liners, curl/wget API calls, bq queries, git, npm/node, etc. The user is "sunil".
112
+
113
+ Reply with ONLY a single short sentence (under 30 words) giving your honest take on whether this is a real problem or likely benign. Be direct, opinionated, and useful — like a senior engineer glancing at an alert. No preamble.`;
114
+ try {
115
+ const { stdout } = await execFileAsync("openclaw", ["agent", "--agent", "main", "--message", prompt, "--json"], {
116
+ timeout: 30_000,
117
+ });
118
+ // Parse JSON response — openclaw agent --json returns { result: { payloads: [{ text: "..." }] } }
119
+ try {
120
+ // Skip any non-JSON prefix lines (e.g. "Config warnings:...")
121
+ const jsonStart = stdout.indexOf("{");
122
+ const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : stdout;
123
+ const parsed = JSON.parse(jsonStr.trim());
124
+ const text = parsed?.result?.payloads?.[0]?.text
125
+ ?? parsed?.message
126
+ ?? parsed?.text
127
+ ?? null;
128
+ return typeof text === "string" ? text.trim().slice(0, 200) : null;
129
+ }
130
+ catch {
131
+ // Fallback: treat raw stdout as the response
132
+ const clean = stdout.replace(/^Config warnings:.*\n?/gm, "").trim();
133
+ return clean.slice(0, 200) || null;
134
+ }
135
+ }
136
+ catch (err) {
137
+ console.warn(`[sentinel] LLM assessment failed: ${err?.message ?? err}`);
138
+ return null;
139
+ }
140
+ }
96
141
  /**
97
142
  * Handle an osquery result batch — route to appropriate analyzer.
98
143
  */
@@ -126,7 +171,21 @@ function handleResult(result, config, sendAlert) {
126
171
  if (suppressed) {
127
172
  console.log(`[sentinel] Alert suppressed by rule "${suppressed.reason}" (${SuppressionStore.describe(suppressed)})`);
128
173
  }
174
+ else if (config.llmAlertAssessment) {
175
+ // Get LLM assessment and include it in the alert
176
+ console.log(`[sentinel] LLM assessment enabled, calling for: ${evt.title}`);
177
+ llmAssessEvent(evt).then((assessment) => {
178
+ console.log(`[sentinel] LLM assessment result: ${assessment?.slice(0, 80) ?? "(null)"}`);
179
+ sendAlert(formatAlert(evt, assessment)).catch((err) => {
180
+ console.error("[sentinel] alert failed:", err);
181
+ });
182
+ }).catch((err) => {
183
+ console.warn(`[sentinel] LLM assessment promise rejected: ${err}`);
184
+ sendAlert(formatAlert(evt)).catch(() => { });
185
+ });
186
+ }
129
187
  else {
188
+ console.log(`[sentinel] LLM assessment NOT enabled (llmAlertAssessment=${config.llmAlertAssessment})`);
130
189
  sendAlert(formatAlert(evt)).catch((err) => {
131
190
  console.error("[sentinel] alert failed:", err);
132
191
  });
@@ -151,7 +210,7 @@ export default function sentinel(api) {
151
210
  }
152
211
  catch { /* ignore */ }
153
212
  const pluginConfig = { ...fileConfig, ...apiConfig };
154
- console.log(`[sentinel] Config: alertSeverity=${pluginConfig.alertSeverity}, alertChannel=${pluginConfig.alertChannel}`);
213
+ console.log(`[sentinel] Config v0.3.0: alertSeverity=${pluginConfig.alertSeverity}, alertChannel=${pluginConfig.alertChannel}, llmAssess=${pluginConfig.llmAlertAssessment}, trustedPatterns=${(pluginConfig.trustedCommandPatterns ?? []).length}`);
155
214
  let watcher = null;
156
215
  let logStreamWatcher = null;
157
216
  const sentinelDir = pluginConfig.logPath ?? SENTINEL_DIR_DEFAULT;
@@ -506,6 +565,15 @@ export default function sentinel(api) {
506
565
  if (suppressed) {
507
566
  console.log(`[sentinel] Alert suppressed by rule "${suppressed.reason}" (${SuppressionStore.describe(suppressed)})`);
508
567
  }
568
+ else if (pluginConfig.llmAlertAssessment) {
569
+ llmAssessEvent(evt).then((assessment) => {
570
+ sendAlert(formatAlert(evt, assessment)).catch((err) => {
571
+ console.error("[sentinel] alert failed:", err);
572
+ });
573
+ }).catch(() => {
574
+ sendAlert(formatAlert(evt)).catch(() => { });
575
+ });
576
+ }
509
577
  else {
510
578
  sendAlert(formatAlert(evt)).catch((err) => {
511
579
  console.error("[sentinel] alert failed:", err);
@@ -2,7 +2,7 @@
2
2
  "id": "sentinel",
3
3
  "name": "Sentinel",
4
4
  "description": "Real-time endpoint security monitoring for macOS and Linux — process execution, SSH connections, privilege escalation, and file integrity alerts via OpenClaw.",
5
- "version": "0.2.1",
5
+ "version": "0.3.1",
6
6
  "skills": ["skills/sentinel"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -61,10 +61,19 @@
61
61
  "items": { "type": "string" },
62
62
  "description": "Process paths to trust (skip alerts for these)"
63
63
  },
64
+ "trustedCommandPatterns": {
65
+ "type": "array",
66
+ "items": { "type": "string" },
67
+ "description": "Regex patterns for commands to skip (e.g. OpenClaw heartbeat scripts)"
68
+ },
64
69
  "watchPaths": {
65
70
  "type": "array",
66
71
  "items": { "type": "string" },
67
72
  "description": "File paths to monitor for integrity changes"
73
+ },
74
+ "llmAlertAssessment": {
75
+ "type": "boolean",
76
+ "description": "Use LLM to assess suspicious commands before alerting (reduces false positives from automation)"
68
77
  }
69
78
  }
70
79
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-sentinel",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Real-time endpoint security monitoring plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",