@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.
@@ -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',
@@ -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) { analytics.track('command_blocked', { blocker: 'api_key' }); return 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextfort-ai/openclaw-secure",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Runtime security guard for OpenClaw — blocks malicious commands before they execute",
5
5
  "bin": {
6
6
  "openclaw-secure": "./bin/openclaw-secure.js"