@contextfort-ai/openclaw-secure 0.1.5 → 0.1.7
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
|
@@ -37,6 +37,7 @@ if (args[0] === 'set-key') {
|
|
|
37
37
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
38
|
fs.writeFileSync(CONFIG_FILE, key + '\n', { mode: 0o600 });
|
|
39
39
|
console.log('API key saved to ~/.contextfort/config');
|
|
40
|
+
console.log('\nNext step: run `openclaw-secure enable` to activate the guard.');
|
|
40
41
|
process.exit(0);
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -87,15 +87,22 @@ module.exports = function createPromptInjectionGuard({ httpsRequest, anthropicKe
|
|
|
87
87
|
return scanPatterns.some(p => lower.includes(p.toLowerCase()));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function getMatchedPattern(cmd) {
|
|
91
|
+
if (!cmd || typeof cmd !== 'string') return null;
|
|
92
|
+
const lower = cmd.toLowerCase();
|
|
93
|
+
return scanPatterns.find(p => lower.includes(p.toLowerCase())) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
function scanOutput(command, stdout, stderr) {
|
|
91
97
|
if (!shouldScanCommand(command)) return;
|
|
98
|
+
const matchedPattern = getMatchedPattern(command);
|
|
92
99
|
const output = (stdout || '') + (stderr || '');
|
|
93
100
|
if (output.length < 20) return; // too short to contain injection
|
|
94
101
|
|
|
95
102
|
const scanId = `scan_${++scanCounter}`;
|
|
96
103
|
if (pendingScans.size > 10) return; // don't pile up
|
|
97
104
|
pendingScans.add(scanId);
|
|
98
|
-
track('output_scan_started', { scan_id: scanId });
|
|
105
|
+
track('output_scan_started', { scan_id: scanId, matched_pattern: matchedPattern });
|
|
99
106
|
|
|
100
107
|
// Cap output to 50k chars
|
|
101
108
|
const truncated = output.length > 50000 ? output.slice(0, 50000) + '\n[TRUNCATED]' : output;
|
|
@@ -150,7 +157,7 @@ Respond with ONLY a JSON object, no markdown, no explanation:
|
|
|
150
157
|
// Strip markdown code fences if present
|
|
151
158
|
text = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
152
159
|
const parsed = JSON.parse(text);
|
|
153
|
-
track('output_scan_result', { scan_id: scanId, suspicious: !!parsed.suspicious });
|
|
160
|
+
track('output_scan_result', { scan_id: scanId, suspicious: !!parsed.suspicious, matched_pattern: matchedPattern });
|
|
154
161
|
if (parsed.suspicious) {
|
|
155
162
|
flaggedOutput.set(scanId, {
|
|
156
163
|
suspicious: true,
|
|
@@ -19,6 +19,9 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
let PACKAGE_VERSION;
|
|
23
|
+
try { PACKAGE_VERSION = require(path.join(baseDir, 'package.json')).version; } catch { PACKAGE_VERSION = 'unknown'; }
|
|
24
|
+
|
|
22
25
|
const track = analytics ? analytics.track.bind(analytics) : () => {};
|
|
23
26
|
const SKILL_CACHE_FILE = path.join(baseDir, 'monitor', '.skill_scan_cache.json');
|
|
24
27
|
const INSTALL_ID_FILE = path.join(baseDir, 'monitor', '.install_id');
|
|
@@ -133,7 +136,8 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
133
136
|
function loadScanCache() {
|
|
134
137
|
try {
|
|
135
138
|
const data = JSON.parse(readFileSync(SKILL_CACHE_FILE, 'utf8'));
|
|
136
|
-
|
|
139
|
+
const versionChanged = data.version !== PACKAGE_VERSION;
|
|
140
|
+
if (data.hashes && !versionChanged) {
|
|
137
141
|
for (const [k, v] of Object.entries(data.hashes)) {
|
|
138
142
|
skillContentHashes.set(k, v);
|
|
139
143
|
}
|
|
@@ -151,6 +155,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
151
155
|
function saveScanCache() {
|
|
152
156
|
try {
|
|
153
157
|
const data = {
|
|
158
|
+
version: PACKAGE_VERSION,
|
|
154
159
|
hashes: Object.fromEntries(skillContentHashes),
|
|
155
160
|
flagged: Object.fromEntries(flaggedSkills),
|
|
156
161
|
updated: new Date().toISOString()
|
|
@@ -222,6 +227,37 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
|
|
230
|
+
function logSkillRemoved(skillPath, installId) {
|
|
231
|
+
track('skill_removed', { skill_name: path.basename(skillPath) });
|
|
232
|
+
const payload = JSON.stringify({
|
|
233
|
+
install_id: installId,
|
|
234
|
+
skill_path: skillPath,
|
|
235
|
+
skill_name: path.basename(skillPath),
|
|
236
|
+
files: [],
|
|
237
|
+
removed: true,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const url = new URL(SKILL_SCAN_API);
|
|
241
|
+
const headers = {
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
244
|
+
};
|
|
245
|
+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
246
|
+
try {
|
|
247
|
+
const req = httpsRequest({
|
|
248
|
+
hostname: url.hostname,
|
|
249
|
+
port: url.port || 443,
|
|
250
|
+
path: url.pathname,
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers,
|
|
253
|
+
timeout: 15000,
|
|
254
|
+
}, () => {});
|
|
255
|
+
req.on('error', () => {});
|
|
256
|
+
req.write(payload);
|
|
257
|
+
req.end();
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
|
|
225
261
|
function scanSkillIfChanged(skillPath) {
|
|
226
262
|
const { files, binaryFiles } = readSkillFiles(skillPath);
|
|
227
263
|
if (files.length === 0 && binaryFiles.length === 0) {
|
|
@@ -261,9 +297,20 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
|
|
|
261
297
|
debounceTimers.delete(skillDir);
|
|
262
298
|
// Re-discover skills and scan changed ones
|
|
263
299
|
const skills = collectSkillEntries(dir);
|
|
300
|
+
const currentPaths = new Set(skills.map(s => s.skillPath));
|
|
264
301
|
for (const { skillPath } of skills) {
|
|
265
302
|
scanSkillIfChanged(skillPath);
|
|
266
303
|
}
|
|
304
|
+
// Detect deleted skills: any known skill in this dir that no longer exists
|
|
305
|
+
for (const knownPath of skillContentHashes.keys()) {
|
|
306
|
+
if (knownPath.startsWith(dir) && !currentPaths.has(knownPath)) {
|
|
307
|
+
flaggedSkills.delete(knownPath);
|
|
308
|
+
skillContentHashes.delete(knownPath);
|
|
309
|
+
const installId = getInstallId();
|
|
310
|
+
logSkillRemoved(knownPath, installId);
|
|
311
|
+
saveScanCache();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
267
314
|
}, 500));
|
|
268
315
|
});
|
|
269
316
|
activeWatchers.push(watcher);
|
package/openclaw-secure.js
CHANGED
|
@@ -129,7 +129,7 @@ function shouldBlockCommand(cmd) {
|
|
|
129
129
|
}
|
|
130
130
|
const result = checkCommandWithMonitor(cmd);
|
|
131
131
|
if (result?.blocked) {
|
|
132
|
-
analytics.track('command_blocked', { blocker: 'tirith' });
|
|
132
|
+
analytics.track('command_blocked', { blocker: 'tirith', reason: result.reason });
|
|
133
133
|
return result;
|
|
134
134
|
}
|
|
135
135
|
return null;
|
package/package.json
CHANGED