@drakon-systems/shieldcortex-realtime 4.47.4 → 4.47.5

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.
@@ -143,6 +143,51 @@ export function formatApprovalPrompt(input) {
143
143
  '[Approve] [Deny]',
144
144
  ].join('\n');
145
145
  }
146
+ // --- WS2 fail-closed fallback (guard load/eval failure) ---
147
+ // Deliberately DUPLICATED from tool-action-guard.ts's CATASTROPHIC list, not
148
+ // imported — this file already avoids a compile-time dependency on the main
149
+ // package across the plugin build boundary (see ToolGuardVerdictLike above),
150
+ // and the fallback specifically must keep working when THAT dependency is
151
+ // what's broken. Narrow by design: only the unambiguous, essentially-never-
152
+ // benign catastrophic shapes, so a broken/unwired guard still fails closed on
153
+ // "rm -rf /"-class commands without turning every tool call into a denial
154
+ // whenever the guard is merely unavailable (e.g. mid-upgrade) — that would
155
+ // itself break unattended agents/cron, the outcome ShieldCortex exists to
156
+ // prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
157
+ const FALLBACK_CATASTROPHIC_PATTERNS = [
158
+ /\brm\b[^|;&\n]*?(?:-\w*r\w*f\w*|-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
159
+ /\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
160
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
161
+ /\bmkfs(\.\w+)?\b/i,
162
+ /\bdd\b[^|;&\n]*\bof=\/dev\/(sd|nvme|hd|disk|mmcblk|vd)/i,
163
+ /\b(fdisk|parted|sgdisk|wipefs|blkdiscard)\b/i,
164
+ // Leading `[^\n|]*` (not `[^\n]*`, issue #92 must-fix 1: ReDoS) + `env <assign>
165
+ // <interp>` admitted (issue #92 must-fix 3) — mirrors tool-action-guard.ts's
166
+ // pipe-download-to-shell pattern exactly; kept in sync there.
167
+ /\b(?:curl|wget|fetch)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:env\s+)?(?:\w+=\S*\s+)*(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?!(?:\s+-[a-z]+)*\s+-[cem]\b)/i,
168
+ /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
169
+ ];
170
+ const FALLBACK_SURFACE_KEYS = [
171
+ 'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
172
+ 'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
173
+ 'url', 'uri', 'endpoint', 'href', 'host', 'to',
174
+ ];
175
+ /** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
176
+ function fallbackExecSurface(args) {
177
+ const parts = [];
178
+ for (const k of FALLBACK_SURFACE_KEYS) {
179
+ const v = args?.[k];
180
+ if (typeof v === 'string' && v.length > 0)
181
+ parts.push(v);
182
+ }
183
+ return parts.join(' ');
184
+ }
185
+ function fallbackCatastrophicMatch(args) {
186
+ const text = fallbackExecSurface(args);
187
+ if (!text)
188
+ return false;
189
+ return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
190
+ }
146
191
  /** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
147
192
  export function summariseToolArgs(args) {
148
193
  if (!args)
@@ -247,19 +292,43 @@ export function createInterceptor(config, pipeline, options) {
247
292
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
248
293
  };
249
294
  }
295
+ // WS2 fail-closed path: when the real guard was never wired in or throws,
296
+ // run the narrow fallback scan and DENY (throw) a catastrophic-looking
297
+ // command instead of silently allowing it. The fallback recognising nothing
298
+ // is not evidence the command is safe, only that it isn't one of the
299
+ // handful of unambiguous shapes — everything else still falls through to
300
+ // the pre-existing fail-OPEN log-and-allow.
301
+ function handleGuardUnavailable(context, reason) {
302
+ if (!fallbackCatastrophicMatch(context.arguments)) {
303
+ log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — allowing ${context.toolName}`);
304
+ return;
305
+ }
306
+ const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
307
+ emitAudit({
308
+ type: 'intercept', tool: context.toolName, severity: 'critical',
309
+ firewallResult: 'ACTION_GUARD_FALLBACK', threats: ['fallback-scan'],
310
+ anomalyScore: 1, trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null,
311
+ pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
312
+ action: 'auto_deny', outcome: 'auto_denied',
313
+ });
314
+ log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
315
+ throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
316
+ }
250
317
  // Action Guard: gates non-memory tool calls (shell / file / network / git).
251
318
  // This is what makes "Iron Dome protects what the agent DOES" true at runtime.
252
319
  async function runActionGuard(context) {
253
- if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function')
320
+ if (!actionGuardCfg.enabled)
321
+ return;
322
+ if (typeof evaluateToolCall !== 'function') {
323
+ handleGuardUnavailable(context, 'evaluateToolCall not wired');
254
324
  return;
325
+ }
255
326
  let v;
256
327
  try {
257
328
  v = evaluateToolCall(context.toolName, context.arguments || {});
258
329
  }
259
330
  catch (err) {
260
- // A guard error must never break the agent — log and allow. (The memory
261
- // pipeline is the hard-fail path; the action guard is best-effort.)
262
- log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
331
+ handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
263
332
  return;
264
333
  }
265
334
  if (v.decision === 'allow')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.4",
3
+ "version": "4.47.5",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
package/interceptor.ts CHANGED
@@ -247,6 +247,53 @@ export function formatApprovalPrompt(input: ApprovalPromptInput): string {
247
247
  ].join('\n');
248
248
  }
249
249
 
250
+ // --- WS2 fail-closed fallback (guard load/eval failure) ---
251
+ // Deliberately DUPLICATED from tool-action-guard.ts's CATASTROPHIC list, not
252
+ // imported — this file already avoids a compile-time dependency on the main
253
+ // package across the plugin build boundary (see ToolGuardVerdictLike above),
254
+ // and the fallback specifically must keep working when THAT dependency is
255
+ // what's broken. Narrow by design: only the unambiguous, essentially-never-
256
+ // benign catastrophic shapes, so a broken/unwired guard still fails closed on
257
+ // "rm -rf /"-class commands without turning every tool call into a denial
258
+ // whenever the guard is merely unavailable (e.g. mid-upgrade) — that would
259
+ // itself break unattended agents/cron, the outcome ShieldCortex exists to
260
+ // prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
261
+ const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
262
+ /\brm\b[^|;&\n]*?(?:-\w*r\w*f\w*|-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
263
+ /\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
264
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
265
+ /\bmkfs(\.\w+)?\b/i,
266
+ /\bdd\b[^|;&\n]*\bof=\/dev\/(sd|nvme|hd|disk|mmcblk|vd)/i,
267
+ /\b(fdisk|parted|sgdisk|wipefs|blkdiscard)\b/i,
268
+ // Leading `[^\n|]*` (not `[^\n]*`, issue #92 must-fix 1: ReDoS) + `env <assign>
269
+ // <interp>` admitted (issue #92 must-fix 3) — mirrors tool-action-guard.ts's
270
+ // pipe-download-to-shell pattern exactly; kept in sync there.
271
+ /\b(?:curl|wget|fetch)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:env\s+)?(?:\w+=\S*\s+)*(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?!(?:\s+-[a-z]+)*\s+-[cem]\b)/i,
272
+ /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
273
+ ];
274
+
275
+ const FALLBACK_SURFACE_KEYS = [
276
+ 'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
277
+ 'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
278
+ 'url', 'uri', 'endpoint', 'href', 'host', 'to',
279
+ ];
280
+
281
+ /** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
282
+ function fallbackExecSurface(args: Record<string, unknown> | undefined): string {
283
+ const parts: string[] = [];
284
+ for (const k of FALLBACK_SURFACE_KEYS) {
285
+ const v = args?.[k];
286
+ if (typeof v === 'string' && v.length > 0) parts.push(v);
287
+ }
288
+ return parts.join(' ');
289
+ }
290
+
291
+ function fallbackCatastrophicMatch(args: Record<string, unknown> | undefined): boolean {
292
+ const text = fallbackExecSurface(args);
293
+ if (!text) return false;
294
+ return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
295
+ }
296
+
250
297
  /** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
251
298
  export function summariseToolArgs(args: Record<string, unknown> | undefined): string {
252
299
  if (!args) return '';
@@ -403,18 +450,44 @@ export function createInterceptor(
403
450
  };
404
451
  }
405
452
 
453
+ // WS2 fail-closed path: when the real guard was never wired in or throws,
454
+ // run the narrow fallback scan and DENY (throw) a catastrophic-looking
455
+ // command instead of silently allowing it. The fallback recognising nothing
456
+ // is not evidence the command is safe, only that it isn't one of the
457
+ // handful of unambiguous shapes — everything else still falls through to
458
+ // the pre-existing fail-OPEN log-and-allow.
459
+ function handleGuardUnavailable(context: ToolCallContext, reason: string): void {
460
+ if (!fallbackCatastrophicMatch(context.arguments)) {
461
+ log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — allowing ${context.toolName}`);
462
+ return;
463
+ }
464
+ const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
465
+ emitAudit({
466
+ type: 'intercept', tool: context.toolName, severity: 'critical',
467
+ firewallResult: 'ACTION_GUARD_FALLBACK', threats: ['fallback-scan'],
468
+ anomalyScore: 1, trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null,
469
+ pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
470
+ action: 'auto_deny', outcome: 'auto_denied',
471
+ });
472
+ log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
473
+ throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
474
+ }
475
+
406
476
  // Action Guard: gates non-memory tool calls (shell / file / network / git).
407
477
  // This is what makes "Iron Dome protects what the agent DOES" true at runtime.
408
478
  async function runActionGuard(context: ToolCallContext): Promise<void> {
409
- if (!actionGuardCfg.enabled || typeof evaluateToolCall !== 'function') return;
479
+ if (!actionGuardCfg.enabled) return;
480
+
481
+ if (typeof evaluateToolCall !== 'function') {
482
+ handleGuardUnavailable(context, 'evaluateToolCall not wired');
483
+ return;
484
+ }
410
485
 
411
486
  let v: ToolGuardVerdictLike;
412
487
  try {
413
488
  v = evaluateToolCall(context.toolName, context.arguments || {});
414
489
  } catch (err) {
415
- // A guard error must never break the agent — log and allow. (The memory
416
- // pipeline is the hard-fail path; the action guard is best-effort.)
417
- log.warn(`[shieldcortex] ⚠️ action-guard error (allowing ${context.toolName}): ${err instanceof Error ? err.message : err}`);
490
+ handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
418
491
  return;
419
492
  }
420
493
  if (v.decision === 'allow') return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.4",
3
+ "version": "4.47.5",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
5
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "kind": null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.4",
3
+ "version": "4.47.5",
4
4
  "description": "OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory extraction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",