@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.
- package/dist/interceptor.js +73 -4
- package/dist/openclaw.plugin.json +1 -1
- package/interceptor.ts +77 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/interceptor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
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