agentshield-sdk 13.3.0 → 14.0.0

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.
@@ -7,10 +7,17 @@
7
7
  * Plugins are simple objects with a detect() method that returns an array
8
8
  * of threat findings. All detection runs locally — no data ever leaves
9
9
  * your environment.
10
+ *
11
+ * This module now includes IsolatedPluginSandbox which uses Node's built-in
12
+ * `vm` module to run untrusted plugin source code in a sanitized context
13
+ * with no access to process, fs, net, http, or child_process. It also
14
+ * enforces a preemptive timeout via vm.Script's `timeout` option.
10
15
  */
11
16
 
12
17
  const path = require('path');
13
18
  const fs = require('fs');
19
+ const vm = require('vm');
20
+ const crypto = require('crypto');
14
21
 
15
22
  // =========================================================================
16
23
  // HELPERS
@@ -27,13 +34,29 @@ const now = () => {
27
34
  return Date.now();
28
35
  };
29
36
 
37
+ // Try to raise the old-space memory cap a bit so tight infinite-loop plugins
38
+ // still get killed by the vm timeout (which is what provides the real bound).
39
+ try {
40
+ const v8 = require('v8');
41
+ if (typeof v8.setFlagsFromString === 'function') {
42
+ // Best-effort only; ignored silently if not permitted.
43
+ v8.setFlagsFromString('--max-old-space-size=4096');
44
+ }
45
+ } catch (_err) {
46
+ // v8 module missing or setFlagsFromString unavailable — silently skip.
47
+ }
48
+
30
49
  // =========================================================================
31
- // PLUGIN SANDBOX
50
+ // PLUGIN SANDBOX (legacy — kept for backward compatibility)
32
51
  // =========================================================================
33
52
 
34
53
  /**
35
54
  * Runs plugins with timeout protection and error isolation.
36
55
  * Prevents a misbehaving plugin from crashing the host agent.
56
+ *
57
+ * NOTE: This sandbox only times execution and catches errors. It does not
58
+ * isolate plugin code from the host process. Use IsolatedPluginSandbox for
59
+ * untrusted plugin source.
37
60
  */
