@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.
@@ -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
- if (data.hashes) {
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);
@@ -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.5",
3
+ "version": "0.1.7",
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"