shieldcortex 3.4.37 → 4.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.
@@ -0,0 +1,280 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, appendFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ const WATCHED_TOOLS = ['remember', 'mcp__memory__remember'];
6
+ const CONTENT_FIELDS = {
7
+ remember: ['content', 'title'],
8
+ mcp__memory__remember: ['content', 'title'],
9
+ };
10
+ const DEFAULT_CONFIG = {
11
+ enabled: true,
12
+ severityActions: {
13
+ low: 'log',
14
+ medium: 'warn',
15
+ high: 'require_approval',
16
+ critical: 'require_approval',
17
+ },
18
+ failurePolicy: {
19
+ low: 'allow',
20
+ medium: 'allow',
21
+ high: 'deny',
22
+ critical: 'deny',
23
+ },
24
+ };
25
+ export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
26
+ export function extractContent(toolName, args) {
27
+ const fields = CONTENT_FIELDS[toolName];
28
+ if (!fields)
29
+ return { title: '', content: '' };
30
+ const title = typeof args.title === 'string' ? args.title : '';
31
+ const content = typeof args.content === 'string' ? args.content : '';
32
+ return { title, content };
33
+ }
34
+ export function mapSeverity(firewall) {
35
+ if (firewall.result === 'BLOCK')
36
+ return 'critical';
37
+ if (firewall.result === 'QUARANTINE')
38
+ return 'high';
39
+ if (firewall.result === 'ALLOW' && firewall.anomalyScore >= 0.3)
40
+ return 'medium';
41
+ return 'low';
42
+ }
43
+ // --- Deny Cache ---
44
+ // Exact replica of normalizeMemoryText() from index.ts (lines 426-434).
45
+ // Must produce identical output for SHA-256 hash consistency.
46
+ function normaliseContent(text) {
47
+ return String(text || '')
48
+ .toLowerCase()
49
+ .replace(/[`"'\\]/g, ' ')
50
+ .replace(/https?:\/\/\S+/g, ' ')
51
+ .replace(/[^a-z0-9\s]/g, ' ')
52
+ .replace(/\s+/g, ' ')
53
+ .trim();
54
+ }
55
+ function hashContent(text) {
56
+ return createHash('sha256').update(normaliseContent(text)).digest('hex');
57
+ }
58
+ const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
59
+ export class DenyCache {
60
+ cache = new Map();
61
+ maxPerTool;
62
+ ttlMs;
63
+ constructor(maxPerTool = 200, ttlMs = TWO_HOURS_MS) {
64
+ this.maxPerTool = maxPerTool;
65
+ this.ttlMs = ttlMs;
66
+ }
67
+ isDenied(tool, content) {
68
+ const entries = this.cache.get(tool);
69
+ if (!entries)
70
+ return false;
71
+ const hash = hashContent(content);
72
+ const now = Date.now();
73
+ return entries.some(e => e.hash === hash && (now - e.ts) < this.ttlMs);
74
+ }
75
+ addDenial(tool, content) {
76
+ const hash = hashContent(content);
77
+ const now = Date.now();
78
+ if (!this.cache.has(tool)) {
79
+ this.cache.set(tool, []);
80
+ }
81
+ const entries = this.cache.get(tool);
82
+ const live = entries.filter(e => (now - e.ts) < this.ttlMs);
83
+ if (live.some(e => e.hash === hash))
84
+ return;
85
+ live.push({ hash, ts: now });
86
+ while (live.length > this.maxPerTool) {
87
+ live.shift();
88
+ }
89
+ this.cache.set(tool, live);
90
+ }
91
+ reset() {
92
+ this.cache.clear();
93
+ }
94
+ }
95
+ // --- Rate Limiter ---
96
+ export class RateLimiter {
97
+ timestamps = [];
98
+ maxPerWindow;
99
+ windowMs;
100
+ constructor(maxPerWindow = 5, windowMs = 60_000) {
101
+ this.maxPerWindow = maxPerWindow;
102
+ this.windowMs = windowMs;
103
+ }
104
+ shouldAllow() {
105
+ const now = Date.now();
106
+ this.timestamps = this.timestamps.filter(t => now - t < this.windowMs);
107
+ if (this.timestamps.length >= this.maxPerWindow)
108
+ return false;
109
+ this.timestamps.push(now);
110
+ return true;
111
+ }
112
+ }
113
+ export function formatApprovalPrompt(input) {
114
+ const preview = input.content.length > 200
115
+ ? input.content.slice(0, 200) + '...'
116
+ : input.content;
117
+ const threatList = input.threats.length > 0
118
+ ? input.threats.join(', ')
119
+ : 'none identified';
120
+ return [
121
+ '🛡️ ShieldCortex — Tool Call Intercepted',
122
+ '',
123
+ `Tool: ${input.tool}`,
124
+ `Risk: ${input.severity} (${input.firewallResult})`,
125
+ `Threats: ${threatList}`,
126
+ `Content: "${preview}"`,
127
+ '',
128
+ '[Approve] [Deny]',
129
+ ].join('\n');
130
+ }
131
+ // --- Audit Logging (local JSONL) ---
132
+ const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
133
+ function writeAuditEntry(entry) {
134
+ try {
135
+ mkdirSync(AUDIT_DIR, { recursive: true });
136
+ const date = new Date().toISOString().slice(0, 10);
137
+ const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
138
+ appendFileSync(file, JSON.stringify(entry) + '\n');
139
+ }
140
+ catch {
141
+ // Best-effort — never block on audit failure
142
+ }
143
+ }
144
+ export function createInterceptor(config, pipeline, options) {
145
+ const denyCache = new DenyCache();
146
+ const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
147
+ const log = config.logger ?? { info: console.log, warn: console.warn };
148
+ const onAuditEntry = options?.onAuditEntry;
149
+ function emitAudit(entry) {
150
+ writeAuditEntry(entry);
151
+ onAuditEntry?.(entry);
152
+ }
153
+ async function handleToolCall(context) {
154
+ if (!WATCHED_TOOLS.includes(context.toolName))
155
+ return;
156
+ const { title, content } = extractContent(context.toolName, context.arguments);
157
+ const fullContent = [title, content].filter(Boolean).join(' ');
158
+ if (!fullContent.trim())
159
+ return;
160
+ let severity;
161
+ let firewallResult;
162
+ let threats;
163
+ let anomalyScore;
164
+ try {
165
+ const result = pipeline(content, title, { type: 'agent', identifier: 'openclaw' });
166
+ severity = mapSeverity(result.firewall);
167
+ firewallResult = result.firewall.result;
168
+ threats = result.firewall.threatIndicators;
169
+ anomalyScore = result.firewall.anomalyScore;
170
+ }
171
+ catch (err) {
172
+ log.warn(`[shieldcortex] ⚠️ Defence pipeline error: ${err instanceof Error ? err.message : err}`);
173
+ const failAction = config.failurePolicy.high;
174
+ const entry = {
175
+ type: 'intercept', tool: context.toolName, severity: 'high',
176
+ firewallResult: 'ERROR', threats: ['pipeline_error'], anomalyScore: 0,
177
+ action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
178
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
179
+ };
180
+ emitAudit(entry);
181
+ if (failAction === 'deny') {
182
+ throw new Error('ShieldCortex: tool call blocked — pipeline error, failure policy: deny');
183
+ }
184
+ return;
185
+ }
186
+ if (denyCache.isDenied(context.toolName, fullContent)) {
187
+ const entry = {
188
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
189
+ threats, anomalyScore, action: 'auto_deny', outcome: 'auto_denied',
190
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
191
+ };
192
+ emitAudit(entry);
193
+ throw new Error('ShieldCortex: tool call auto-denied (previously denied content)');
194
+ }
195
+ const action = config.severityActions[severity];
196
+ if (action === 'log') {
197
+ const entry = {
198
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
199
+ threats, anomalyScore, action: 'log', outcome: 'logged',
200
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
201
+ };
202
+ emitAudit(entry);
203
+ return;
204
+ }
205
+ if (action === 'warn') {
206
+ log.warn(`[shieldcortex] ⚠️ ${severity} risk in ${context.toolName}: ${threats.join(', ') || 'anomaly detected'}`);
207
+ const entry = {
208
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
209
+ threats, anomalyScore, action: 'warn', outcome: 'warned',
210
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
211
+ };
212
+ emitAudit(entry);
213
+ return;
214
+ }
215
+ // action === 'require_approval'
216
+ if (typeof context.requireApproval !== 'function') {
217
+ log.warn(`[shieldcortex] ⚠️ requireApproval not available — falling back to warn for ${severity} risk in ${context.toolName}`);
218
+ const entry = {
219
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
220
+ threats, anomalyScore, action: 'warn', outcome: 'warned',
221
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
222
+ };
223
+ emitAudit(entry);
224
+ return;
225
+ }
226
+ if (!rateLimiter.shouldAllow()) {
227
+ log.warn('[shieldcortex] ⚠️ Too many approval prompts — auto-denying');
228
+ const entry = {
229
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
230
+ threats, anomalyScore, action: 'rate_limit', outcome: 'auto_denied',
231
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
232
+ };
233
+ emitAudit(entry);
234
+ denyCache.addDenial(context.toolName, fullContent);
235
+ throw new Error('ShieldCortex: tool call auto-denied (rate limit exceeded)');
236
+ }
237
+ const message = formatApprovalPrompt({ tool: context.toolName, severity, firewallResult, threats, content: fullContent });
238
+ let approved;
239
+ try {
240
+ approved = await context.requireApproval(message);
241
+ }
242
+ catch (err) {
243
+ const failAction = config.failurePolicy[severity];
244
+ log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
245
+ const entry = {
246
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
247
+ threats, anomalyScore, action: 'require_approval',
248
+ outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
249
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
250
+ };
251
+ emitAudit(entry);
252
+ if (failAction === 'deny') {
253
+ throw new Error(`ShieldCortex: tool call blocked — requireApproval error, failure policy: deny`);
254
+ }
255
+ return;
256
+ }
257
+ if (approved) {
258
+ const entry = {
259
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
260
+ threats, anomalyScore, action: 'require_approval', outcome: 'approved',
261
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
262
+ };
263
+ emitAudit(entry);
264
+ return;
265
+ }
266
+ // Denied
267
+ denyCache.addDenial(context.toolName, fullContent);
268
+ const entry = {
269
+ type: 'intercept', tool: context.toolName, severity, firewallResult,
270
+ threats, anomalyScore, action: 'require_approval', outcome: 'denied',
271
+ preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
272
+ };
273
+ emitAudit(entry);
274
+ throw new Error('ShieldCortex: tool call denied by user');
275
+ }
276
+ function resetSession() {
277
+ denyCache.reset();
278
+ }
279
+ return { handleToolCall, resetSession };
280
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "3.4.33",
3
+ "version": "3.5.0",
4
4
  "name": "ShieldCortex Real-time Scanner",
5
- "description": "Real-time defence scanning on LLM input and memory extraction on LLM output.",
5
+ "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
6
6
  "uiHints": {
7
7
  "binaryPath": {
8
8
  "label": "ShieldCortex Binary Path",
@@ -40,11 +40,26 @@
40
40
  "label": "Recent Memory Cache Size",
41
41
  "help": "How many recent extracted memories to keep in the dedupe cache.",
42
42
  "advanced": true
43
+ },
44
+ "interceptor.enabled": {
45
+ "label": "Enable Tool Call Interceptor",
46
+ "description": "Scan memory-write tool calls and gate suspicious content behind user approval",
47
+ "type": "boolean"
48
+ },
49
+ "interceptor.severityActions.high": {
50
+ "label": "High Severity Action",
51
+ "description": "Action for high-severity threats (log, warn, require_approval)",
52
+ "type": "string"
53
+ },
54
+ "interceptor.severityActions.critical": {
55
+ "label": "Critical Severity Action",
56
+ "description": "Action for critical-severity threats (log, warn, require_approval)",
57
+ "type": "string"
43
58
  }
44
59
  },
45
60
  "configSchema": {
46
61
  "type": "object",
47
- "additionalProperties": false,
62
+ "additionalProperties": true,
48
63
  "properties": {
49
64
  "enabled": {
50
65
  "type": "boolean"
@@ -73,6 +88,30 @@
73
88
  "type": "integer",
74
89
  "minimum": 50,
75
90
  "maximum": 1000
91
+ },
92
+ "interceptor": {
93
+ "type": "object",
94
+ "properties": {
95
+ "enabled": { "type": "boolean", "default": true },
96
+ "severityActions": {
97
+ "type": "object",
98
+ "properties": {
99
+ "low": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "log" },
100
+ "medium": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "warn" },
101
+ "high": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "require_approval" },
102
+ "critical": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "require_approval" }
103
+ }
104
+ },
105
+ "failurePolicy": {
106
+ "type": "object",
107
+ "properties": {
108
+ "low": { "type": "string", "enum": ["allow", "deny"], "default": "allow" },
109
+ "medium": { "type": "string", "enum": ["allow", "deny"], "default": "allow" },
110
+ "high": { "type": "string", "enum": ["allow", "deny"], "default": "deny" },
111
+ "critical": { "type": "string", "enum": ["allow", "deny"], "default": "deny" }
112
+ }
113
+ }
114
+ }
76
115
  }
77
116
  }
78
117
  }