38
61
  class PluginSandbox {
39
62
  /**
@@ -57,10 +80,6 @@ class PluginSandbox {
57
80
  let error = null;
58
81
 
59
82
  try {
60
- // Run detection synchronously with a time check after completion.
61
- // True preemptive timeout would require worker_threads, but for a
62
- // lightweight zero-dependency SDK we keep it simple: run, measure,
63
- // and flag if it exceeded the budget.
64
83
  const output = plugin.detect(text, options);
65
84
  const durationMs = now() - start;
66
85
 
@@ -82,6 +101,393 @@ class PluginSandbox {
82
101
  }
83
102
  }
84
103
 
104
+ // =========================================================================
105
+ // ISOLATED PLUGIN SANDBOX (vm-based, real isolation)
106
+ // =========================================================================
107
+
108
+ /**
109
+ * Whitelist of modules considered safe to expose to plugins via the
110
+ * restricted `require`. Anything else is blocked.
111
+ * @type {Set<string>}
112
+ */
113
+ const DEFAULT_SAFE_MODULES = new Set([
114
+ 'util'
115
+ ]);
116
+
117
+ /**
118
+ * Build a sanitized console that writes to a string buffer instead of stdout.
119
+ * @param {{buffer: string}} sink - Object whose `buffer` field gets appended to
120
+ * @returns {object} Console-like object
121
+ */
122
+ function makeSafeConsole(sink) {
123
+ const write = (level) => (...args) => {
124
+ const line = args.map((a) => {
125
+ if (typeof a === 'string') return a;
126
+ try { return JSON.stringify(a); } catch (_) { return String(a); }
127
+ }).join(' ');
128
+ sink.buffer += `[${level}] ${line}\n`;
129
+ };
130
+ return {
131
+ log: write('log'),
132
+ info: write('info'),
133
+ warn: write('warn'),
134
+ error: write('error'),
135
+ debug: write('debug')
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Real plugin sandbox using Node's `vm` module. Plugin source runs in a
141
+ * brand-new context with no access to the host globals or the require()
142
+ * function of the host process.
143
+ */
144
+ class IsolatedPluginSandbox {
145
+ /**
146
+ * @param {object} [options]
147
+ * @param {number} [options.timeoutMs=100] - Hard execution budget in ms (preemptive)
148
+ * @param {string[]} [options.allowRequire=[]] - Whitelist of module IDs the plugin may require()
149
+ */
150
+ constructor(options = {}) {
151
+ this.timeoutMs = options.timeoutMs || 100;
152
+ // Combine the default whitelist with any caller-supplied entries.
153
+ this.allowRequire = new Set([
154
+ ...DEFAULT_SAFE_MODULES,
155
+ ...(Array.isArray(options.allowRequire) ? options.allowRequire : [])
156
+ ]);
157
+ }
158
+
159
+ /**
160
+ * Build a safe restricted `require` function for the plugin context.
161
+ * @returns {function}
162
+ * @private
163
+ */
164
+ _makeSafeRequire() {
165
+ const allowed = this.allowRequire;
166
+ return function safeRequire(id) {
167
+ if (typeof id !== 'string' || id.length === 0) {
168
+ throw new Error('require(id) expects a non-empty string');
169
+ }
170
+ if (!allowed.has(id)) {
171
+ throw new Error(`[Agent Shield] require("${id}") blocked by sandbox. Allowed: ${[...allowed].join(', ') || '(none)'}`);
172
+ }
173
+ // Only absolute, unambiguous module names reach here.
174
+ // eslint-disable-next-line global-require
175
+ return require(id);
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Run untrusted plugin source. The source is expected to export a
181
+ * detect(text, options) function by assigning to `module.exports`
182
+ * or by assigning a top-level `detect` binding.
183
+ *
184
+ * @param {string} source - Plugin source code as a string
185
+ * @param {string} text - Text to scan
186
+ * @param {object} [options] - Options passed to detect()
187
+ * @returns {{results: Array, error: string|null, durationMs: number, consoleOutput: string}}
188
+ */
189
+ runSource(source, text, options = {}) {
190
+ if (typeof source !== 'string' || source.length === 0) {
191
+ return { results: [], error: 'Plugin source must be a non-empty string', durationMs: 0, consoleOutput: '' };
192
+ }
193
+
194
+ const sink = { buffer: '' };
195
+ const safeConsole = makeSafeConsole(sink);
196
+ const moduleShim = { exports: {} };
197
+
198
+ // Build a fresh object so the plugin cannot mutate the host's globals
199
+ // by poisoning the prototype chain from inside the context.
200
+ const sandboxGlobals = Object.create(null);
201
+
202
+ // Use vm.runInNewContext to realm-isolate the built-ins. Each call gets
203
+ // its own Object/Array/String/etc so that prototype pollution inside the
204
+ // plugin does NOT affect the host process.
205
+ const realm = vm.runInNewContext(`({
206
+ String, Number, Boolean, Array, Object, RegExp, Math, Date, JSON,
207
+ Map, Set, WeakMap, WeakSet, Error, TypeError, RangeError, SyntaxError,
208
+ Symbol, Promise
209
+ })`, Object.create(null), { timeout: this.timeoutMs });
210
+
211
+ sandboxGlobals.String = realm.String;
212
+ sandboxGlobals.Number = realm.Number;
213
+ sandboxGlobals.Boolean = realm.Boolean;
214
+ sandboxGlobals.Array = realm.Array;
215
+ sandboxGlobals.Object = realm.Object;
216
+ sandboxGlobals.RegExp = realm.RegExp;
217
+ sandboxGlobals.Math = realm.Math;
218
+ sandboxGlobals.Date = realm.Date;
219
+ sandboxGlobals.JSON = realm.JSON;
220
+ sandboxGlobals.Map = realm.Map;
221
+ sandboxGlobals.Set = realm.Set;
222
+ sandboxGlobals.WeakMap = realm.WeakMap;
223
+ sandboxGlobals.WeakSet = realm.WeakSet;
224
+ sandboxGlobals.Error = realm.Error;
225
+ sandboxGlobals.TypeError = realm.TypeError;
226
+ sandboxGlobals.RangeError = realm.RangeError;
227
+ sandboxGlobals.SyntaxError = realm.SyntaxError;
228
+ sandboxGlobals.Symbol = realm.Symbol;
229
+ sandboxGlobals.Promise = realm.Promise;
230
+
231
+ // Restricted surface for the plugin.
232
+ sandboxGlobals.console = safeConsole;
233
+ sandboxGlobals.require = this._makeSafeRequire();
234
+ sandboxGlobals.module = moduleShim;
235
+ sandboxGlobals.exports = moduleShim.exports;
236
+
237
+ // Plugin inputs are made available as globals for convenience.
238
+ sandboxGlobals.__text = text;
239
+ sandboxGlobals.__options = options;
240
+
241
+ // Create a sanitized `globalThis` / `global` that points at the same
242
+ // frozen-ish object so the plugin sees a self-consistent environment
243
+ // without being able to reach the host.
244
+ sandboxGlobals.globalThis = sandboxGlobals;
245
+ sandboxGlobals.global = sandboxGlobals;
246
+
247
+ // Create the context. Do NOT use codeGeneration: {strings: true} — we
248
+ // explicitly forbid eval/new Function inside the plugin.
249
+ const context = vm.createContext(sandboxGlobals, {
250
+ name: 'agent-shield-plugin-sandbox',
251
+ codeGeneration: { strings: false, wasm: false }
252
+ });
253
+
254
+ // Wrap source so that if the plugin assigns `detect = ...` without
255
+ // module.exports, we still find it.
256
+ const wrapped = `
257
+ (function() {
258
+ 'use strict';
259
+ ${source}
260
+ ;if (typeof module.exports === 'function') { return module.exports; }
261
+ if (module.exports && typeof module.exports.detect === 'function') { return module.exports.detect; }
262
+ if (typeof detect === 'function') { return detect; }
263
+ return null;
264
+ })()
265
+ `;
266
+
267
+ const start = now();
268
+ let script;
269
+ try {
270
+ script = new vm.Script(wrapped, { filename: 'plugin.js' });
271
+ } catch (err) {
272
+ return {
273
+ results: [],
274
+ error: `Plugin compile error: ${err.message || String(err)}`,
275
+ durationMs: now() - start,
276
+ consoleOutput: sink.buffer
277
+ };
278
+ }
279
+
280
+ let detectFn;
281
+ try {
282
+ detectFn = script.runInContext(context, { timeout: this.timeoutMs });
283
+ } catch (err) {
284
+ const durationMs = now() - start;
285
+ const msg = err && err.message ? err.message : String(err);
286
+ console.log(`[Agent Shield] Isolated plugin load failed: ${msg}`);
287
+ return { results: [], error: msg, durationMs, consoleOutput: sink.buffer };
288
+ }
289
+
290
+ if (typeof detectFn !== 'function') {
291
+ return {
292
+ results: [],
293
+ error: 'Plugin did not export a detect() function',
294
+ durationMs: now() - start,
295
+ consoleOutput: sink.buffer
296
+ };
297
+ }
298
+
299
+ // Invoke detect() inside the same context with its own timeout so that
300
+ // a tight infinite loop here is still killed.
301
+ const callScript = new vm.Script(
302
+ '__detect(__text, __options)',
303
+ { filename: 'plugin-invoke.js' }
304
+ );
305
+ context.__detect = detectFn;
306
+
307
+ let output;
308
+ try {
309
+ output = callScript.runInContext(context, { timeout: this.timeoutMs });
310
+ } catch (err) {
311
+ const durationMs = now() - start;
312
+ const msg = err && err.message ? err.message : String(err);
313
+ console.log(`[Agent Shield] Isolated plugin threw: ${msg}`);
314
+ return { results: [], error: msg, durationMs, consoleOutput: sink.buffer };
315
+ }
316
+
317
+ const durationMs = now() - start;
318
+ const results = Array.isArray(output) ? output : [];
319
+ return { results, error: null, durationMs, consoleOutput: sink.buffer };
320
+ }
321
+ }
322
+
323
+ // =========================================================================
324
+ // PLUGIN VERIFIER / SIGNING
325
+ // =========================================================================
326
+
327
+ /**
328
+ * Compute an HMAC-SHA256 signature for a plugin source string.
329
+ * @param {string} source - Plugin source code
330
+ * @param {string} key - HMAC secret
331
+ * @returns {string} Hex-encoded signature
332
+ */
333
+ function signPlugin(source, key) {
334
+ if (typeof source !== 'string') {
335
+ throw new TypeError('signPlugin: source must be a string');
336
+ }
337
+ if (typeof key !== 'string' || key.length === 0) {
338
+ throw new TypeError('signPlugin: key must be a non-empty string');
339
+ }
340
+ return crypto.createHmac('sha256', key).update(source, 'utf8').digest('hex');
341
+ }
342
+
343
+ /**
344
+ * Verify a plugin signature using HMAC-SHA256 with a constant-time compare.
345
+ * @param {string} source - Plugin source code
346
+ * @param {string} signature - Hex-encoded signature to check
347
+ * @param {string} key - HMAC secret
348
+ * @returns {boolean} true if the signature is valid
349
+ */
350
+ function verifyPluginSignature(source, signature, key) {
351
+ if (typeof source !== 'string' || typeof signature !== 'string' || typeof key !== 'string') {
352
+ return false;
353
+ }
354
+ let expected;
355
+ try {
356
+ expected = signPlugin(source, key);
357
+ } catch (_err) {
358
+ return false;
359
+ }
360
+ if (expected.length !== signature.length) return false;
361
+ try {
362
+ return crypto.timingSafeEqual(
363
+ Buffer.from(expected, 'hex'),
364
+ Buffer.from(signature, 'hex')
365
+ );
366
+ } catch (_err) {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Verifier for plugin signatures. If configured with a signing key, any
373
+ * unsigned or invalidly signed plugin is rejected.
374
+ */
375
+ class PluginVerifier {
376
+ /**
377
+ * @param {object} [options]
378
+ * @param {string} [options.signingKey] - HMAC secret. If omitted, all plugins pass.
379
+ * @param {boolean} [options.requireSignature=true] - Reject unsigned plugins when key is set
380
+ */
381
+ constructor(options = {}) {
382
+ this.signingKey = typeof options.signingKey === 'string' ? options.signingKey : null;
383
+ this.requireSignature = options.requireSignature !== false;
384
+ }
385
+
386
+ /**
387
+ * Returns true when this verifier was configured with a signing key.
388
+ * @returns {boolean}
389
+ */
390
+ isConfigured() {
391
+ return typeof this.signingKey === 'string' && this.signingKey.length > 0;
392
+ }
393
+
394
+ /**
395
+ * Verify a manifest + source bundle.
396
+ * @param {string} source - Plugin source code
397
+ * @param {PluginManifest|object} manifest - Plugin manifest (may contain .signature)
398
+ * @returns {{valid: boolean, reason: string|null}}
399
+ */
400
+ verify(source, manifest) {
401
+ if (!this.isConfigured()) {
402
+ return { valid: true, reason: null };
403
+ }
404
+ const signature = manifest && typeof manifest.signature === 'string' ? manifest.signature : '';
405
+ if (!signature) {
406
+ if (this.requireSignature) {
407
+ return { valid: false, reason: 'Plugin is unsigned but verifier requires a signature' };
408
+ }
409
+ return { valid: true, reason: null };
410
+ }
411
+ const ok = verifyPluginSignature(source, signature, this.signingKey);
412
+ return ok
413
+ ? { valid: true, reason: null }
414
+ : { valid: false, reason: 'Plugin signature does not match' };
415
+ }
416
+ }
417
+
418
+ // =========================================================================
419
+ // PLUGIN MANIFEST
420
+ // =========================================================================
421
+
422
+ /**
423
+ * Capability strings a plugin may declare. A plugin that only uses regex
424
+ * matching should declare ['read_text', 'regex_only'] so the host can
425
+ * decide what to trust it with.
426
+ */
427
+ const VALID_CAPABILITIES = new Set([
428
+ 'read_text',
429
+ 'regex_only',
430
+ 'read_options',
431
+ 'network', // explicit — almost always should NOT be granted
432
+ 'filesystem', // explicit — almost always should NOT be granted
433
+ 'require_modules'
434
+ ]);
435
+
436
+ /**
437
+ * Manifest schema helper for plugins. A manifest describes what the plugin
438
+ * is and what it needs access to.
439
+ */
440
+ class PluginManifest {
441
+ /**
442
+ * Validate a manifest object.
443
+ * @param {object} manifest
444
+ * @returns {{valid: boolean, errors: string[]}}
445
+ */
446
+ static validate(manifest) {
447
+ const errors = [];
448
+ if (!manifest || typeof manifest !== 'object') {
449
+ return { valid: false, errors: ['Manifest must be a non-null object'] };
450
+ }
451
+ if (typeof manifest.name !== 'string' || manifest.name.length === 0) {
452
+ errors.push('Manifest "name" must be a non-empty string');
453
+ }
454
+ if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
455
+ errors.push('Manifest "version" must be a non-empty string');
456
+ }
457
+ if (typeof manifest.author !== 'string' || manifest.author.length === 0) {
458
+ errors.push('Manifest "author" must be a non-empty string');
459
+ }
460
+ if (!Array.isArray(manifest.capabilities)) {
461
+ errors.push('Manifest "capabilities" must be an array of strings');
462
+ } else {
463
+ for (const cap of manifest.capabilities) {
464
+ if (typeof cap !== 'string' || !VALID_CAPABILITIES.has(cap)) {
465
+ errors.push(`Unknown capability: ${String(cap)}`);
466
+ }
467
+ }
468
+ }
469
+ if (manifest.signature !== undefined && typeof manifest.signature !== 'string') {
470
+ errors.push('Manifest "signature" must be a string if provided');
471
+ }
472
+ return { valid: errors.length === 0, errors };
473
+ }
474
+
475
+ /**
476
+ * Create a signed manifest by attaching an HMAC signature over `source`.
477
+ * @param {object} manifest - Base manifest (name, version, author, capabilities)
478
+ * @param {string} source - Plugin source code
479
+ * @param {string} key - HMAC secret
480
+ * @returns {object} A new manifest object with a `signature` field
481
+ */
482
+ static sign(manifest, source, key) {
483
+ const { valid, errors } = PluginManifest.validate({ ...manifest, capabilities: manifest.capabilities || [] });
484
+ if (!valid) {
485
+ throw new Error(`[Agent Shield] Cannot sign invalid manifest: ${errors.join('; ')}`);
486
+ }
487
+ return { ...manifest, signature: signPlugin(source, key) };
488
+ }
489
+ }
490
+
85
491
  // =========================================================================
86
492
  // PLUGIN TEMPLATE
87
493
  // =========================================================================
@@ -346,4 +752,14 @@ class PluginManager {
346
752
  // EXPORTS
347
753
  // =========================================================================
348
754
 
349
- module.exports = { PluginManager, PluginTemplate, PluginSandbox };
755
+ module.exports = {
756
+ PluginManager,
757
+ PluginTemplate,
758
+ PluginSandbox,
759
+ IsolatedPluginSandbox,
760
+ PluginVerifier,
761
+ PluginManifest,
762
+ signPlugin,
763
+ verifyPluginSignature,
764
+ VALID_CAPABILITIES
765
+ };
@@ -43,6 +43,14 @@ const KNOWN_BAD_SERVERS = Object.freeze({
43
43
  'postmark-clone': {
44
44
  reason: 'Tool definition bait-and-switch (Postmark-style rugpull)',
45
45
  severity: 'critical'
46
+ },
47
+ 'aws-mcp-server-unpatched': {
48
+ reason: 'Multiple critical RCE vulnerabilities (CVE-2026-5058, CVE-2026-5059)',
49
+ severity: 'critical'
50
+ },
51
+ 'flowise-unpatched': {
52
+ reason: 'CVSS 10.0 RCE actively exploited (CVE-2025-59528)',
53
+ severity: 'critical'
46
54
  }
47
55
  });
48
56
 
@@ -62,6 +70,12 @@ const CVE_REGISTRY = Object.freeze({
62
70
  severity: 'critical',
63
71
  description: 'Azure MCP Server SSRF (CVSS 8.8). Attacker sends crafted URL via tool parameter, server forwards request with managed identity token to attacker-controlled endpoint.',
64
72
  fix: 'Apply March 2026 Patch Tuesday update. Validate all URLs against allowlists. Block private IPs and cloud metadata endpoints (169.254.169.254).'
73
+ },
74
+ {
75
+ cve: 'CVE-2026-32211',
76
+ severity: 'critical',
77
+ description: 'Azure MCP Server lacks authentication entirely (CVSS 9.1), allowing unauthorized access to sensitive data.',
78
+ fix: 'Enable authentication on Azure MCP Server. Never deploy without auth.'
65
79
  }
66
80
  ],
67
81
  'adx-mcp-server': [
@@ -78,6 +92,36 @@ const CVE_REGISTRY = Object.freeze({
78
92
  severity: 'critical',
79
93
  description: 'OpenClaw WebSocket token theft (CVSS 8.8). Control UI accepts gatewayUrl query parameter without validation, redirecting WebSocket to attacker server and leaking auth tokens.',
80
94
  fix: 'Upgrade to OpenClaw >=2026.1.29. Validate gatewayUrl against allowlist. Never pass auth tokens to unvalidated endpoints.'
95
+ },
96
+ {
97
+ cve: 'CVE-2026-33579',
98
+ severity: 'critical',
99
+ description: 'Silent admin takeover. Attacker gains full admin access without detection. Patched April 5 2026.',
100
+ fix: 'Upgrade OpenClaw immediately. Audit admin access logs.'
101
+ },
102
+ {
103
+ cve: 'CVE-2026-24763',
104
+ severity: 'high',
105
+ description: 'Command injection in OpenClaw.',
106
+ fix: 'Upgrade OpenClaw to latest patched version.'
107
+ },
108
+ {
109
+ cve: 'CVE-2026-26322',
110
+ severity: 'high',
111
+ description: 'SSRF in OpenClaw.',
112
+ fix: 'Upgrade OpenClaw to latest patched version.'
113
+ },
114
+ {
115
+ cve: 'CVE-2026-26329',
116
+ severity: 'high',
117
+ description: 'Path traversal enables local file reads in OpenClaw.',
118
+ fix: 'Upgrade OpenClaw to latest patched version.'
119
+ },
120
+ {
121
+ cve: 'CVE-2026-30741',
122
+ severity: 'critical',
123
+ description: 'Prompt-injection-driven code execution in OpenClaw.',
124
+ fix: 'Upgrade OpenClaw to latest patched version.'
81
125
  }
82
126
  ],
83
127
  'mcp-typescript-sdk': [
@@ -133,6 +177,72 @@ const CVE_REGISTRY = Object.freeze({
133
177
  description: 'MCPJam Inspector RCE. HTTP server binds to 0.0.0.0 by default with no authentication on server management endpoint. Any device on the same network can execute arbitrary commands.',
134
178
  fix: 'Upgrade MCPJam Inspector to >=1.4.3. Bind to 127.0.0.1 only. Add authentication to management endpoints.'
135
179
  }
180
+ ],
181
+ 'aws-mcp-server': [
182
+ {
183
+ cve: 'CVE-2026-5058',
184
+ severity: 'critical',
185
+ description: 'Command injection in aws-mcp-server (CVSS 9.8) allows remote code execution without authentication.',
186
+ fix: 'Upgrade aws-mcp-server. Sanitize all CLI arguments. Block shell metacharacters.'
187
+ },
188
+ {
189
+ cve: 'CVE-2026-5059',
190
+ severity: 'critical',
191
+ description: 'Remote code execution in aws-mcp-server via unsanitized inputs.',
192
+ fix: 'Upgrade aws-mcp-server to latest patched version.'
193
+ }
194
+ ],
195
+ 'vscode-mcp': [
196
+ {
197
+ cve: 'CVE-2026-21518',
198
+ severity: 'high',
199
+ description: 'VS Code mcp.json command injection. Opening malicious project executes arbitrary code through mcp.json file handling.',
200
+ fix: 'Update VS Code. Never open untrusted projects. Audit mcp.json files before opening.'
201
+ }
202
+ ],
203
+ 'flowise': [
204
+ {
205
+ cve: 'CVE-2025-59528',
206
+ severity: 'critical',
207
+ description: 'Code injection in Flowise MCP node (CVSS 10.0) allows remote code execution. 12,000-15,000 instances exposed. Actively exploited since April 6.',
208
+ fix: 'Upgrade Flowise to >=3.0.6. Restrict access to MCP node.'
209
+ },
210
+ {
211
+ cve: 'CVE-2025-8943',
212
+ severity: 'critical',
213
+ description: 'Missing authentication in Flowise.',
214
+ fix: 'Upgrade Flowise. Enable authentication.'
215
+ },
216
+ {
217
+ cve: 'CVE-2025-26319',
218
+ severity: 'critical',
219
+ description: 'Arbitrary file upload in Flowise.',
220
+ fix: 'Upgrade Flowise to latest.'
221
+ }
222
+ ],
223
+ 'mcp-data-vis': [
224
+ {
225
+ cve: 'CVE-2026-5322',
226
+ severity: 'high',
227
+ description: 'SQL injection in AlejandroArciniegas mcp-data-vis.',
228
+ fix: 'Avoid using mcp-data-vis or patch SQL query handling.'
229
+ }
230
+ ],
231
+ 'chatbox-mcp': [
232
+ {
233
+ cve: 'CVE-2026-6130',
234
+ severity: 'high',
235
+ description: 'OS command injection in chatboxai chatbox MCP server management.',
236
+ fix: 'Upgrade chatbox. Sanitize all management API inputs.'
237
+ }
238
+ ],
239
+ 'codebase-mcp': [
240
+ {
241
+ cve: 'CVE-2026-5023',
242
+ severity: 'high',
243
+ description: 'OS command injection RCE in codebase-mcp.',
244
+ fix: 'Upgrade codebase-mcp. Never pass unsanitized inputs to shell.'
245
+ }
136
246
  ]
137
247
  });
138
248
 
@@ -175,7 +285,7 @@ const SSRF_PATTERNS = [
175
285
  /^(?:https?:\/\/)?(?:127\.0\.0\.1|0\.0\.0\.0|localhost)/
176
286
  ];
177
287
 
178
- /** Known malicious skill/plugin patterns (ref ClawHavoc campaign — 820+ malicious skills). */
288
+ /** Known malicious skill/plugin patterns (ref ClawHavoc campaign — 1,184+ malicious skills found on ClawHub). */
179
289
  const CLAWHAVOC_INDICATORS = [
180
290
  /(?:reverse.?shell|bind.?shell)/i,
181
291
  /(?:AMOS|atomic.?macos.?stealer)/i,
@@ -817,7 +927,7 @@ class SupplyChainScanner {
817
927
 
818
928
  /**
819
929
  * Scan tool code/description for ClawHavoc-style malicious patterns.
820
- * Ref: 820+ malicious skills found on ClawHub, delivering AMOS stealer.
930
+ * Ref: 1,184+ malicious skills found on ClawHub, delivering AMOS stealer.
821
931
  * @private
822
932
  */
823
933
  _scanForClawHavoc(tool, findings) {
@@ -76,8 +76,7 @@ class SybilDetector {
76
76
  /** @type {Map<string, Array<object>>} */
77
77
  this._actions = new Map();
78
78
 
79
- console.log('%s SybilDetector initialized (threshold: %s, window: %dms, minCluster: %d)',
80
- LOG_PREFIX, this.similarityThreshold, this.timeWindowMs, this.minClusterSize);
79
+ console.log('%s SybilDetector initialized (threshold: %s, window: %dms, minCluster: %d)', LOG_PREFIX, this.similarityThreshold, this.timeWindowMs, this.minClusterSize);
81
80
  }
82
81
 
83
82
  /**
@@ -222,8 +221,7 @@ class SybilDetector {
222
221
  }
223
222
  }
224
223
 
225
- console.log('%s Sybil detection complete: %d cluster(s), risk=%s',
226
- LOG_PREFIX, clusters.length, sybilRisk);
224
+ console.log('%s Sybil detection complete: %d cluster(s), risk=%s', LOG_PREFIX, clusters.length, sybilRisk);
227
225
 
228
226
  return { clusters, sybilRisk };
229
227
  }
@@ -514,8 +512,7 @@ class AgentIdentityVerifier {
514
512
 
515
513
  const hasSharedKeys = sharedKeyGroups.length > 0;
516
514
  if (hasSharedKeys) {
517
- console.log('%s Shared secret detected among %d group(s)',
518
- LOG_PREFIX, sharedKeyGroups.length);
515
+ console.log('%s Shared secret detected among %d group(s)', LOG_PREFIX, sharedKeyGroups.length);
519
516
  }
520
517
 
521
518
  return { sharedKeyGroups, hasSharedKeys };