@drakon-systems/shieldcortex-realtime 4.47.7 → 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/README.md +1 -0
- package/dist/interceptor.js +128 -25
- package/dist/openclaw.plugin.json +11 -1
- package/interceptor.ts +141 -27
- package/openclaw.plugin.json +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,7 @@ Supported plugin config keys:
|
|
|
113
113
|
- `actionGuard.enabled`: turn the before-tool-call Action Guard on or off (default `true`)
|
|
114
114
|
- `actionGuard.enforce`: enforce dangerous-operation gating (default `true`); `false` opts down to warn-and-allow. Catastrophic operations are blocked regardless.
|
|
115
115
|
- `actionGuard.autoApprove`: array of operation allowlist entries for unattended agents that legitimately need specific dangerous operations
|
|
116
|
+
- `actionGuard.auditAllows`: audit recognised (sensitive-tier) allow-decisions so "scanned & allowed" is distinguishable from "never scanned" (default `true`; benign allows are never audited)
|
|
116
117
|
- `failurePolicy`: per-severity verdict when a decision can't be obtained unattended (defaults: `low`/`medium` allow, `high`/`critical` deny)
|
|
117
118
|
|
|
118
119
|
## Auto-memory
|
package/dist/interceptor.js
CHANGED
|
@@ -35,6 +35,7 @@ const DEFAULT_CONFIG = {
|
|
|
35
35
|
enabled: true,
|
|
36
36
|
enforce: true,
|
|
37
37
|
autoApprove: [],
|
|
38
|
+
auditAllows: true,
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
|
|
@@ -155,7 +156,7 @@ export function formatApprovalPrompt(input) {
|
|
|
155
156
|
// itself break unattended agents/cron, the outcome ShieldCortex exists to
|
|
156
157
|
// prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
|
|
157
158
|
const FALLBACK_CATASTROPHIC_PATTERNS = [
|
|
158
|
-
/\brm\b[^|;&\n]*?(
|
|
159
|
+
/\brm\b[^|;&\n]*?(?:(?<![\w.\/-])-\w*r\w*f\w*|(?<![\w.\/-])-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
|
|
159
160
|
/\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
|
|
160
161
|
/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
|
|
161
162
|
/\bmkfs(\.\w+)?\b/i,
|
|
@@ -170,11 +171,49 @@ const FALLBACK_CATASTROPHIC_PATTERNS = [
|
|
|
170
171
|
/\b(?:curl|wget|fetch)\b[^|\n]*\|[^\n]*\bpython\d?\b[^\n]*\s-m\s*(?:code|pty|pdb)(?![\w.])/i,
|
|
171
172
|
/\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
|
|
172
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
|
+
];
|
|
173
206
|
const FALLBACK_SURFACE_KEYS = [
|
|
174
207
|
'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
|
|
175
208
|
'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
|
|
176
209
|
'url', 'uri', 'endpoint', 'href', 'host', 'to',
|
|
177
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;
|
|
178
217
|
/** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
|
|
179
218
|
function fallbackExecSurface(args) {
|
|
180
219
|
const parts = [];
|
|
@@ -183,7 +222,7 @@ function fallbackExecSurface(args) {
|
|
|
183
222
|
if (typeof v === 'string' && v.length > 0)
|
|
184
223
|
parts.push(v);
|
|
185
224
|
}
|
|
186
|
-
return parts.join(' ');
|
|
225
|
+
return parts.join(' ').slice(0, FALLBACK_SCAN_CAP);
|
|
187
226
|
}
|
|
188
227
|
function fallbackCatastrophicMatch(args) {
|
|
189
228
|
const text = fallbackExecSurface(args);
|
|
@@ -191,6 +230,17 @@ function fallbackCatastrophicMatch(args) {
|
|
|
191
230
|
return false;
|
|
192
231
|
return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
|
|
193
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
|
+
}
|
|
194
244
|
/** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
|
|
195
245
|
export function summariseToolArgs(args) {
|
|
196
246
|
if (!args)
|
|
@@ -220,6 +270,20 @@ export function formatActionGuardPrompt(toolName, v) {
|
|
|
220
270
|
}
|
|
221
271
|
// --- Audit Logging (local JSONL) ---
|
|
222
272
|
const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
|
|
273
|
+
// Issue #95: an unwritable audit sink used to be swallowed by this bare catch —
|
|
274
|
+
// entries silently dropped forever. Still best-effort (an audit failure must
|
|
275
|
+
// never block the agent), but the FIRST failure now warns loudly with the sink
|
|
276
|
+
// path and the error, and later failures keep a drop count for the breadcrumb.
|
|
277
|
+
let auditSinkFailures = 0;
|
|
278
|
+
export function noteAuditSinkFailure(err) {
|
|
279
|
+
auditSinkFailures++;
|
|
280
|
+
if (auditSinkFailures === 1) {
|
|
281
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
282
|
+
console.warn(`[shieldcortex] ⚠️ audit sink UNWRITABLE (${AUDIT_DIR}): ${detail} — audit entries are being DROPPED. ` +
|
|
283
|
+
`Fix the directory permissions/disk; enforcement continues but leaves no trail until this is resolved.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export function __resetAuditSinkFailuresForTest() { auditSinkFailures = 0; }
|
|
223
287
|
function writeAuditEntry(entry) {
|
|
224
288
|
try {
|
|
225
289
|
mkdirSync(AUDIT_DIR, { recursive: true });
|
|
@@ -227,8 +291,9 @@ function writeAuditEntry(entry) {
|
|
|
227
291
|
const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
|
|
228
292
|
appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
229
293
|
}
|
|
230
|
-
catch {
|
|
231
|
-
// Best-effort — never block on audit failure
|
|
294
|
+
catch (err) {
|
|
295
|
+
// Best-effort — never block on audit failure, but never silent either (#95).
|
|
296
|
+
noteAuditSinkFailure(err);
|
|
232
297
|
}
|
|
233
298
|
}
|
|
234
299
|
// --- X-Ray Inline Guard ---
|
|
@@ -288,34 +353,62 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
288
353
|
function guardAuditBase(toolName, v, preview) {
|
|
289
354
|
return {
|
|
290
355
|
type: 'intercept', tool: toolName,
|
|
291
|
-
severity: v.severity === 'catastrophic' ? 'critical' : 'high',
|
|
356
|
+
severity: v.severity === 'catastrophic' ? 'critical' : v.decision === 'allow' ? 'low' : 'high',
|
|
292
357
|
firewallResult: 'ACTION_GUARD', threats: v.signals,
|
|
293
|
-
anomalyScore: v.decision === 'block' ? 1 : 0.6,
|
|
358
|
+
anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
|
|
294
359
|
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
295
360
|
preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
296
361
|
};
|
|
297
362
|
}
|
|
298
|
-
// WS2 fail-closed path: when the real guard was never wired in or
|
|
299
|
-
// run the
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
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.
|
|
304
374
|
function handleGuardUnavailable(context, reason) {
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
}
|
|
307
406
|
return;
|
|
308
407
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
anomalyScore: 1, trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null,
|
|
314
|
-
pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
315
|
-
action: 'auto_deny', outcome: 'auto_denied',
|
|
316
|
-
});
|
|
317
|
-
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
|
|
318
|
-
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)`);
|
|
319
412
|
}
|
|
320
413
|
// Action Guard: gates non-memory tool calls (shell / file / network / git).
|
|
321
414
|
// This is what makes "Iron Dome protects what the agent DOES" true at runtime.
|
|
@@ -334,8 +427,18 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
334
427
|
handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
|
|
335
428
|
return;
|
|
336
429
|
}
|
|
337
|
-
if (v.decision === 'allow')
|
|
430
|
+
if (v.decision === 'allow') {
|
|
431
|
+
// Issue #95: a RECOGNISED allow (the guard evaluated a known operation
|
|
432
|
+
// family and let it through — severity above benign) leaves an audit
|
|
433
|
+
// entry, so forensics can distinguish "scanned & allowed" from "never
|
|
434
|
+
// scanned". Benign allows stay unaudited by design (volume discipline);
|
|
435
|
+
// `actionGuard.auditAllows: false` opts the recognised entries off too.
|
|
436
|
+
if (v.severity !== 'benign' && actionGuardCfg.auditAllows !== false) {
|
|
437
|
+
const allowPreview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
|
|
438
|
+
emitAudit({ ...guardAuditBase(context.toolName, v, allowPreview), action: 'allow', outcome: 'allowed' });
|
|
439
|
+
}
|
|
338
440
|
return;
|
|
441
|
+
}
|
|
339
442
|
const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
|
|
340
443
|
const base = guardAuditBase(context.toolName, v, preview);
|
|
341
444
|
const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
|
|
@@ -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,
|
|
@@ -93,6 +93,12 @@
|
|
|
93
93
|
"description": "Family/action/signal names pre-approved for unattended agents that legitimately need specific dangerous operations (case-insensitive substring match). Never applies to catastrophic operations.",
|
|
94
94
|
"type": "array",
|
|
95
95
|
"advanced": true
|
|
96
|
+
},
|
|
97
|
+
"interceptor.actionGuard.auditAllows": {
|
|
98
|
+
"label": "Audit Recognised Allows",
|
|
99
|
+
"description": "Write an audit entry when the guard evaluates a recognised (sensitive-tier) operation and allows it, so forensics can distinguish scanned-and-allowed from never-scanned. Benign allows are never audited.",
|
|
100
|
+
"type": "boolean",
|
|
101
|
+
"advanced": true
|
|
96
102
|
}
|
|
97
103
|
},
|
|
98
104
|
"configSchema": {
|
|
@@ -232,6 +238,10 @@
|
|
|
232
238
|
"type": "string"
|
|
233
239
|
},
|
|
234
240
|
"default": []
|
|
241
|
+
},
|
|
242
|
+
"auditAllows": {
|
|
243
|
+
"type": "boolean",
|
|
244
|
+
"default": true
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
}
|
package/interceptor.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface ActionGuardConfig {
|
|
|
26
26
|
enabled: boolean;
|
|
27
27
|
enforce: boolean;
|
|
28
28
|
autoApprove?: string[];
|
|
29
|
+
/** Audit recognised (severity 'sensitive'+) allow-decisions. Default true (issue #95).
|
|
30
|
+
* Benign allows are never audited — on a busy agent every `ls` would drown the stream. */
|
|
31
|
+
auditAllows?: boolean;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/** Structural shape of a Tool Action Guard verdict (kept local to avoid a
|
|
@@ -66,8 +69,8 @@ export interface InterceptAuditEntry {
|
|
|
66
69
|
sensitivityLevel: string; // from the pipeline result's sensitivity level
|
|
67
70
|
fragmentationScore: number | null; // from the pipeline result's fragmentation score, or null
|
|
68
71
|
pipelineDurationMs: number; // wall-clock ms around the runDefencePipeline call
|
|
69
|
-
action: InterceptAction | 'auto_deny' | 'rate_limit';
|
|
70
|
-
outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied';
|
|
72
|
+
action: InterceptAction | 'auto_deny' | 'rate_limit' | 'allow' | 'gate_degraded';
|
|
73
|
+
outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied' | 'allowed';
|
|
71
74
|
preview: string;
|
|
72
75
|
ts: string;
|
|
73
76
|
}
|
|
@@ -107,6 +110,7 @@ const DEFAULT_CONFIG: InterceptorConfig = {
|
|
|
107
110
|
enabled: true,
|
|
108
111
|
enforce: true,
|
|
109
112
|
autoApprove: [],
|
|
113
|
+
auditAllows: true,
|
|
110
114
|
},
|
|
111
115
|
};
|
|
112
116
|
|
|
@@ -259,7 +263,7 @@ export function formatApprovalPrompt(input: ApprovalPromptInput): string {
|
|
|
259
263
|
// itself break unattended agents/cron, the outcome ShieldCortex exists to
|
|
260
264
|
// prevent. Mirrored in scripts/pre-tool-hook.mjs for the Claude Code surface.
|
|
261
265
|
const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
|
|
262
|
-
/\brm\b[^|;&\n]*?(
|
|
266
|
+
/\brm\b[^|;&\n]*?(?:(?<![\w.\/-])-\w*r\w*f\w*|(?<![\w.\/-])-\w*f\w*r\w*|(?=[^|;&\n]*--recursive)(?=[^|;&\n]*--force))/i,
|
|
263
267
|
/\brm\b[^|;&\n]*\s(?:-\w+\s+)*(?:\/|~|\$HOME|\/\*|\*|\.\/\*)(?:\s|$)/i,
|
|
264
268
|
/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:?\s*&?\s*\}\s*;\s*:/,
|
|
265
269
|
/\bmkfs(\.\w+)?\b/i,
|
|
@@ -275,12 +279,52 @@ const FALLBACK_CATASTROPHIC_PATTERNS: RegExp[] = [
|
|
|
275
279
|
/\bch(?:mod|own)\b[^|;&\n]*(?:-\w*R\w*|--recursive)\b[^|;&\n]*\s\/(?:\s|$)/i,
|
|
276
280
|
];
|
|
277
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
|
+
|
|
278
315
|
const FALLBACK_SURFACE_KEYS = [
|
|
279
316
|
'command', 'cmd', 'script', 'code', 'input', 'shell', 'run',
|
|
280
317
|
'path', 'file_path', 'filePath', 'file', 'target', 'destination', 'dir', 'directory',
|
|
281
318
|
'url', 'uri', 'endpoint', 'href', 'host', 'to',
|
|
282
319
|
];
|
|
283
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
|
+
|
|
284
328
|
/** Same command/path/url field set tool-action-guard.ts extracts — narrow, not the whole args object. */
|
|
285
329
|
function fallbackExecSurface(args: Record<string, unknown> | undefined): string {
|
|
286
330
|
const parts: string[] = [];
|
|
@@ -288,7 +332,7 @@ function fallbackExecSurface(args: Record<string, unknown> | undefined): string
|
|
|
288
332
|
const v = args?.[k];
|
|
289
333
|
if (typeof v === 'string' && v.length > 0) parts.push(v);
|
|
290
334
|
}
|
|
291
|
-
return parts.join(' ');
|
|
335
|
+
return parts.join(' ').slice(0, FALLBACK_SCAN_CAP);
|
|
292
336
|
}
|
|
293
337
|
|
|
294
338
|
function fallbackCatastrophicMatch(args: Record<string, unknown> | undefined): boolean {
|
|
@@ -297,6 +341,16 @@ function fallbackCatastrophicMatch(args: Record<string, unknown> | undefined): b
|
|
|
297
341
|
return FALLBACK_CATASTROPHIC_PATTERNS.some(re => re.test(text));
|
|
298
342
|
}
|
|
299
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
|
+
|
|
300
354
|
/** One-line summary of tool args for audit previews (bounded, no secrets dumped). */
|
|
301
355
|
export function summariseToolArgs(args: Record<string, unknown> | undefined): string {
|
|
302
356
|
if (!args) return '';
|
|
@@ -327,14 +381,32 @@ export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLik
|
|
|
327
381
|
|
|
328
382
|
const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
|
|
329
383
|
|
|
384
|
+
// Issue #95: an unwritable audit sink used to be swallowed by this bare catch —
|
|
385
|
+
// entries silently dropped forever. Still best-effort (an audit failure must
|
|
386
|
+
// never block the agent), but the FIRST failure now warns loudly with the sink
|
|
387
|
+
// path and the error, and later failures keep a drop count for the breadcrumb.
|
|
388
|
+
let auditSinkFailures = 0;
|
|
389
|
+
export function noteAuditSinkFailure(err: unknown): void {
|
|
390
|
+
auditSinkFailures++;
|
|
391
|
+
if (auditSinkFailures === 1) {
|
|
392
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
393
|
+
console.warn(
|
|
394
|
+
`[shieldcortex] ⚠️ audit sink UNWRITABLE (${AUDIT_DIR}): ${detail} — audit entries are being DROPPED. ` +
|
|
395
|
+
`Fix the directory permissions/disk; enforcement continues but leaves no trail until this is resolved.`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
export function __resetAuditSinkFailuresForTest(): void { auditSinkFailures = 0; }
|
|
400
|
+
|
|
330
401
|
function writeAuditEntry(entry: InterceptAuditEntry): void {
|
|
331
402
|
try {
|
|
332
403
|
mkdirSync(AUDIT_DIR, { recursive: true });
|
|
333
404
|
const date = new Date().toISOString().slice(0, 10);
|
|
334
405
|
const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
|
|
335
406
|
appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
336
|
-
} catch {
|
|
337
|
-
// Best-effort — never block on audit failure
|
|
407
|
+
} catch (err) {
|
|
408
|
+
// Best-effort — never block on audit failure, but never silent either (#95).
|
|
409
|
+
noteAuditSinkFailure(err);
|
|
338
410
|
}
|
|
339
411
|
}
|
|
340
412
|
|
|
@@ -445,35 +517,66 @@ export function createInterceptor(
|
|
|
445
517
|
function guardAuditBase(toolName: string, v: ToolGuardVerdictLike, preview: string): Omit<InterceptAuditEntry, 'action' | 'outcome'> {
|
|
446
518
|
return {
|
|
447
519
|
type: 'intercept', tool: toolName,
|
|
448
|
-
severity: v.severity === 'catastrophic' ? 'critical' : 'high',
|
|
520
|
+
severity: v.severity === 'catastrophic' ? 'critical' : v.decision === 'allow' ? 'low' : 'high',
|
|
449
521
|
firewallResult: 'ACTION_GUARD', threats: v.signals,
|
|
450
|
-
anomalyScore: v.decision === 'block' ? 1 : 0.6,
|
|
522
|
+
anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
|
|
451
523
|
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
452
524
|
preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
453
525
|
};
|
|
454
526
|
}
|
|
455
527
|
|
|
456
|
-
// WS2 fail-closed path: when the real guard was never wired in or
|
|
457
|
-
// run the
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
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.
|
|
462
539
|
function handleGuardUnavailable(context: ToolCallContext, reason: string): void {
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
}
|
|
465
573
|
return;
|
|
466
574
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
pipelineDurationMs: 0, preview: preview.slice(0, 200), ts: new Date().toISOString(),
|
|
473
|
-
action: 'auto_deny', outcome: 'auto_denied',
|
|
474
|
-
});
|
|
475
|
-
log.warn(`[shieldcortex] action-guard UNAVAILABLE (${reason}) and fallback scan matched a catastrophic pattern — DENYING ${context.toolName} (fail-closed, WS2)`);
|
|
476
|
-
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)`);
|
|
477
580
|
}
|
|
478
581
|
|
|
479
582
|
// Action Guard: gates non-memory tool calls (shell / file / network / git).
|
|
@@ -493,7 +596,18 @@ export function createInterceptor(
|
|
|
493
596
|
handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
|
|
494
597
|
return;
|
|
495
598
|
}
|
|
496
|
-
if (v.decision === 'allow')
|
|
599
|
+
if (v.decision === 'allow') {
|
|
600
|
+
// Issue #95: a RECOGNISED allow (the guard evaluated a known operation
|
|
601
|
+
// family and let it through — severity above benign) leaves an audit
|
|
602
|
+
// entry, so forensics can distinguish "scanned & allowed" from "never
|
|
603
|
+
// scanned". Benign allows stay unaudited by design (volume discipline);
|
|
604
|
+
// `actionGuard.auditAllows: false` opts the recognised entries off too.
|
|
605
|
+
if (v.severity !== 'benign' && actionGuardCfg.auditAllows !== false) {
|
|
606
|
+
const allowPreview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
|
|
607
|
+
emitAudit({ ...guardAuditBase(context.toolName, v, allowPreview), action: 'allow', outcome: 'allowed' });
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
497
611
|
|
|
498
612
|
const preview = `${context.toolName} :: ${summariseToolArgs(context.arguments)}`;
|
|
499
613
|
const base = guardAuditBase(context.toolName, v, preview);
|
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,
|
|
@@ -93,6 +93,12 @@
|
|
|
93
93
|
"description": "Family/action/signal names pre-approved for unattended agents that legitimately need specific dangerous operations (case-insensitive substring match). Never applies to catastrophic operations.",
|
|
94
94
|
"type": "array",
|
|
95
95
|
"advanced": true
|
|
96
|
+
},
|
|
97
|
+
"interceptor.actionGuard.auditAllows": {
|
|
98
|
+
"label": "Audit Recognised Allows",
|
|
99
|
+
"description": "Write an audit entry when the guard evaluates a recognised (sensitive-tier) operation and allows it, so forensics can distinguish scanned-and-allowed from never-scanned. Benign allows are never audited.",
|
|
100
|
+
"type": "boolean",
|
|
101
|
+
"advanced": true
|
|
96
102
|
}
|
|
97
103
|
},
|
|
98
104
|
"configSchema": {
|
|
@@ -232,6 +238,10 @@
|
|
|
232
238
|
"type": "string"
|
|
233
239
|
},
|
|
234
240
|
"default": []
|
|
241
|
+
},
|
|
242
|
+
"auditAllows": {
|
|
243
|
+
"type": "boolean",
|
|
244
|
+
"default": true
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
}
|
package/package.json
CHANGED