@drakon-systems/shieldcortex-realtime 4.47.8 → 4.47.9
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 +97 -20
- package/dist/openclaw.plugin.json +1 -1
- package/interceptor.ts +102 -21
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/interceptor.js
CHANGED
|
@@ -156,7 +156,7 @@ export function formatApprovalPrompt(input) {
|
|
|
156
156
|
// itself break unattended agents/cron, the outcome ShieldCortex exists to
|
|
157
157
|
// prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
|
|
158
158
|
const FALLBACK_CATASTROPHIC_PATTERNS = [
|
|
159
|
-
/\brm\b[^|;&\n]*?(
|
|
159
|
+
/\brm\b[^|;&\n]*?(?:(?<![\w.\/-])-\w*r\w*f\w*|(?<![\w.\/-])-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
|
|
160
160
|
/\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
|
|
161
161
|
/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
|
|
162
162
|
/\bmkfs(\.\w+)?\b/i,
|
|
@@ -171,11 +171,49 @@ const FALLBACK_CATASTROPHIC_PATTERNS = [
|
|
|
171
171
|
/\b(?:curl|wget|fetch)\b[^|\n]*\|[^\n]*\bpython\d?\b[^\n]*\s-m\s*(?:code|pty|pdb)(?![\w.])/i,
|
|
172
172
|
/\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
|
|
173
173
|
];
|
|
174
|
+
// WS2 dangerous-tier fallback (issue #59). Ports EVERY signal in
|
|
175
|
+
// tool-action-guard.ts's DANGEROUS array — a drift test
|
|
176
|
+
// (ws2-gate-degraded-integration-59) fails if the guard gains a DANGEROUS
|
|
177
|
+
// signal this list doesn't cover, so "kept in sync" is enforced, not just
|
|
178
|
+
// claimed. Used ONLY when the real guard can't scan: a recognised-dangerous
|
|
179
|
+
// shape routes through `failurePolicy` exactly as an unattended real verdict
|
|
180
|
+
// would, instead of the pre-#59 fail-OPEN. Copies the real (already anti-ReDoS,
|
|
181
|
+
// FP-narrowed) patterns verbatim, so read-only forms — `crontab -l`, `npm ls
|
|
182
|
+
// -g`, `--global-style`, shred use/mention — still pass. The one guard shape
|
|
183
|
+
// NOT here is bare `npx`/`bunx`: those are gated CONDITIONALLY by
|
|
184
|
+
// `isGatedNpxBunx` (shape-based, #96), not by a DANGEROUS pattern, and a blunt
|
|
185
|
+
// fallback matching them would over-gate `npx tsc`; `uvx`/`dlx` (unconditional)
|
|
186
|
+
// ARE covered. Mirrored in scripts/pre-tool-hook.mjs + hermes/sc_client.py.
|
|
187
|
+
const FALLBACK_DANGEROUS_PATTERNS = [
|
|
188
|
+
{ re: /\brm\b|\bunlink\b|\brmdir\b|(?:(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?|\bxargs\s+(?:-{1,2}\S+\s+)*|-exec\s+)shred\b/i, signal: 'file-delete' },
|
|
189
|
+
{ re: /\bsudo\b|\bdoas\b|\bsu\s/i, signal: 'privilege-escalation' },
|
|
190
|
+
{ re: /\bgit\b[^|\n]*\bpush\b[^|\n]*(--force\b|-f\b|\+)/i, signal: 'git-force-push' },
|
|
191
|
+
{ re: /\bgit\b[^|\n]*\b(branch\s+-D|push\b[^|\n]*--delete|push\b[^|\n]*\s:)/i, signal: 'git-delete-branch' },
|
|
192
|
+
{ re: /\b(systemctl|service)\b[^|\n]*\b(stop|disable|mask)\b|\b(kill|pkill|killall)\b/i, signal: 'stop-process-or-service' },
|
|
193
|
+
{ re: /\b(iptables|ufw|nft|netplan|firewall-cmd)\b/i, signal: 'modify-network-firewall' },
|
|
194
|
+
{ re: /\b(?:apt|apt-get|yum|dnf|brew|pip|pip3|gem|cargo)\b[^|\n]*\b(?:install|add)\b/i, signal: 'install-package' },
|
|
195
|
+
{ re: /\b(?:npm|yarn|pnpm|bun)\b(?=[^|;&\n]*(?:\s['"]?-g\b['"]?|--global(?![\w-])|\bglobal\s+add\b))(?=[^|;&\n]*\s(?:install|add)(?=\s|$|[|;&\n]))|\b(?:npm|pnpm|bun)\s+(?:i(?:n(?:s(?:t(?:a(?:ll?)?)?)?)?)?|isnt(?:all)?)\b[^|;&\n]*(?:\s['"]?-g\b['"]?|--global(?![\w-]))/i, signal: 'install-package-global' },
|
|
196
|
+
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:(?:env|nohup|time|stdbuf|nice)\b(?:\s+(?:-{1,2}\S+|\w+=\S*|\d+))*\s+)*(?:sudo\s+)?(?:crontab\b(?!\s+-l\b)|at\b(?!\s+-l\b)(?!\s*$))|\/etc\/cron|\bsystemd-run\b[^|;&\n]*--on-(?:calendar|active|boot|startup|unit-active|unit-inactive)\b/i, signal: 'modify-scheduler' },
|
|
197
|
+
{ re: /\bdd\b[^|;&\n]*\bof=/i, signal: 'dd-overwrite' },
|
|
198
|
+
{ re: /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:etc|usr|var|home|bin|sbin|boot|lib|lib64|opt|root)(?:\/\*?)?(?:\s|$)/i, signal: 'recursive-perms-system-dir' },
|
|
199
|
+
{ re: /\btruncate\b[^|;&\n]*(?:-s\s*0\b|--size(?:=|\s+)0\b)/i, signal: 'truncate-to-zero' },
|
|
200
|
+
{ re: /\bhistory\s+-c\b|\.bash_history|truncate\b[^|\n]*\.log/i, signal: 'wipe-history-or-logs' },
|
|
201
|
+
{ re: /\/etc\/(passwd|shadow|sudoers)|~\/\.ssh|id_rsa|\.aws\/credentials|\.env\b/i, signal: 'touch-sensitive-path' },
|
|
202
|
+
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?uvx\b/i, signal: 'registry-code-exec' },
|
|
203
|
+
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:pnpm|yarn)\b[^|;&\n]*\bdlx\b/i, signal: 'registry-code-exec' },
|
|
204
|
+
{ re: /\b(?:base64|openssl|xxd|cat|http)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?:\s+-)?\s*(?:[;&|\n]|$)/i, signal: 'decode-pipe-to-shell' },
|
|
205
|
+
];
|
|
174
206
|
const FALLBACK_SURFACE_KEYS = [
|
|
175
207
|
'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
|
|
176
208
|
'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
|
|
177
209
|
'url', 'uri', 'endpoint', 'href', 'host', 'to',
|
|
178
210
|
];
|
|
211
|
+
// The fallback is an outage-only blunt scanner; an UNBOUNDED scan over crafted
|
|
212
|
+
// input is a ReDoS vector (some ported guard patterns are O(n²) on pathological
|
|
213
|
+
// token runs like `git push push push …`). Dangerous/catastrophic shapes appear
|
|
214
|
+
// early in any real command, so cap the scanned surface — 4 KB is far beyond a
|
|
215
|
+
// real shell command. The real guard (no cap) still scans in full when it works.
|
|
216
|
+
const FALLBACK_SCAN_CAP = 4096;
|
|
179
217
|
/** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
|
|
180
218
|
function fallbackExecSurface(args) {
|
|
181
219
|
const parts = [];
|
|
@@ -184,7 +222,7 @@ function fallbackExecSurface(args) {
|
|
|
184
222
|
if (typeof v === 'string' && v.length > 0)
|
|
185
223
|
parts.push(v);
|
|
186
224
|
}
|
|
187
|
-
return parts.join(' ');
|
|
225
|
+
return parts.join(' ').slice(0, FALLBACK_SCAN_CAP);
|
|
188
226
|
}
|
|
189
227
|
function fallbackCatastrophicMatch(args) {
|
|
190
228
|
const text = fallbackExecSurface(args);
|
|
@@ -192,6 +230,17 @@ function fallbackCatastrophicMatch(args) {
|
|
|
192
230
|
return false;
|
|
193
231
|
return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
|
|
194
232
|
}
|
|
233
|
+
/** First matching dangerous signal for the WS2 fallback, or null (issue #59). */
|
|
234
|
+
function fallbackDangerousMatch(args) {
|
|
235
|
+
const text = fallbackExecSurface(args);
|
|
236
|
+
if (!text)
|
|
237
|
+
return null;
|
|
238
|
+
for (const { re, signal } of FALLBACK_DANGEROUS_PATTERNS) {
|
|
239
|
+
if (re.test(text))
|
|
240
|
+
return signal;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
195
244
|
/** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
|
|
196
245
|
export function summariseToolArgs(args) {
|
|
197
246
|
if (!args)
|
|
@@ -311,27 +360,55 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
311
360
|
preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
312
361
|
};
|
|
313
362
|
}
|
|
314
|
-
// WS2 fail-closed path: when the real guard was never wired in or
|
|
315
|
-
// run the
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
363
|
+
// WS2 fail-closed path (issue #59): when the real guard was never wired in or
|
|
364
|
+
// throws, run the dependency-free fallback scan. Three tiers, so no dangerous
|
|
365
|
+
// op is ever silently allowed on a scan failure — and every could-not-scan
|
|
366
|
+
// decision leaves a `gate_degraded` audit row (ACTION_GUARD_FALLBACK marker),
|
|
367
|
+
// so forensics can distinguish "scanned & allowed" from "could not scan":
|
|
368
|
+
// 1. catastrophic → hard deny, always (ignores enforce:false).
|
|
369
|
+
// 2. dangerous → route through `failurePolicy` exactly as an unattended
|
|
370
|
+
// real verdict would (deny by default); enforce:false
|
|
371
|
+
// opts down to advisory.
|
|
372
|
+
// 3. no match → benign/unknown: fail OPEN (a degraded guard must not
|
|
373
|
+
// wedge normal work) but leave a visible breadcrumb.
|
|
320
374
|
function handleGuardUnavailable(context, reason) {
|
|
321
|
-
|
|
322
|
-
|
|
375
|
+
const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`.slice(0, 200);
|
|
376
|
+
const degradedBase = {
|
|
377
|
+
type: 'intercept', tool: context.toolName,
|
|
378
|
+
firewallResult: 'ACTION_GUARD_FALLBACK', trustScore: 0, sensitivityLevel: 'INTERNAL',
|
|
379
|
+
fragmentationScore: null, pipelineDurationMs: 0, preview, ts: new Date().toISOString(),
|
|
380
|
+
};
|
|
381
|
+
// 1. Catastrophic — hard deny, always.
|
|
382
|
+
if (fallbackCatastrophicMatch(context.arguments)) {
|
|
383
|
+
emitAudit({
|
|
384
|
+
...degradedBase, severity: 'critical', threats: ['fallback-scan'], anomalyScore: 1,
|
|
385
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
386
|
+
});
|
|
387
|
+
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
|
|
388
|
+
throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
|
|
389
|
+
}
|
|
390
|
+
// 2. Dangerous — route through failurePolicy (the "can't obtain a verdict"
|
|
391
|
+
// policy; a degraded guard is precisely that). enforce:false → advisory.
|
|
392
|
+
const dangerousSignal = fallbackDangerousMatch(context.arguments);
|
|
393
|
+
if (dangerousSignal) {
|
|
394
|
+
const dBase = { ...degradedBase, severity: 'high', threats: ['fallback-scan', dangerousSignal], anomalyScore: 0.6 };
|
|
395
|
+
if (!actionGuardCfg.enforce) {
|
|
396
|
+
emitAudit({ ...dBase, action: 'gate_degraded', outcome: 'failure_allowed' });
|
|
397
|
+
log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — advisory (enforce:false), allowing dangerous ${context.toolName} [${dangerousSignal}]`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const failAction = config.failurePolicy.high;
|
|
401
|
+
emitAudit({ ...dBase, action: 'gate_degraded', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
|
|
402
|
+
if (failAction === 'deny') {
|
|
403
|
+
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback matched a DANGEROUS op [${dangerousSignal}] — DENYING ${context.toolName} (fail-closed, failure policy: deny)`);
|
|
404
|
+
throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), dangerous fallback match [${dangerousSignal}], failure policy: deny`);
|
|
405
|
+
}
|
|
323
406
|
return;
|
|
324
407
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
anomalyScore: 1, trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null,
|
|
330
|
-
pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
331
|
-
action: 'auto_deny', outcome: 'auto_denied',
|
|
332
|
-
});
|
|
333
|
-
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
|
|
334
|
-
throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
|
|
408
|
+
// 3. No match — benign/unknown. Fail open, but never silently: the
|
|
409
|
+
// gate_degraded breadcrumb makes the outage window auditable.
|
|
410
|
+
emitAudit({ ...degradedBase, severity: 'low', threats: ['fallback-scan'], anomalyScore: 0.1, action: 'gate_degraded', outcome: 'failure_allowed' });
|
|
411
|
+
log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — allowing ${context.toolName} (fallback matched nothing; fail-open)`);
|
|
335
412
|
}
|
|
336
413
|
// Action Guard: gates non-memory tool calls (shell / file / network / git).
|
|
337
414
|
// This is what makes "Iron Dome protects what the agent DOES" true at runtime.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.9",
|
|
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
|
@@ -69,7 +69,7 @@ export interface InterceptAuditEntry {
|
|
|
69
69
|
sensitivityLevel: string; // from the pipeline result's sensitivity level
|
|
70
70
|
fragmentationScore: number | null; // from the pipeline result's fragmentation score, or null
|
|
71
71
|
pipelineDurationMs: number; // wall-clock ms around the runDefencePipeline call
|
|
72
|
-
action: InterceptAction | 'auto_deny' | 'rate_limit' | 'allow';
|
|
72
|
+
action: InterceptAction | 'auto_deny' | 'rate_limit' | 'allow' | 'gate_degraded';
|
|
73
73
|
outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied' | 'allowed';
|
|
74
74
|
preview: string;
|
|
75
75
|
ts: string;
|
|
@@ -263,7 +263,7 @@ export function formatApprovalPrompt(input: ApprovalPromptInput): string {
|
|
|
263
263
|
// itself break unattended agents/cron, the outcome ShieldCortex exists to
|
|
264
264
|
// prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
|
|
265
265
|
const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
|
|
266
|
-
/\brm\b[^|;&\n]*?(
|
|
266
|
+
/\brm\b[^|;&\n]*?(?:(?<![\w.\/-])-\w*r\w*f\w*|(?<![\w.\/-])-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
|
|
267
267
|
/\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
|
|
268
268
|
/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
|
|
269
269
|
/\bmkfs(\.\w+)?\b/i,
|
|
@@ -279,12 +279,52 @@ const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
|
|
|
279
279
|
/\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
|
|
280
280
|
];
|
|
281
281
|
|
|
282
|
+
// WS2 dangerous-tier fallback (issue #59). Ports EVERY signal in
|
|
283
|
+
// tool-action-guard.ts's DANGEROUS array — a drift test
|
|
284
|
+
// (ws2-gate-degraded-integration-59) fails if the guard gains a DANGEROUS
|
|
285
|
+
// signal this list doesn't cover, so "kept in sync" is enforced, not just
|
|
286
|
+
// claimed. Used ONLY when the real guard can't scan: a recognised-dangerous
|
|
287
|
+
// shape routes through `failurePolicy` exactly as an unattended real verdict
|
|
288
|
+
// would, instead of the pre-#59 fail-OPEN. Copies the real (already anti-ReDoS,
|
|
289
|
+
// FP-narrowed) patterns verbatim, so read-only forms — `crontab -l`, `npm ls
|
|
290
|
+
// -g`, `--global-style`, shred use/mention — still pass. The one guard shape
|
|
291
|
+
// NOT here is bare `npx`/`bunx`: those are gated CONDITIONALLY by
|
|
292
|
+
// `isGatedNpxBunx` (shape-based, #96), not by a DANGEROUS pattern, and a blunt
|
|
293
|
+
// fallback matching them would over-gate `npx tsc`; `uvx`/`dlx` (unconditional)
|
|
294
|
+
// ARE covered. Mirrored in scripts/pre-tool-hook.mjs + hermes/sc_client.py.
|
|
295
|
+
const FALLBACK_DANGEROUS_PATTERNS: Array<{ re: RegExp; signal: string }> = [
|
|
296
|
+
{ re: /\brm\b|\bunlink\b|\brmdir\b|(?:(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?|\bxargs\s+(?:-{1,2}\S+\s+)*|-exec\s+)shred\b/i, signal: 'file-delete' },
|
|
297
|
+
{ re: /\bsudo\b|\bdoas\b|\bsu\s/i, signal: 'privilege-escalation' },
|
|
298
|
+
{ re: /\bgit\b[^|\n]*\bpush\b[^|\n]*(--force\b|-f\b|\+)/i, signal: 'git-force-push' },
|
|
299
|
+
{ re: /\bgit\b[^|\n]*\b(branch\s+-D|push\b[^|\n]*--delete|push\b[^|\n]*\s:)/i, signal: 'git-delete-branch' },
|
|
300
|
+
{ re: /\b(systemctl|service)\b[^|\n]*\b(stop|disable|mask)\b|\b(kill|pkill|killall)\b/i, signal: 'stop-process-or-service' },
|
|
301
|
+
{ re: /\b(iptables|ufw|nft|netplan|firewall-cmd)\b/i, signal: 'modify-network-firewall' },
|
|
302
|
+
{ re: /\b(?:apt|apt-get|yum|dnf|brew|pip|pip3|gem|cargo)\b[^|\n]*\b(?:install|add)\b/i, signal: 'install-package' },
|
|
303
|
+
{ re: /\b(?:npm|yarn|pnpm|bun)\b(?=[^|;&\n]*(?:\s['"]?-g\b['"]?|--global(?![\w-])|\bglobal\s+add\b))(?=[^|;&\n]*\s(?:install|add)(?=\s|$|[|;&\n]))|\b(?:npm|pnpm|bun)\s+(?:i(?:n(?:s(?:t(?:a(?:ll?)?)?)?)?)?|isnt(?:all)?)\b[^|;&\n]*(?:\s['"]?-g\b['"]?|--global(?![\w-]))/i, signal: 'install-package-global' },
|
|
304
|
+
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:(?:env|nohup|time|stdbuf|nice)\b(?:\s+(?:-{1,2}\S+|\w+=\S*|\d+))*\s+)*(?:sudo\s+)?(?:crontab\b(?!\s+-l\b)|at\b(?!\s+-l\b)(?!\s*$))|\/etc\/cron|\bsystemd-run\b[^|;&\n]*--on-(?:calendar|active|boot|startup|unit-active|unit-inactive)\b/i, signal: 'modify-scheduler' },
|
|
305
|
+
{ re: /\bdd\b[^|;&\n]*\bof=/i, signal: 'dd-overwrite' },
|
|
306
|
+
{ re: /\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:etc|usr|var|home|bin|sbin|boot|lib|lib64|opt|root)(?:\/\*?)?(?:\s|$)/i, signal: 'recursive-perms-system-dir' },
|
|
307
|
+
{ re: /\btruncate\b[^|;&\n]*(?:-s\s*0\b|--size(?:=|\s+)0\b)/i, signal: 'truncate-to-zero' },
|
|
308
|
+
{ re: /\bhistory\s+-c\b|\.bash_history|truncate\b[^|\n]*\.log/i, signal: 'wipe-history-or-logs' },
|
|
309
|
+
{ re: /\/etc\/(passwd|shadow|sudoers)|~\/\.ssh|id_rsa|\.aws\/credentials|\.env\b/i, signal: 'touch-sensitive-path' },
|
|
310
|
+
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?uvx\b/i, signal: 'registry-code-exec' },
|
|
311
|
+
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:pnpm|yarn)\b[^|;&\n]*\bdlx\b/i, signal: 'registry-code-exec' },
|
|
312
|
+
{ re: /\b(?:base64|openssl|xxd|cat|http)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?:\s+-)?\s*(?:[;&|\n]|$)/i, signal: 'decode-pipe-to-shell' },
|
|
313
|
+
];
|
|
314
|
+
|
|
282
315
|
const FALLBACK_SURFACE_KEYS = [
|
|
283
316
|
'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
|
|
284
317
|
'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
|
|
285
318
|
'url', 'uri', 'endpoint', 'href', 'host', 'to',
|
|
286
319
|
];
|
|
287
320
|
|
|
321
|
+
// The fallback is an outage-only blunt scanner; an UNBOUNDED scan over crafted
|
|
322
|
+
// input is a ReDoS vector (some ported guard patterns are O(n²) on pathological
|
|
323
|
+
// token runs like `git push push push …`). Dangerous/catastrophic shapes appear
|
|
324
|
+
// early in any real command, so cap the scanned surface — 4 KB is far beyond a
|
|
325
|
+
// real shell command. The real guard (no cap) still scans in full when it works.
|
|
326
|
+
const FALLBACK_SCAN_CAP = 4096;
|
|
327
|
+
|
|
288
328
|
/** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
|
|
289
329
|
function fallbackExecSurface(args: Record<string, unknown> | undefined): string {
|
|
290
330
|
const parts: string[] = [];
|
|
@@ -292,7 +332,7 @@ function fallbackExecSurface(args: Record<string, unknown> | undefined): string
|
|
|
292
332
|
const v = args?.[k];
|
|
293
333
|
if (typeof v === 'string' && v.length > 0) parts.push(v);
|
|
294
334
|
}
|
|
295
|
-
return parts.join(' ');
|
|
335
|
+
return parts.join(' ').slice(0, FALLBACK_SCAN_CAP);
|
|
296
336
|
}
|
|
297
337
|
|
|
298
338
|
function fallbackCatastrophicMatch(args: Record<string, unknown> | undefined): boolean {
|
|
@@ -301,6 +341,16 @@ function fallbackCatastrophicMatch(args: Record<string, unknown> | undefined): b
|
|
|
301
341
|
return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
|
|
302
342
|
}
|
|
303
343
|
|
|
344
|
+
/** First matching dangerous signal for the WS2 fallback, or null (issue #59). */
|
|
345
|
+
function fallbackDangerousMatch(args: Record<string, unknown> | undefined): string | null {
|
|
346
|
+
const text = fallbackExecSurface(args);
|
|
347
|
+
if (!text) return null;
|
|
348
|
+
for (const { re, signal } of FALLBACK_DANGEROUS_PATTERNS) {
|
|
349
|
+
if (re.test(text)) return signal;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
304
354
|
/** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
|
|
305
355
|
export function summariseToolArgs(args: Record<string, unknown> | undefined): string {
|
|
306
356
|
if (!args) return '';
|
|
@@ -475,27 +525,58 @@ export function createInterceptor(
|
|
|
475
525
|
};
|
|
476
526
|
}
|
|
477
527
|
|
|
478
|
-
// WS2 fail-closed path: when the real guard was never wired in or
|
|
479
|
-
// run the
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
528
|
+
// WS2 fail-closed path (issue #59): when the real guard was never wired in or
|
|
529
|
+
// throws, run the dependency-free fallback scan. Three tiers, so no dangerous
|
|
530
|
+
// op is ever silently allowed on a scan failure — and every could-not-scan
|
|
531
|
+
// decision leaves a `gate_degraded` audit row (ACTION_GUARD_FALLBACK marker),
|
|
532
|
+
// so forensics can distinguish "scanned & allowed" from "could not scan":
|
|
533
|
+
// 1. catastrophic → hard deny, always (ignores enforce:false).
|
|
534
|
+
// 2. dangerous → route through `failurePolicy` exactly as an unattended
|
|
535
|
+
// real verdict would (deny by default); enforce:false
|
|
536
|
+
// opts down to advisory.
|
|
537
|
+
// 3. no match → benign/unknown: fail OPEN (a degraded guard must not
|
|
538
|
+
// wedge normal work) but leave a visible breadcrumb.
|
|
484
539
|
function handleGuardUnavailable(context: ToolCallContext, reason: string): void {
|
|
485
|
-
|
|
486
|
-
|
|
540
|
+
const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`.slice(0, 200);
|
|
541
|
+
const degradedBase = {
|
|
542
|
+
type: 'intercept' as const, tool: context.toolName,
|
|
543
|
+
firewallResult: 'ACTION_GUARD_FALLBACK', trustScore: 0, sensitivityLevel: 'INTERNAL',
|
|
544
|
+
fragmentationScore: null, pipelineDurationMs: 0, preview, ts: new Date().toISOString(),
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// 1. Catastrophic — hard deny, always.
|
|
548
|
+
if (fallbackCatastrophicMatch(context.arguments)) {
|
|
549
|
+
emitAudit({
|
|
550
|
+
...degradedBase, severity: 'critical', threats: ['fallback-scan'], anomalyScore: 1,
|
|
551
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
552
|
+
});
|
|
553
|
+
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
|
|
554
|
+
throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 2. Dangerous — route through failurePolicy (the "can't obtain a verdict"
|
|
558
|
+
// policy; a degraded guard is precisely that). enforce:false → advisory.
|
|
559
|
+
const dangerousSignal = fallbackDangerousMatch(context.arguments);
|
|
560
|
+
if (dangerousSignal) {
|
|
561
|
+
const dBase = { ...degradedBase, severity: 'high' as Severity, threats: ['fallback-scan', dangerousSignal], anomalyScore: 0.6 };
|
|
562
|
+
if (!actionGuardCfg.enforce) {
|
|
563
|
+
emitAudit({ ...dBase, action: 'gate_degraded', outcome: 'failure_allowed' });
|
|
564
|
+
log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — advisory (enforce:false), allowing dangerous ${context.toolName} [${dangerousSignal}]`);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const failAction = config.failurePolicy.high;
|
|
568
|
+
emitAudit({ ...dBase, action: 'gate_degraded', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
|
|
569
|
+
if (failAction === 'deny') {
|
|
570
|
+
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback matched a DANGEROUS op [${dangerousSignal}] — DENYING ${context.toolName} (fail-closed, failure policy: deny)`);
|
|
571
|
+
throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), dangerous fallback match [${dangerousSignal}], failure policy: deny`);
|
|
572
|
+
}
|
|
487
573
|
return;
|
|
488
574
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
495
|
-
action: 'auto_deny', outcome: 'auto_denied',
|
|
496
|
-
});
|
|
497
|
-
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
|
|
498
|
-
throw new Error(`ShieldCortex: tool call blocked — action guard unavailable (${reason}), fallback catastrophic scan matched`);
|
|
575
|
+
|
|
576
|
+
// 3. No match — benign/unknown. Fail open, but never silently: the
|
|
577
|
+
// gate_degraded breadcrumb makes the outage window auditable.
|
|
578
|
+
emitAudit({ ...degradedBase, severity: 'low', threats: ['fallback-scan'], anomalyScore: 0.1, action: 'gate_degraded', outcome: 'failure_allowed' });
|
|
579
|
+
log.warn(`[shieldcortex] ⚠️ action-guard unavailable (${reason}) — allowing ${context.toolName} (fallback matched nothing; fail-open)`);
|
|
499
580
|
}
|
|
500
581
|
|
|
501
582
|
// Action Guard: gates non-memory tool calls (shell / file / network / git).
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.9",
|
|
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