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.
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.js +24 -6
- package/dist/config.d.ts +3 -0
- package/dist/index.js +69 -1
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -1
package/dist/analyzer.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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);
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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
|
},
|