@contextfort-ai/openclaw-secure 0.1.7 → 0.1.9
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/bin/openclaw-secure.js +173 -0
- package/monitor/analytics.js +9 -3
- package/monitor/dashboard/public/index.html +515 -0
- package/monitor/dashboard/server.js +165 -0
- package/monitor/exfil_guard/index.js +253 -0
- package/monitor/local_logger.js +105 -0
- package/monitor/secrets_guard/index.js +849 -0
- package/monitor/skills_guard/index.js +6 -1
- package/openclaw-secure.js +124 -3
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ const os = require('os');
|
|
|
8
8
|
const SKILL_SCAN_API = 'https://lschqndjjwtyrlcojvly.supabase.co/functions/v1/scan-skill';
|
|
9
9
|
const HOME = os.homedir();
|
|
10
10
|
|
|
11
|
-
module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDir, apiKey, analytics, enabled = true }) {
|
|
11
|
+
module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDir, apiKey, analytics, enabled = true, localLogger }) {
|
|
12
12
|
// If skill delivery is disabled, return a no-op guard
|
|
13
13
|
if (!enabled) {
|
|
14
14
|
return {
|
|
@@ -177,6 +177,11 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
177
177
|
files: files,
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
// Log what we're sending to Supabase (omit file contents for privacy)
|
|
181
|
+
if (localLogger) {
|
|
182
|
+
try { localLogger.logServerSend({ destination: 'supabase', event: 'skill_scan', payload: { skill_name: path.basename(skillPath), file_count: files.length } }); } catch {}
|
|
183
|
+
}
|
|
184
|
+
|
|
180
185
|
const url = new URL(SKILL_SCAN_API);
|
|
181
186
|
const headers = {
|
|
182
187
|
'Content-Type': 'application/json',
|
package/openclaw-secure.js
CHANGED
|
@@ -19,12 +19,17 @@ function loadPreferences() {
|
|
|
19
19
|
const PREFS = loadPreferences();
|
|
20
20
|
const SKILL_DELIVER = PREFS.skillDeliver !== false; // default true
|
|
21
21
|
|
|
22
|
+
// === Local Audit Logger ===
|
|
23
|
+
const localLogger = require('./monitor/local_logger')({ baseDir: CONFIG_DIR });
|
|
24
|
+
|
|
22
25
|
// === Analytics ===
|
|
23
26
|
const analytics = require('./monitor/analytics')({
|
|
24
27
|
httpsRequest: _originalHttpsRequest,
|
|
25
28
|
readFileSync: _originalReadFileSync,
|
|
26
29
|
baseDir: __dirname,
|
|
30
|
+
localLogger,
|
|
27
31
|
});
|
|
32
|
+
localLogger.logLocal({ event: 'hook_loaded' });
|
|
28
33
|
analytics.track('hook_loaded');
|
|
29
34
|
|
|
30
35
|
function loadApiKey() {
|
|
@@ -57,6 +62,20 @@ const skillsGuard = require('./monitor/skills_guard')({
|
|
|
57
62
|
apiKey: API_KEY,
|
|
58
63
|
analytics,
|
|
59
64
|
enabled: SKILL_DELIVER,
|
|
65
|
+
localLogger,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// === Secrets Guard (env var leak monitoring) ===
|
|
69
|
+
const secretsGuard = require('./monitor/secrets_guard')({
|
|
70
|
+
spawnSync: _originalSpawnSync,
|
|
71
|
+
baseDir: __dirname,
|
|
72
|
+
analytics,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// === Exfil Guard (logging-only) ===
|
|
76
|
+
const exfilGuard = require('./monitor/exfil_guard')({
|
|
77
|
+
analytics,
|
|
78
|
+
localLogger,
|
|
60
79
|
});
|
|
61
80
|
|
|
62
81
|
// === Prompt Injection Guard (PostToolUse) ===
|
|
@@ -115,23 +134,61 @@ function extractShellCommand(command, args) {
|
|
|
115
134
|
|
|
116
135
|
function shouldBlockCommand(cmd) {
|
|
117
136
|
if (!cmd || typeof cmd !== 'string') return null;
|
|
137
|
+
const cmdSlice = cmd.slice(0, 500);
|
|
138
|
+
const guards = [];
|
|
139
|
+
|
|
118
140
|
const keyBlock = checkApiKey();
|
|
119
|
-
if (keyBlock) {
|
|
141
|
+
if (keyBlock) {
|
|
142
|
+
analytics.track('command_blocked', { blocker: 'api_key' });
|
|
143
|
+
localLogger.logLocal({ event: 'command_blocked', command: cmdSlice, guards: ['api_key'], decision: 'block', blocker: 'api_key', reason: 'No API key' });
|
|
144
|
+
return keyBlock;
|
|
145
|
+
}
|
|
146
|
+
guards.push('api_key');
|
|
147
|
+
|
|
120
148
|
const outputBlock = promptInjectionGuard.checkFlaggedOutput();
|
|
121
149
|
if (outputBlock) {
|
|
122
150
|
analytics.track('command_blocked', { blocker: 'prompt_injection' });
|
|
151
|
+
localLogger.logLocal({ event: 'command_blocked', command: cmdSlice, guards: [...guards, 'prompt_injection'], decision: 'block', blocker: 'prompt_injection', reason: outputBlock.reason || 'Flagged output' });
|
|
123
152
|
return { blocked: true, reason: promptInjectionGuard.formatOutputBlockError(outputBlock) };
|
|
124
153
|
}
|
|
154
|
+
guards.push('prompt_injection');
|
|
155
|
+
|
|
125
156
|
const skillBlock = skillsGuard.checkFlaggedSkills();
|
|
126
157
|
if (skillBlock) {
|
|
127
158
|
analytics.track('command_blocked', { blocker: 'skill' });
|
|
159
|
+
localLogger.logLocal({ event: 'command_blocked', command: cmdSlice, guards: [...guards, 'skill'], decision: 'block', blocker: 'skill', reason: skillBlock.reason || 'Flagged skill' });
|
|
128
160
|
return { blocked: true, reason: skillsGuard.formatSkillBlockError(skillBlock) };
|
|
129
161
|
}
|
|
162
|
+
guards.push('skill');
|
|
163
|
+
|
|
164
|
+
const envCheck = secretsGuard.checkEnvVarLeak(cmd);
|
|
165
|
+
if (envCheck && envCheck.blocked) {
|
|
166
|
+
analytics.track('command_blocked', { blocker: 'env_var_leak', vars: envCheck.vars, type: envCheck.type });
|
|
167
|
+
localLogger.logLocal({ event: 'command_blocked', command: cmdSlice, guards: [...guards, 'env_var'], decision: 'block', blocker: 'env_var', reason: `Env var leak: ${envCheck.vars.join(', ')}` });
|
|
168
|
+
return { blocked: true, reason: secretsGuard.formatEnvVarBlockError(envCheck) };
|
|
169
|
+
}
|
|
170
|
+
if (envCheck && !envCheck.blocked && envCheck.vars.length > 0) {
|
|
171
|
+
analytics.track('env_var_used', { vars: envCheck.vars, command_prefix: cmd.slice(0, 100) });
|
|
172
|
+
}
|
|
173
|
+
guards.push('env_var');
|
|
174
|
+
|
|
175
|
+
const exfilCheck = exfilGuard.checkExfilAttempt(cmd);
|
|
176
|
+
if (exfilCheck) {
|
|
177
|
+
analytics.track('exfil_attempt', { tool: exfilCheck.tool, destination: exfilCheck.destination, vars_count: exfilCheck.vars.length });
|
|
178
|
+
localLogger.logLocal({ event: 'exfil_attempt', command: cmdSlice, guards: [...guards, 'exfil'], decision: 'log', blocker: 'exfil', reason: null, detection: exfilCheck });
|
|
179
|
+
}
|
|
180
|
+
guards.push('exfil');
|
|
181
|
+
|
|
130
182
|
const result = checkCommandWithMonitor(cmd);
|
|
131
183
|
if (result?.blocked) {
|
|
132
184
|
analytics.track('command_blocked', { blocker: 'tirith', reason: result.reason });
|
|
185
|
+
localLogger.logLocal({ event: 'command_blocked', command: cmdSlice, guards: [...guards, 'tirith'], decision: 'block', blocker: 'tirith', reason: result.reason });
|
|
133
186
|
return result;
|
|
134
187
|
}
|
|
188
|
+
guards.push('tirith');
|
|
189
|
+
|
|
190
|
+
// All guards passed — log the allowed command
|
|
191
|
+
localLogger.logLocal({ event: 'command_check', command: cmdSlice, guards, decision: 'allow', blocker: null, reason: null });
|
|
135
192
|
return null;
|
|
136
193
|
}
|
|
137
194
|
|
|
@@ -190,6 +247,26 @@ function hookAllSpawnMethods(cp) {
|
|
|
190
247
|
const result = orig.apply(this, arguments);
|
|
191
248
|
if (shellCmd) {
|
|
192
249
|
try { promptInjectionGuard.scanOutput(shellCmd, (result.stdout || '').toString(), (result.stderr || '').toString()); } catch {}
|
|
250
|
+
// Redact secrets from output before LLM sees them
|
|
251
|
+
try {
|
|
252
|
+
const stdoutStr = (result.stdout || '').toString();
|
|
253
|
+
const stderrStr = (result.stderr || '').toString();
|
|
254
|
+
const stdoutScan = secretsGuard.scanOutputForSecrets(stdoutStr);
|
|
255
|
+
const stderrScan = secretsGuard.scanOutputForSecrets(stderrStr);
|
|
256
|
+
if (stdoutScan.found || stderrScan.found) {
|
|
257
|
+
const allSecrets = [...(stdoutScan.secrets || []), ...(stderrScan.secrets || [])];
|
|
258
|
+
const notice = secretsGuard.formatRedactionNotice({ secrets: allSecrets });
|
|
259
|
+
localLogger.logLocal({ event: 'output_redacted', command: shellCmd.slice(0, 500), guards: ['secrets'], decision: 'redact', secrets_count: allSecrets.length });
|
|
260
|
+
if (stdoutScan.found) {
|
|
261
|
+
const redacted = stdoutScan.redacted + notice;
|
|
262
|
+
result.stdout = Buffer.isBuffer(result.stdout) ? Buffer.from(redacted) : redacted;
|
|
263
|
+
}
|
|
264
|
+
if (stderrScan.found) {
|
|
265
|
+
const redacted = stderrScan.redacted + notice;
|
|
266
|
+
result.stderr = Buffer.isBuffer(result.stderr) ? Buffer.from(redacted) : redacted;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {}
|
|
193
270
|
}
|
|
194
271
|
return result;
|
|
195
272
|
};
|
|
@@ -206,9 +283,21 @@ function hookAllSpawnMethods(cp) {
|
|
|
206
283
|
if (cb) { process.nextTick(() => cb(e, '', '')); return { kill: () => {} }; }
|
|
207
284
|
throw e;
|
|
208
285
|
}
|
|
209
|
-
// Wrap callback to capture output for PostToolUse scan
|
|
286
|
+
// Wrap callback to capture output for PostToolUse scan + redact secrets
|
|
210
287
|
const wrapCb = (origCb) => function(err, stdout, stderr) {
|
|
211
288
|
if (!err) { try { promptInjectionGuard.scanOutput(command, (stdout || '').toString(), (stderr || '').toString()); } catch {} }
|
|
289
|
+
// Redact secrets from output before callback sees them
|
|
290
|
+
try {
|
|
291
|
+
let so = stdout, se = stderr;
|
|
292
|
+
const stdoutScan = secretsGuard.scanOutputForSecrets((stdout || '').toString());
|
|
293
|
+
const stderrScan = secretsGuard.scanOutputForSecrets((stderr || '').toString());
|
|
294
|
+
if (stdoutScan.found || stderrScan.found) {
|
|
295
|
+
const notice = secretsGuard.formatRedactionNotice({ secrets: [...(stdoutScan.secrets || []), ...(stderrScan.secrets || [])] });
|
|
296
|
+
if (stdoutScan.found) so = stdoutScan.redacted + notice;
|
|
297
|
+
if (stderrScan.found) se = stderrScan.redacted + notice;
|
|
298
|
+
return origCb.call(this, err, so, se);
|
|
299
|
+
}
|
|
300
|
+
} catch {}
|
|
212
301
|
return origCb.apply(this, arguments);
|
|
213
302
|
};
|
|
214
303
|
if (typeof options === 'function') {
|
|
@@ -228,6 +317,16 @@ function hookAllSpawnMethods(cp) {
|
|
|
228
317
|
if (block) { const e = new Error(formatBlockError(command, block)); e.code = 'EPERM'; throw e; }
|
|
229
318
|
const result = orig.apply(this, arguments);
|
|
230
319
|
try { promptInjectionGuard.scanOutput(command, (result || '').toString(), ''); } catch {}
|
|
320
|
+
// Redact secrets from output before LLM sees them
|
|
321
|
+
try {
|
|
322
|
+
const str = (result || '').toString();
|
|
323
|
+
const scan = secretsGuard.scanOutputForSecrets(str);
|
|
324
|
+
if (scan.found) {
|
|
325
|
+
localLogger.logLocal({ event: 'output_redacted', command: command.slice(0, 500), guards: ['secrets'], decision: 'redact', secrets_count: scan.secrets.length });
|
|
326
|
+
const redacted = scan.redacted + secretsGuard.formatRedactionNotice(scan);
|
|
327
|
+
return Buffer.isBuffer(result) ? Buffer.from(redacted) : redacted;
|
|
328
|
+
}
|
|
329
|
+
} catch {}
|
|
231
330
|
return result;
|
|
232
331
|
};
|
|
233
332
|
cp.execSync.__hooked = true;
|
|
@@ -246,10 +345,22 @@ function hookAllSpawnMethods(cp) {
|
|
|
246
345
|
throw e;
|
|
247
346
|
}
|
|
248
347
|
}
|
|
249
|
-
// Wrap callback for PostToolUse scan
|
|
348
|
+
// Wrap callback for PostToolUse scan + redact secrets
|
|
250
349
|
if (shellCmd) {
|
|
251
350
|
const wrapCb = (origCb) => function(err, stdout, stderr) {
|
|
252
351
|
if (!err) { try { promptInjectionGuard.scanOutput(shellCmd, (stdout || '').toString(), (stderr || '').toString()); } catch {} }
|
|
352
|
+
// Redact secrets from output before callback sees them
|
|
353
|
+
try {
|
|
354
|
+
let so = stdout, se = stderr;
|
|
355
|
+
const stdoutScan = secretsGuard.scanOutputForSecrets((stdout || '').toString());
|
|
356
|
+
const stderrScan = secretsGuard.scanOutputForSecrets((stderr || '').toString());
|
|
357
|
+
if (stdoutScan.found || stderrScan.found) {
|
|
358
|
+
const notice = secretsGuard.formatRedactionNotice({ secrets: [...(stdoutScan.secrets || []), ...(stderrScan.secrets || [])] });
|
|
359
|
+
if (stdoutScan.found) so = stdoutScan.redacted + notice;
|
|
360
|
+
if (stderrScan.found) se = stderrScan.redacted + notice;
|
|
361
|
+
return origCb.call(this, err, so, se);
|
|
362
|
+
}
|
|
363
|
+
} catch {}
|
|
253
364
|
return origCb.apply(this, arguments);
|
|
254
365
|
};
|
|
255
366
|
if (typeof args === 'function') return orig.call(this, file, wrapCb(args));
|
|
@@ -272,6 +383,16 @@ function hookAllSpawnMethods(cp) {
|
|
|
272
383
|
const result = orig.apply(this, arguments);
|
|
273
384
|
if (shellCmd) {
|
|
274
385
|
try { promptInjectionGuard.scanOutput(shellCmd, (result || '').toString(), ''); } catch {}
|
|
386
|
+
// Redact secrets from output
|
|
387
|
+
try {
|
|
388
|
+
const str = (result || '').toString();
|
|
389
|
+
const scan = secretsGuard.scanOutputForSecrets(str);
|
|
390
|
+
if (scan.found) {
|
|
391
|
+
localLogger.logLocal({ event: 'output_redacted', command: shellCmd.slice(0, 500), guards: ['secrets'], decision: 'redact', secrets_count: scan.secrets.length });
|
|
392
|
+
const redacted = scan.redacted + secretsGuard.formatRedactionNotice(scan);
|
|
393
|
+
return Buffer.isBuffer(result) ? Buffer.from(redacted) : redacted;
|
|
394
|
+
}
|
|
395
|
+
} catch {}
|
|
275
396
|
}
|
|
276
397
|
return result;
|
|
277
398
|
};
|
package/package.json
CHANGED