@contextfort-ai/openclaw-secure 0.1.4 → 0.1.6

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.
@@ -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,
@@ -83,6 +83,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
83
83
 
84
84
  function readSkillFiles(skillPath) {
85
85
  const files = [];
86
+ const binaryFiles = [];
86
87
  const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1MB
87
88
  const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
88
89
  let totalSize = 0;
@@ -91,7 +92,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
91
92
  let entries;
92
93
  try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; }
93
94
  for (const e of entries) {
94
- if (e.name.startsWith('.')) continue; // skip hidden
95
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue; // skip hidden and node_modules
95
96
  const full = path.join(dirPath, e.name);
96
97
  if (e.isDirectory()) {
97
98
  walk(full, path.join(base, e.name));
@@ -100,11 +101,14 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
100
101
  const stat = fs.statSync(full);
101
102
  if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
102
103
  if (totalSize + stat.size > MAX_TOTAL_SIZE) continue;
103
- // Skip binaries: read first 512 bytes and check for null bytes
104
+ // Check for binaries: read first 512 bytes and check for null bytes
104
105
  const buf = Buffer.alloc(Math.min(512, stat.size));
105
106
  const fd = fs.openSync(full, 'r');
106
107
  try { fs.readSync(fd, buf, 0, buf.length, 0); } finally { fs.closeSync(fd); }
107
- if (buf.includes(0)) continue; // binary file
108
+ if (buf.includes(0)) {
109
+ binaryFiles.push(path.join(base, e.name));
110
+ continue;
111
+ }
108
112
 
109
113
  const content = readFileSync(full, 'utf8');
110
114
  totalSize += stat.size;
@@ -114,7 +118,7 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
114
118
  }
115
119
  }
116
120
  walk(skillPath, '');
117
- return files;
121
+ return { files, binaryFiles };
118
122
  }
119
123
 
120
124
  function hashSkillFiles(files) {
@@ -218,15 +222,58 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
218
222
  }
219
223
  }
220
224
 
225
+ function logSkillRemoved(skillPath, installId) {
226
+ track('skill_removed', { skill_name: path.basename(skillPath) });
227
+ const payload = JSON.stringify({
228
+ install_id: installId,
229
+ skill_path: skillPath,
230
+ skill_name: path.basename(skillPath),
231
+ files: [],
232
+ removed: true,
233
+ });
234
+
235
+ const url = new URL(SKILL_SCAN_API);
236
+ const headers = {
237
+ 'Content-Type': 'application/json',
238
+ 'Content-Length': Buffer.byteLength(payload),
239
+ };
240
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
241
+ try {
242
+ const req = httpsRequest({
243
+ hostname: url.hostname,
244
+ port: url.port || 443,
245
+ path: url.pathname,
246
+ method: 'POST',
247
+ headers,
248
+ timeout: 15000,
249
+ }, () => {});
250
+ req.on('error', () => {});
251
+ req.write(payload);
252
+ req.end();
253
+ } catch {}
254
+ }
255
+
221
256
  function scanSkillIfChanged(skillPath) {
222
- const files = readSkillFiles(skillPath);
223
- if (files.length === 0) {
257
+ const { files, binaryFiles } = readSkillFiles(skillPath);
258
+ if (files.length === 0 && binaryFiles.length === 0) {
224
259
  // Skill was deleted or is empty — remove from flagged
225
260
  flaggedSkills.delete(skillPath);
226
261
  skillContentHashes.delete(skillPath);
227
262
  saveScanCache();
228
263
  return;
229
264
  }
265
+
266
+ // Flag immediately if binary files found — legitimate skills should not contain binaries
267
+ if (binaryFiles.length > 0) {
268
+ flaggedSkills.set(skillPath, {
269
+ suspicious: true,
270
+ reason: `Skill contains binary files (${binaryFiles.join(', ')}). Legitimate skills should only contain text files. Please delete these binary files or remove this skill.`,
271
+ });
272
+ track('skill_binary_detected', { skill_name: path.basename(skillPath), binary_count: binaryFiles.length });
273
+ saveScanCache();
274
+ return;
275
+ }
276
+
230
277
  const hash = hashSkillFiles(files);
231
278
  if (skillContentHashes.get(skillPath) === hash) return; // unchanged
232
279
 
@@ -245,9 +292,20 @@ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDi
245
292
  debounceTimers.delete(skillDir);
246
293
  // Re-discover skills and scan changed ones
247
294
  const skills = collectSkillEntries(dir);
295
+ const currentPaths = new Set(skills.map(s => s.skillPath));
248
296
  for (const { skillPath } of skills) {
249
297
  scanSkillIfChanged(skillPath);
250
298
  }
299
+ // Detect deleted skills: any known skill in this dir that no longer exists
300
+ for (const knownPath of skillContentHashes.keys()) {
301
+ if (knownPath.startsWith(dir) && !currentPaths.has(knownPath)) {
302
+ flaggedSkills.delete(knownPath);
303
+ skillContentHashes.delete(knownPath);
304
+ const installId = getInstallId();
305
+ logSkillRemoved(knownPath, installId);
306
+ saveScanCache();
307
+ }
308
+ }
251
309
  }, 500));
252
310
  });
253
311
  activeWatchers.push(watcher);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextfort-ai/openclaw-secure",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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"