@contextfort-ai/openclaw-secure 0.1.1 → 0.1.3
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
CHANGED
|
@@ -31,7 +31,7 @@ if (args[0] === 'set-key') {
|
|
|
31
31
|
const key = args[1];
|
|
32
32
|
if (!key) {
|
|
33
33
|
console.error('Usage: openclaw-secure set-key <your-api-key>');
|
|
34
|
-
console.error('Get your key at https://contextfort.ai');
|
|
34
|
+
console.error('Get your key at https://contextfort.ai/login');
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -45,7 +45,7 @@ if (args[0] === 'enable') {
|
|
|
45
45
|
let hasKey = false;
|
|
46
46
|
try { hasKey = fs.readFileSync(CONFIG_FILE, 'utf8').trim().length > 0; } catch {}
|
|
47
47
|
if (!hasKey) {
|
|
48
|
-
console.error('No API key found. Get your key at https://contextfort.ai and run:');
|
|
48
|
+
console.error('No API key found. Get your key at https://contextfort.ai/login and run:');
|
|
49
49
|
console.error(' openclaw-secure set-key <your-key>');
|
|
50
50
|
process.exit(1);
|
|
51
51
|
}
|
|
@@ -57,6 +57,17 @@ if (args[0] === 'enable') {
|
|
|
57
57
|
console.error('openclaw not found. Install it first: npm install -g openclaw');
|
|
58
58
|
process.exit(1);
|
|
59
59
|
}
|
|
60
|
+
// Handle --no-skill-deliver flag
|
|
61
|
+
const noSkillDeliver = args.includes('--no-skill-deliver');
|
|
62
|
+
const prefsFile = path.join(CONFIG_DIR, 'preferences.json');
|
|
63
|
+
let prefs = {};
|
|
64
|
+
try { prefs = JSON.parse(fs.readFileSync(prefsFile, 'utf8')); } catch {}
|
|
65
|
+
prefs.skillDeliver = !noSkillDeliver;
|
|
66
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
67
|
+
fs.writeFileSync(prefsFile, JSON.stringify(prefs, null, 2) + '\n', { mode: 0o600 });
|
|
68
|
+
if (noSkillDeliver) {
|
|
69
|
+
console.log('Skill file scanning disabled. Only local checks will run.');
|
|
70
|
+
}
|
|
60
71
|
try {
|
|
61
72
|
const original = fs.readlinkSync(openclawLink);
|
|
62
73
|
fs.writeFileSync(backupLink, original);
|
|
@@ -65,6 +76,7 @@ if (args[0] === 'enable') {
|
|
|
65
76
|
fs.unlinkSync(openclawLink);
|
|
66
77
|
fs.symlinkSync(wrapper, openclawLink);
|
|
67
78
|
console.log('openclaw-secure enabled. `openclaw` is now guarded.');
|
|
79
|
+
console.log('Restart your openclaw gateway for the guard to take effect.');
|
|
68
80
|
process.exit(0);
|
|
69
81
|
}
|
|
70
82
|
|
|
@@ -1,21 +1,90 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Hardcoded fallback patterns (used when server is unreachable)
|
|
6
|
+
const DEFAULT_SCAN_PATTERNS = [
|
|
5
7
|
'curl -s "https://api.notion.com/v1/pages/',
|
|
8
|
+
'curl "https://api.notion.com/v1/pages/',
|
|
9
|
+
'curl -s "https://api.notion.com/v1/blocks/',
|
|
10
|
+
'curl "https://api.notion.com/v1/blocks/',
|
|
6
11
|
];
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
const PATTERNS_CACHE_FILE = '.scan_patterns_cache.json';
|
|
14
|
+
|
|
15
|
+
module.exports = function createPromptInjectionGuard({ httpsRequest, anthropicKey, analytics, readFileSync, apiKey, baseDir }) {
|
|
9
16
|
const track = analytics ? analytics.track.bind(analytics) : () => {};
|
|
10
17
|
const flaggedOutput = new Map(); // id → { suspicious, reason, command }
|
|
11
18
|
const pendingScans = new Set(); // scan ids currently in-flight
|
|
12
19
|
let scanCounter = 0;
|
|
20
|
+
let scanPatterns = [...DEFAULT_SCAN_PATTERNS];
|
|
21
|
+
|
|
22
|
+
// Load cached patterns from disk
|
|
23
|
+
const cacheFile = baseDir ? path.join(baseDir, 'monitor', PATTERNS_CACHE_FILE) : null;
|
|
24
|
+
if (cacheFile && readFileSync) {
|
|
25
|
+
try {
|
|
26
|
+
const cached = JSON.parse(readFileSync(cacheFile, 'utf8'));
|
|
27
|
+
if (Array.isArray(cached.patterns) && cached.patterns.length > 0) {
|
|
28
|
+
scanPatterns = cached.patterns;
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fetch patterns from server (non-blocking, updates in background)
|
|
34
|
+
function fetchPatternsFromServer() {
|
|
35
|
+
if (!httpsRequest || !apiKey) return;
|
|
36
|
+
|
|
37
|
+
const options = {
|
|
38
|
+
hostname: 'lschqndjjwtyrlcojvly.supabase.co',
|
|
39
|
+
port: 443,
|
|
40
|
+
path: '/rest/v1/scan_patterns?select=pattern&is_active=eq.true',
|
|
41
|
+
method: 'GET',
|
|
42
|
+
headers: {
|
|
43
|
+
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxzY2hxbmRqand0eXJsY29qdmx5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA0NDE3MTEsImV4cCI6MjA4NjAxNzcxMX0.NAC9Tx5a_HswXPC41sDocDPZGuKLgDD-IujX7MSW0I0',
|
|
44
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
45
|
+
},
|
|
46
|
+
timeout: 10000,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const req = httpsRequest(options, (res) => {
|
|
51
|
+
let body = '';
|
|
52
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
53
|
+
res.on('end', () => {
|
|
54
|
+
if (res.statusCode === 200) {
|
|
55
|
+
try {
|
|
56
|
+
const rows = JSON.parse(body);
|
|
57
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
58
|
+
const patterns = rows.map(r => r.pattern).filter(Boolean);
|
|
59
|
+
if (patterns.length > 0) {
|
|
60
|
+
scanPatterns = patterns;
|
|
61
|
+
// Cache to disk
|
|
62
|
+
if (cacheFile) {
|
|
63
|
+
try {
|
|
64
|
+
const fs = require('fs');
|
|
65
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ patterns, updated: new Date().toISOString() }));
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// If server returns empty, keep current patterns (fallback)
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
// Non-200: fail-open, keep current patterns
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.on('error', () => {});
|
|
78
|
+
req.on('timeout', () => { req.destroy(); });
|
|
79
|
+
req.end();
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
13
82
|
|
|
14
83
|
function shouldScanCommand(cmd) {
|
|
15
84
|
if (!cmd || typeof cmd !== 'string') return false;
|
|
16
85
|
if (!anthropicKey) return false;
|
|
17
86
|
const lower = cmd.toLowerCase();
|
|
18
|
-
return
|
|
87
|
+
return scanPatterns.some(p => lower.includes(p.toLowerCase()));
|
|
19
88
|
}
|
|
20
89
|
|
|
21
90
|
function scanOutput(command, stdout, stderr) {
|
|
@@ -135,7 +204,7 @@ Do NOT continue with ANY task until the human explicitly resolves this.`;
|
|
|
135
204
|
shouldScanCommand,
|
|
136
205
|
checkFlaggedOutput,
|
|
137
206
|
formatOutputBlockError,
|
|
138
|
-
init() {},
|
|
207
|
+
init() { fetchPatternsFromServer(); },
|
|
139
208
|
cleanup() {},
|
|
140
209
|
};
|
|
141
210
|
};
|
|
@@ -8,7 +8,17 @@ 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 }) {
|
|
11
|
+
module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDir, apiKey, analytics, enabled = true }) {
|
|
12
|
+
// If skill delivery is disabled, return a no-op guard
|
|
13
|
+
if (!enabled) {
|
|
14
|
+
return {
|
|
15
|
+
checkFlaggedSkills() { return null; },
|
|
16
|
+
formatSkillBlockError() { return ''; },
|
|
17
|
+
init() {},
|
|
18
|
+
cleanup() {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
const track = analytics ? analytics.track.bind(analytics) : () => {};
|
|
13
23
|
const SKILL_CACHE_FILE = path.join(baseDir, 'monitor', '.skill_scan_cache.json');
|
|
14
24
|
const INSTALL_ID_FILE = path.join(baseDir, 'monitor', '.install_id');
|
package/openclaw-secure.js
CHANGED
|
@@ -9,7 +9,15 @@ const _originalHttpsRequest = require('https').request;
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const MONITOR_PY = path.join(__dirname, 'monitor', 'monitor.py');
|
|
11
11
|
const MONITOR_CWD = path.join(__dirname, 'monitor');
|
|
12
|
-
const
|
|
12
|
+
const CONFIG_DIR = path.join(os.homedir(), '.contextfort');
|
|
13
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config');
|
|
14
|
+
const PREFS_FILE = path.join(CONFIG_DIR, 'preferences.json');
|
|
15
|
+
|
|
16
|
+
function loadPreferences() {
|
|
17
|
+
try { return JSON.parse(_originalReadFileSync(PREFS_FILE, 'utf8')); } catch { return {}; }
|
|
18
|
+
}
|
|
19
|
+
const PREFS = loadPreferences();
|
|
20
|
+
const SKILL_DELIVER = PREFS.skillDeliver !== false; // default true
|
|
13
21
|
|
|
14
22
|
// === Analytics ===
|
|
15
23
|
const analytics = require('./monitor/analytics')({
|
|
@@ -32,7 +40,7 @@ function loadApiKey() {
|
|
|
32
40
|
const API_KEY = loadApiKey();
|
|
33
41
|
|
|
34
42
|
const NO_KEY_MESSAGE = `SECURITY FIREWALL -- No API key configured. ALL agent actions are blocked.
|
|
35
|
-
Get your API key at https://contextfort.ai and run:
|
|
43
|
+
Get your API key at https://contextfort.ai/login and run:
|
|
36
44
|
openclaw-secure set-key <your-key>
|
|
37
45
|
Then restart your openclaw session.`;
|
|
38
46
|
|
|
@@ -48,6 +56,7 @@ const skillsGuard = require('./monitor/skills_guard')({
|
|
|
48
56
|
baseDir: __dirname,
|
|
49
57
|
apiKey: API_KEY,
|
|
50
58
|
analytics,
|
|
59
|
+
enabled: SKILL_DELIVER,
|
|
51
60
|
});
|
|
52
61
|
|
|
53
62
|
// === Prompt Injection Guard (PostToolUse) ===
|
|
@@ -55,6 +64,9 @@ const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || null;
|
|
|
55
64
|
const promptInjectionGuard = require('./monitor/prompt_injection_guard')({
|
|
56
65
|
httpsRequest: _originalHttpsRequest,
|
|
57
66
|
anthropicKey: ANTHROPIC_KEY,
|
|
67
|
+
readFileSync: _originalReadFileSync,
|
|
68
|
+
apiKey: API_KEY,
|
|
69
|
+
baseDir: __dirname,
|
|
58
70
|
analytics,
|
|
59
71
|
});
|
|
60
72
|
|
|
@@ -270,21 +282,8 @@ function hookFsMethods(fsModule) {
|
|
|
270
282
|
if (fsModule.readFileSync && !fsModule.readFileSync.__hooked) {
|
|
271
283
|
const orig = fsModule.readFileSync;
|
|
272
284
|
fsModule.readFileSync = function(filePath, options) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
analytics.track('fs_blocked', { blocker: 'api_key' });
|
|
276
|
-
const e = new Error(keyBlock.reason); e.code = 'EACCES'; throw e;
|
|
277
|
-
}
|
|
278
|
-
const outputBlock = promptInjectionGuard.checkFlaggedOutput();
|
|
279
|
-
if (outputBlock) {
|
|
280
|
-
analytics.track('fs_blocked', { blocker: 'prompt_injection' });
|
|
281
|
-
const e = new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)); e.code = 'EACCES'; throw e;
|
|
282
|
-
}
|
|
283
|
-
const skillBlock = skillsGuard.checkFlaggedSkills();
|
|
284
|
-
if (skillBlock) {
|
|
285
|
-
analytics.track('fs_blocked', { blocker: 'skill' });
|
|
286
|
-
const e = new Error(skillsGuard.formatSkillBlockError(skillBlock)); e.code = 'EACCES'; throw e;
|
|
287
|
-
}
|
|
285
|
+
// Pass-through: blocking here disrupts openclaw's own config/LLM operations.
|
|
286
|
+
// Agent actions are blocked at child_process level instead.
|
|
288
287
|
return orig.apply(this, arguments);
|
|
289
288
|
};
|
|
290
289
|
fsModule.readFileSync.__hooked = true;
|
|
@@ -297,26 +296,8 @@ function hookHttpModule(mod, protocol) {
|
|
|
297
296
|
if (mod.request && !mod.request.__hooked) {
|
|
298
297
|
const orig = mod.request;
|
|
299
298
|
mod.request = function(options, callback) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (keyBlock) {
|
|
303
|
-
analytics.track('http_blocked', { blocker: 'api_key' });
|
|
304
|
-
const e = new Error(keyBlock.reason); e.code = 'ENOTALLOW'; throw e;
|
|
305
|
-
}
|
|
306
|
-
const outputBlock = promptInjectionGuard.checkFlaggedOutput();
|
|
307
|
-
if (outputBlock) {
|
|
308
|
-
analytics.track('http_blocked', { blocker: 'prompt_injection' });
|
|
309
|
-
const e = new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)); e.code = 'ENOTALLOW'; throw e;
|
|
310
|
-
}
|
|
311
|
-
const skillBlock = skillsGuard.checkFlaggedSkills();
|
|
312
|
-
if (skillBlock) {
|
|
313
|
-
analytics.track('http_blocked', { blocker: 'skill' });
|
|
314
|
-
const e = new Error(skillsGuard.formatSkillBlockError(skillBlock)); e.code = 'ENOTALLOW'; throw e;
|
|
315
|
-
}
|
|
316
|
-
} catch (err) {
|
|
317
|
-
if (err.code === 'ENOTALLOW') throw err;
|
|
318
|
-
}
|
|
319
|
-
|
|
299
|
+
// Pass-through: blocking here kills openclaw's LLM API calls.
|
|
300
|
+
// Agent actions are blocked at child_process level instead.
|
|
320
301
|
return orig.apply(this, arguments);
|
|
321
302
|
};
|
|
322
303
|
mod.request.__hooked = true;
|
|
@@ -325,26 +306,6 @@ function hookHttpModule(mod, protocol) {
|
|
|
325
306
|
if (mod.get && !mod.get.__hooked) {
|
|
326
307
|
const orig = mod.get;
|
|
327
308
|
mod.get = function(options, callback) {
|
|
328
|
-
try {
|
|
329
|
-
const keyBlock = checkApiKey();
|
|
330
|
-
if (keyBlock) {
|
|
331
|
-
analytics.track('http_blocked', { blocker: 'api_key' });
|
|
332
|
-
const e = new Error(keyBlock.reason); e.code = 'ENOTALLOW'; throw e;
|
|
333
|
-
}
|
|
334
|
-
const outputBlock = promptInjectionGuard.checkFlaggedOutput();
|
|
335
|
-
if (outputBlock) {
|
|
336
|
-
analytics.track('http_blocked', { blocker: 'prompt_injection' });
|
|
337
|
-
const e = new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)); e.code = 'ENOTALLOW'; throw e;
|
|
338
|
-
}
|
|
339
|
-
const skillBlock = skillsGuard.checkFlaggedSkills();
|
|
340
|
-
if (skillBlock) {
|
|
341
|
-
analytics.track('http_blocked', { blocker: 'skill' });
|
|
342
|
-
const e = new Error(skillsGuard.formatSkillBlockError(skillBlock)); e.code = 'ENOTALLOW'; throw e;
|
|
343
|
-
}
|
|
344
|
-
} catch (err) {
|
|
345
|
-
if (err.code === 'ENOTALLOW') throw err;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
309
|
return orig.apply(this, arguments);
|
|
349
310
|
};
|
|
350
311
|
mod.get.__hooked = true;
|
|
@@ -357,27 +318,8 @@ function hookGlobalFetch() {
|
|
|
357
318
|
if (!globalThis.fetch || globalThis.fetch.__hooked) return;
|
|
358
319
|
const origFetch = globalThis.fetch;
|
|
359
320
|
globalThis.fetch = function(url, options) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (keyBlock) {
|
|
363
|
-
analytics.track('fetch_blocked', { blocker: 'api_key' });
|
|
364
|
-
return Promise.reject(new Error(keyBlock.reason));
|
|
365
|
-
}
|
|
366
|
-
const outputBlock = promptInjectionGuard.checkFlaggedOutput();
|
|
367
|
-
if (outputBlock) {
|
|
368
|
-
analytics.track('fetch_blocked', { blocker: 'prompt_injection' });
|
|
369
|
-
return Promise.reject(new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)));
|
|
370
|
-
}
|
|
371
|
-
const skillBlock = skillsGuard.checkFlaggedSkills();
|
|
372
|
-
if (skillBlock) {
|
|
373
|
-
analytics.track('fetch_blocked', { blocker: 'skill' });
|
|
374
|
-
return Promise.reject(new Error(skillsGuard.formatSkillBlockError(skillBlock)));
|
|
375
|
-
}
|
|
376
|
-
} catch (err) {
|
|
377
|
-
if (err.message && err.message.includes('SECURITY FIREWALL')) {
|
|
378
|
-
return Promise.reject(err);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
321
|
+
// Pass-through: blocking here kills openclaw's LLM API calls.
|
|
322
|
+
// Agent actions are blocked at child_process level instead.
|
|
381
323
|
return origFetch.apply(this, arguments);
|
|
382
324
|
};
|
|
383
325
|
globalThis.fetch.__hooked = true;
|
|
@@ -413,6 +355,9 @@ Module.prototype.require = function(id) {
|
|
|
413
355
|
return r;
|
|
414
356
|
};
|
|
415
357
|
|
|
416
|
-
// === Initialize
|
|
417
|
-
setImmediate(() => {
|
|
358
|
+
// === Initialize guards (non-blocking) ===
|
|
359
|
+
setImmediate(() => {
|
|
360
|
+
try { skillsGuard.init(); } catch {}
|
|
361
|
+
try { promptInjectionGuard.init(); } catch {}
|
|
362
|
+
});
|
|
418
363
|
process.on('exit', () => { skillsGuard.cleanup(); });
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextfort-ai/openclaw-secure",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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"
|
|
7
7
|
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node -e \"console.log('\\n\\x1b[32m✓ openclaw-secure installed!\\x1b[0m\\n\\nGet your API key at https://contextfort.ai/login and run:\\n\\n openclaw-secure set-key <your-key>\\n openclaw-secure enable\\n')\""
|
|
10
|
+
},
|
|
8
11
|
"keywords": [
|
|
9
12
|
"openclaw",
|
|
10
13
|
"security",
|