agentshield-sdk 8.0.0 → 10.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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -21
- package/README.md +26 -60
- package/bin/agentshield-audit +51 -0
- package/package.json +7 -10
- package/src/adaptive.js +330 -330
- package/src/alert-tuning.js +480 -480
- package/src/audit-streaming.js +1 -1
- package/src/badges.js +196 -196
- package/src/behavioral-dna.js +12 -0
- package/src/canary.js +2 -3
- package/src/certification.js +563 -563
- package/src/circuit-breaker.js +2 -2
- package/src/confused-deputy.js +4 -0
- package/src/conversation.js +494 -494
- package/src/cross-turn.js +3 -17
- package/src/ctf.js +462 -462
- package/src/detector-core.js +71 -152
- package/src/document-scanner.js +795 -795
- package/src/drift-monitor.js +344 -0
- package/src/encoding.js +429 -429
- package/src/enterprise.js +405 -405
- package/src/flight-recorder.js +2 -0
- package/src/i18n-patterns.js +523 -523
- package/src/index.js +19 -0
- package/src/main.js +61 -41
- package/src/mcp-guard.js +974 -0
- package/src/micro-model.js +762 -0
- package/src/ml-detector.js +316 -0
- package/src/model-finetuning.js +884 -884
- package/src/multimodal.js +296 -296
- package/src/nist-mapping.js +2 -2
- package/src/observability.js +330 -330
- package/src/openclaw.js +450 -450
- package/src/otel.js +544 -544
- package/src/owasp-2025.js +1 -1
- package/src/owasp-agentic.js +420 -0
- package/src/plugin-marketplace.js +628 -628
- package/src/plugin-system.js +349 -349
- package/src/policy-extended.js +635 -635
- package/src/policy.js +443 -443
- package/src/prompt-leakage.js +2 -2
- package/src/real-attack-datasets.js +2 -2
- package/src/redteam-cli.js +439 -0
- package/src/supply-chain-scanner.js +691 -0
- package/src/testing.js +5 -1
- package/src/threat-encyclopedia.js +629 -629
- package/src/threat-intel-network.js +1017 -1017
- package/src/token-analysis.js +467 -467
- package/src/tool-output-validator.js +354 -354
- package/src/watermark.js +1 -2
package/src/enterprise.js
CHANGED
|
@@ -1,405 +1,405 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Agent Shield — Enterprise Features
|
|
5
|
-
*
|
|
6
|
-
* - Multi-tenant support
|
|
7
|
-
* - Role-based policies
|
|
8
|
-
* - Debug mode with detailed traces
|
|
9
|
-
* - Policy inheritance and overrides
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { AgentShield } = require('./index');
|
|
13
|
-
const { loadPolicy } = require('./policy');
|
|
14
|
-
|
|
15
|
-
// =========================================================================
|
|
16
|
-
// Multi-Tenant Shield
|
|
17
|
-
// =========================================================================
|
|
18
|
-
|
|
19
|
-
class MultiTenantShield {
|
|
20
|
-
constructor(options = {}) {
|
|
21
|
-
this.tenants = new Map();
|
|
22
|
-
this.defaultPolicy = options.defaultPolicy || { sensitivity: 'high', blockOnThreat: true };
|
|
23
|
-
this.globalOverrides = options.globalOverrides || {};
|
|
24
|
-
this.onTenantCreated = options.onTenantCreated || null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Register a tenant with its own policy.
|
|
29
|
-
*/
|
|
30
|
-
registerTenant(tenantId, policy = {}) {
|
|
31
|
-
const mergedPolicy = { ...this.defaultPolicy, ...policy, ...this.globalOverrides };
|
|
32
|
-
const shield = new AgentShield(mergedPolicy);
|
|
33
|
-
|
|
34
|
-
this.tenants.set(tenantId, {
|
|
35
|
-
id: tenantId,
|
|
36
|
-
policy: mergedPolicy,
|
|
37
|
-
shield,
|
|
38
|
-
stats: { scans: 0, threats: 0, blocked: 0 },
|
|
39
|
-
createdAt: new Date().toISOString()
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (this.onTenantCreated) {
|
|
43
|
-
this.onTenantCreated(tenantId, mergedPolicy);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return this;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get or auto-create a tenant shield.
|
|
51
|
-
*/
|
|
52
|
-
getTenant(tenantId) {
|
|
53
|
-
if (!this.tenants.has(tenantId)) {
|
|
54
|
-
this.registerTenant(tenantId);
|
|
55
|
-
}
|
|
56
|
-
return this.tenants.get(tenantId);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Scan input for a specific tenant.
|
|
61
|
-
*/
|
|
62
|
-
scan(tenantId, text, options = {}) {
|
|
63
|
-
const tenant = this.getTenant(tenantId);
|
|
64
|
-
tenant.stats.scans++;
|
|
65
|
-
|
|
66
|
-
const result = tenant.shield.scan(text, options);
|
|
67
|
-
|
|
68
|
-
if (result.threats.length > 0) {
|
|
69
|
-
tenant.stats.threats += result.threats.length;
|
|
70
|
-
}
|
|
71
|
-
if (result.blocked) {
|
|
72
|
-
tenant.stats.blocked++;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { ...result, tenantId };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Scan input for a specific tenant.
|
|
80
|
-
*/
|
|
81
|
-
scanInput(tenantId, text) {
|
|
82
|
-
return this.scan(tenantId, text);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Scan output for a specific tenant.
|
|
87
|
-
*/
|
|
88
|
-
scanOutput(tenantId, text) {
|
|
89
|
-
const tenant = this.getTenant(tenantId);
|
|
90
|
-
return tenant.shield.scanOutput(text);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Update a tenant's policy.
|
|
95
|
-
*/
|
|
96
|
-
updatePolicy(tenantId, policy) {
|
|
97
|
-
const tenant = this.getTenant(tenantId);
|
|
98
|
-
tenant.policy = { ...tenant.policy, ...policy, ...this.globalOverrides };
|
|
99
|
-
tenant.shield = new AgentShield(tenant.policy);
|
|
100
|
-
return tenant.policy;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Get stats for all tenants.
|
|
105
|
-
*/
|
|
106
|
-
getAllStats() {
|
|
107
|
-
const stats = {};
|
|
108
|
-
for (const [id, tenant] of this.tenants) {
|
|
109
|
-
stats[id] = { ...tenant.stats, policy: tenant.policy };
|
|
110
|
-
}
|
|
111
|
-
return stats;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Remove a tenant.
|
|
116
|
-
*/
|
|
117
|
-
removeTenant(tenantId) {
|
|
118
|
-
return this.tenants.delete(tenantId);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get tenant count.
|
|
123
|
-
*/
|
|
124
|
-
get size() {
|
|
125
|
-
return this.tenants.size;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// =========================================================================
|
|
130
|
-
// Role-Based Policies
|
|
131
|
-
// =========================================================================
|
|
132
|
-
|
|
133
|
-
const DEFAULT_ROLES = {
|
|
134
|
-
admin: {
|
|
135
|
-
name: 'Administrator',
|
|
136
|
-
sensitivity: 'medium',
|
|
137
|
-
blockOnThreat: false,
|
|
138
|
-
allowedTools: '*',
|
|
139
|
-
blockedTools: [],
|
|
140
|
-
bypassCircuitBreaker: true,
|
|
141
|
-
canViewAuditTrail: true,
|
|
142
|
-
canModifyPolicy: true
|
|
143
|
-
},
|
|
144
|
-
operator: {
|
|
145
|
-
name: 'Operator',
|
|
146
|
-
sensitivity: 'high',
|
|
147
|
-
blockOnThreat: true,
|
|
148
|
-
allowedTools: ['search', 'readFile', 'calculator'],
|
|
149
|
-
blockedTools: ['bash', 'shell', 'exec'],
|
|
150
|
-
bypassCircuitBreaker: false,
|
|
151
|
-
canViewAuditTrail: true,
|
|
152
|
-
canModifyPolicy: false
|
|
153
|
-
},
|
|
154
|
-
user: {
|
|
155
|
-
name: 'Standard User',
|
|
156
|
-
sensitivity: 'high',
|
|
157
|
-
blockOnThreat: true,
|
|
158
|
-
allowedTools: ['search', 'calculator'],
|
|
159
|
-
blockedTools: ['bash', 'shell', 'exec', 'readFile', 'writeFile'],
|
|
160
|
-
bypassCircuitBreaker: false,
|
|
161
|
-
canViewAuditTrail: false,
|
|
162
|
-
canModifyPolicy: false
|
|
163
|
-
},
|
|
164
|
-
restricted: {
|
|
165
|
-
name: 'Restricted User',
|
|
166
|
-
sensitivity: 'high',
|
|
167
|
-
blockOnThreat: true,
|
|
168
|
-
blockThreshold: 'low',
|
|
169
|
-
allowedTools: [],
|
|
170
|
-
blockedTools: '*',
|
|
171
|
-
bypassCircuitBreaker: false,
|
|
172
|
-
canViewAuditTrail: false,
|
|
173
|
-
canModifyPolicy: false
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
class RoleBasedPolicy {
|
|
178
|
-
constructor(options = {}) {
|
|
179
|
-
this.roles = { ...DEFAULT_ROLES, ...(options.customRoles || {}) };
|
|
180
|
-
this.userRoles = new Map();
|
|
181
|
-
this.shields = new Map();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Assign a role to a user.
|
|
186
|
-
*/
|
|
187
|
-
assignRole(userId, role) {
|
|
188
|
-
if (!this.roles[role]) {
|
|
189
|
-
throw new Error(`Unknown role: ${role}. Available: ${Object.keys(this.roles).join(', ')}`);
|
|
190
|
-
}
|
|
191
|
-
this.userRoles.set(userId, role);
|
|
192
|
-
|
|
193
|
-
// Create a shield for this role if not exists
|
|
194
|
-
if (!this.shields.has(role)) {
|
|
195
|
-
const roleConfig = this.roles[role];
|
|
196
|
-
this.shields.set(role, new AgentShield({
|
|
197
|
-
sensitivity: roleConfig.sensitivity,
|
|
198
|
-
blockOnThreat: roleConfig.blockOnThreat,
|
|
199
|
-
blockThreshold: roleConfig.blockThreshold
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return this;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Get the effective policy for a user.
|
|
208
|
-
*/
|
|
209
|
-
getPolicy(userId) {
|
|
210
|
-
const role = this.userRoles.get(userId) || 'user';
|
|
211
|
-
return { role, ...this.roles[role] };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Scan input with the user's role-based policy.
|
|
216
|
-
*/
|
|
217
|
-
scan(userId, text, options = {}) {
|
|
218
|
-
const role = this.userRoles.get(userId) || 'user';
|
|
219
|
-
|
|
220
|
-
// Reuse shield created in assignRole, or create lazily
|
|
221
|
-
if (!this.shields.has(role)) {
|
|
222
|
-
this.assignRole(userId, role);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const result = this.shields.get(role).scan(text, options);
|
|
226
|
-
return { ...result, userId, role };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Check if a user can use a specific tool.
|
|
231
|
-
*/
|
|
232
|
-
checkToolAccess(userId, toolName) {
|
|
233
|
-
const role = this.userRoles.get(userId) || 'user';
|
|
234
|
-
const roleConfig = this.roles[role];
|
|
235
|
-
|
|
236
|
-
if (roleConfig.blockedTools === '*') return { allowed: false, reason: 'All tools blocked for this role' };
|
|
237
|
-
if (Array.isArray(roleConfig.blockedTools) && roleConfig.blockedTools.includes(toolName)) {
|
|
238
|
-
return { allowed: false, reason: `Tool "${toolName}" is blocked for role "${role}"` };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (roleConfig.allowedTools === '*') return { allowed: true };
|
|
242
|
-
if (Array.isArray(roleConfig.allowedTools) && roleConfig.allowedTools.includes(toolName)) {
|
|
243
|
-
return { allowed: true };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { allowed: false, reason: `Tool "${toolName}" is not in the allowed list for role "${role}"` };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Define a custom role.
|
|
251
|
-
*/
|
|
252
|
-
defineRole(name, config) {
|
|
253
|
-
this.roles[name] = { name: config.name || name, ...config };
|
|
254
|
-
return this;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Get all available roles.
|
|
259
|
-
*/
|
|
260
|
-
getRoles() {
|
|
261
|
-
return Object.entries(this.roles).map(([key, val]) => ({ key, ...val }));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// =========================================================================
|
|
266
|
-
// Debug Mode
|
|
267
|
-
// =========================================================================
|
|
268
|
-
|
|
269
|
-
class DebugShield {
|
|
270
|
-
constructor(options = {}) {
|
|
271
|
-
this.shield = new AgentShield(options);
|
|
272
|
-
this.traces = [];
|
|
273
|
-
this.enabled = options.debug !== false;
|
|
274
|
-
this.maxTraces = options.maxTraces || 1000;
|
|
275
|
-
this.verbose = options.verbose || false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Scan with full debug trace.
|
|
280
|
-
*/
|
|
281
|
-
scan(text, options = {}) {
|
|
282
|
-
const startTime = this.enabled ? process.hrtime.bigint() : null;
|
|
283
|
-
|
|
284
|
-
// Scan
|
|
285
|
-
const result = this.shield.scan(text, options);
|
|
286
|
-
|
|
287
|
-
// Only build trace if debug is enabled
|
|
288
|
-
let trace = null;
|
|
289
|
-
if (this.enabled) {
|
|
290
|
-
const endTime = process.hrtime.bigint();
|
|
291
|
-
const elapsedMs = Number(endTime - startTime) / 1e6;
|
|
292
|
-
|
|
293
|
-
trace = {
|
|
294
|
-
id: `trace_${Date.now()}_${Math.random().toString(36).slice(2, 8).padEnd(6, '0')}`,
|
|
295
|
-
timestamp: new Date().toISOString(),
|
|
296
|
-
input: text.substring(0, 500),
|
|
297
|
-
inputLength: text.length,
|
|
298
|
-
options,
|
|
299
|
-
steps: [
|
|
300
|
-
{
|
|
301
|
-
step: 'input_received',
|
|
302
|
-
time: 0,
|
|
303
|
-
detail: { length: text.length, hasUnicode: /[^\x00-\x7F]/.test(text) }
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
step: 'pattern_matching',
|
|
307
|
-
time: elapsedMs,
|
|
308
|
-
detail: {
|
|
309
|
-
patternsChecked: this.shield.getPatterns().length,
|
|
310
|
-
threatsFound: result.threats.length,
|
|
311
|
-
threats: result.threats.map(t => ({
|
|
312
|
-
severity: t.severity,
|
|
313
|
-
category: t.category,
|
|
314
|
-
description: t.description,
|
|
315
|
-
confidence: t.confidence
|
|
316
|
-
}))
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
step: 'decision',
|
|
321
|
-
time: elapsedMs,
|
|
322
|
-
detail: {
|
|
323
|
-
status: result.status,
|
|
324
|
-
blocked: result.blocked,
|
|
325
|
-
threatCount: result.threats.length
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
],
|
|
329
|
-
totalTimeMs: parseFloat(elapsedMs.toFixed(3)),
|
|
330
|
-
result: {
|
|
331
|
-
status: result.status,
|
|
332
|
-
blocked: result.blocked,
|
|
333
|
-
threatCount: result.threats.length
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
this.traces.push(trace);
|
|
338
|
-
while (this.traces.length > this.maxTraces) {
|
|
339
|
-
this.traces.shift();
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (this.verbose) {
|
|
344
|
-
const ms = trace ? trace.totalTimeMs : 0;
|
|
345
|
-
console.log(`[Agent Shield] DEBUG Scan: ${text.substring(0, 50)}... → ${result.status} (${ms.toFixed(1)}ms, ${result.threats.length} threats)`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return { ...result, _trace: trace };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Get all traces.
|
|
353
|
-
*/
|
|
354
|
-
getTraces() {
|
|
355
|
-
return this.traces;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Get the last N traces.
|
|
360
|
-
*/
|
|
361
|
-
getRecentTraces(n = 10) {
|
|
362
|
-
return this.traces.slice(-n);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Export traces as JSON.
|
|
367
|
-
*/
|
|
368
|
-
exportTraces() {
|
|
369
|
-
return JSON.stringify(this.traces, null, 2);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Clear traces.
|
|
374
|
-
*/
|
|
375
|
-
clearTraces() {
|
|
376
|
-
this.traces = [];
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Get timing statistics across all traces.
|
|
381
|
-
*/
|
|
382
|
-
getTimingStats() {
|
|
383
|
-
if (this.traces.length === 0) return null;
|
|
384
|
-
|
|
385
|
-
const times = this.traces.map(t => t.totalTimeMs);
|
|
386
|
-
times.sort((a, b) => a - b);
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
count: times.length,
|
|
390
|
-
min: times[0],
|
|
391
|
-
max: times[times.length - 1],
|
|
392
|
-
avg: parseFloat((times.reduce((a, b) => a + b, 0) / times.length).toFixed(3)),
|
|
393
|
-
median: times[Math.floor(times.length / 2)],
|
|
394
|
-
p95: times[Math.min(Math.floor(times.length * 0.95), times.length - 1)],
|
|
395
|
-
p99: times[Math.min(Math.floor(times.length * 0.99), times.length - 1)]
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
module.exports = {
|
|
401
|
-
MultiTenantShield,
|
|
402
|
-
RoleBasedPolicy,
|
|
403
|
-
DebugShield,
|
|
404
|
-
DEFAULT_ROLES
|
|
405
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Enterprise Features
|
|
5
|
+
*
|
|
6
|
+
* - Multi-tenant support
|
|
7
|
+
* - Role-based policies
|
|
8
|
+
* - Debug mode with detailed traces
|
|
9
|
+
* - Policy inheritance and overrides
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { AgentShield } = require('./index');
|
|
13
|
+
const { loadPolicy } = require('./policy');
|
|
14
|
+
|
|
15
|
+
// =========================================================================
|
|
16
|
+
// Multi-Tenant Shield
|
|
17
|
+
// =========================================================================
|
|
18
|
+
|
|
19
|
+
class MultiTenantShield {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.tenants = new Map();
|
|
22
|
+
this.defaultPolicy = options.defaultPolicy || { sensitivity: 'high', blockOnThreat: true };
|
|
23
|
+
this.globalOverrides = options.globalOverrides || {};
|
|
24
|
+
this.onTenantCreated = options.onTenantCreated || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a tenant with its own policy.
|
|
29
|
+
*/
|
|
30
|
+
registerTenant(tenantId, policy = {}) {
|
|
31
|
+
const mergedPolicy = { ...this.defaultPolicy, ...policy, ...this.globalOverrides };
|
|
32
|
+
const shield = new AgentShield(mergedPolicy);
|
|
33
|
+
|
|
34
|
+
this.tenants.set(tenantId, {
|
|
35
|
+
id: tenantId,
|
|
36
|
+
policy: mergedPolicy,
|
|
37
|
+
shield,
|
|
38
|
+
stats: { scans: 0, threats: 0, blocked: 0 },
|
|
39
|
+
createdAt: new Date().toISOString()
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (this.onTenantCreated) {
|
|
43
|
+
this.onTenantCreated(tenantId, mergedPolicy);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get or auto-create a tenant shield.
|
|
51
|
+
*/
|
|
52
|
+
getTenant(tenantId) {
|
|
53
|
+
if (!this.tenants.has(tenantId)) {
|
|
54
|
+
this.registerTenant(tenantId);
|
|
55
|
+
}
|
|
56
|
+
return this.tenants.get(tenantId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scan input for a specific tenant.
|
|
61
|
+
*/
|
|
62
|
+
scan(tenantId, text, options = {}) {
|
|
63
|
+
const tenant = this.getTenant(tenantId);
|
|
64
|
+
tenant.stats.scans++;
|
|
65
|
+
|
|
66
|
+
const result = tenant.shield.scan(text, options);
|
|
67
|
+
|
|
68
|
+
if (result.threats.length > 0) {
|
|
69
|
+
tenant.stats.threats += result.threats.length;
|
|
70
|
+
}
|
|
71
|
+
if (result.blocked) {
|
|
72
|
+
tenant.stats.blocked++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { ...result, tenantId };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Scan input for a specific tenant.
|
|
80
|
+
*/
|
|
81
|
+
scanInput(tenantId, text) {
|
|
82
|
+
return this.scan(tenantId, text);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Scan output for a specific tenant.
|
|
87
|
+
*/
|
|
88
|
+
scanOutput(tenantId, text) {
|
|
89
|
+
const tenant = this.getTenant(tenantId);
|
|
90
|
+
return tenant.shield.scanOutput(text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update a tenant's policy.
|
|
95
|
+
*/
|
|
96
|
+
updatePolicy(tenantId, policy) {
|
|
97
|
+
const tenant = this.getTenant(tenantId);
|
|
98
|
+
tenant.policy = { ...tenant.policy, ...policy, ...this.globalOverrides };
|
|
99
|
+
tenant.shield = new AgentShield(tenant.policy);
|
|
100
|
+
return tenant.policy;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get stats for all tenants.
|
|
105
|
+
*/
|
|
106
|
+
getAllStats() {
|
|
107
|
+
const stats = {};
|
|
108
|
+
for (const [id, tenant] of this.tenants) {
|
|
109
|
+
stats[id] = { ...tenant.stats, policy: tenant.policy };
|
|
110
|
+
}
|
|
111
|
+
return stats;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Remove a tenant.
|
|
116
|
+
*/
|
|
117
|
+
removeTenant(tenantId) {
|
|
118
|
+
return this.tenants.delete(tenantId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get tenant count.
|
|
123
|
+
*/
|
|
124
|
+
get size() {
|
|
125
|
+
return this.tenants.size;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =========================================================================
|
|
130
|
+
// Role-Based Policies
|
|
131
|
+
// =========================================================================
|
|
132
|
+
|
|
133
|
+
const DEFAULT_ROLES = {
|
|
134
|
+
admin: {
|
|
135
|
+
name: 'Administrator',
|
|
136
|
+
sensitivity: 'medium',
|
|
137
|
+
blockOnThreat: false,
|
|
138
|
+
allowedTools: '*',
|
|
139
|
+
blockedTools: [],
|
|
140
|
+
bypassCircuitBreaker: true,
|
|
141
|
+
canViewAuditTrail: true,
|
|
142
|
+
canModifyPolicy: true
|
|
143
|
+
},
|
|
144
|
+
operator: {
|
|
145
|
+
name: 'Operator',
|
|
146
|
+
sensitivity: 'high',
|
|
147
|
+
blockOnThreat: true,
|
|
148
|
+
allowedTools: ['search', 'readFile', 'calculator'],
|
|
149
|
+
blockedTools: ['bash', 'shell', 'exec'],
|
|
150
|
+
bypassCircuitBreaker: false,
|
|
151
|
+
canViewAuditTrail: true,
|
|
152
|
+
canModifyPolicy: false
|
|
153
|
+
},
|
|
154
|
+
user: {
|
|
155
|
+
name: 'Standard User',
|
|
156
|
+
sensitivity: 'high',
|
|
157
|
+
blockOnThreat: true,
|
|
158
|
+
allowedTools: ['search', 'calculator'],
|
|
159
|
+
blockedTools: ['bash', 'shell', 'exec', 'readFile', 'writeFile'],
|
|
160
|
+
bypassCircuitBreaker: false,
|
|
161
|
+
canViewAuditTrail: false,
|
|
162
|
+
canModifyPolicy: false
|
|
163
|
+
},
|
|
164
|
+
restricted: {
|
|
165
|
+
name: 'Restricted User',
|
|
166
|
+
sensitivity: 'high',
|
|
167
|
+
blockOnThreat: true,
|
|
168
|
+
blockThreshold: 'low',
|
|
169
|
+
allowedTools: [],
|
|
170
|
+
blockedTools: '*',
|
|
171
|
+
bypassCircuitBreaker: false,
|
|
172
|
+
canViewAuditTrail: false,
|
|
173
|
+
canModifyPolicy: false
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
class RoleBasedPolicy {
|
|
178
|
+
constructor(options = {}) {
|
|
179
|
+
this.roles = { ...DEFAULT_ROLES, ...(options.customRoles || {}) };
|
|
180
|
+
this.userRoles = new Map();
|
|
181
|
+
this.shields = new Map();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Assign a role to a user.
|
|
186
|
+
*/
|
|
187
|
+
assignRole(userId, role) {
|
|
188
|
+
if (!this.roles[role]) {
|
|
189
|
+
throw new Error(`Unknown role: ${role}. Available: ${Object.keys(this.roles).join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
this.userRoles.set(userId, role);
|
|
192
|
+
|
|
193
|
+
// Create a shield for this role if not exists
|
|
194
|
+
if (!this.shields.has(role)) {
|
|
195
|
+
const roleConfig = this.roles[role];
|
|
196
|
+
this.shields.set(role, new AgentShield({
|
|
197
|
+
sensitivity: roleConfig.sensitivity,
|
|
198
|
+
blockOnThreat: roleConfig.blockOnThreat,
|
|
199
|
+
blockThreshold: roleConfig.blockThreshold
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the effective policy for a user.
|
|
208
|
+
*/
|
|
209
|
+
getPolicy(userId) {
|
|
210
|
+
const role = this.userRoles.get(userId) || 'user';
|
|
211
|
+
return { role, ...this.roles[role] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Scan input with the user's role-based policy.
|
|
216
|
+
*/
|
|
217
|
+
scan(userId, text, options = {}) {
|
|
218
|
+
const role = this.userRoles.get(userId) || 'user';
|
|
219
|
+
|
|
220
|
+
// Reuse shield created in assignRole, or create lazily
|
|
221
|
+
if (!this.shields.has(role)) {
|
|
222
|
+
this.assignRole(userId, role);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = this.shields.get(role).scan(text, options);
|
|
226
|
+
return { ...result, userId, role };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if a user can use a specific tool.
|
|
231
|
+
*/
|
|
232
|
+
checkToolAccess(userId, toolName) {
|
|
233
|
+
const role = this.userRoles.get(userId) || 'user';
|
|
234
|
+
const roleConfig = this.roles[role];
|
|
235
|
+
|
|
236
|
+
if (roleConfig.blockedTools === '*') return { allowed: false, reason: 'All tools blocked for this role' };
|
|
237
|
+
if (Array.isArray(roleConfig.blockedTools) && roleConfig.blockedTools.includes(toolName)) {
|
|
238
|
+
return { allowed: false, reason: `Tool "${toolName}" is blocked for role "${role}"` };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (roleConfig.allowedTools === '*') return { allowed: true };
|
|
242
|
+
if (Array.isArray(roleConfig.allowedTools) && roleConfig.allowedTools.includes(toolName)) {
|
|
243
|
+
return { allowed: true };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { allowed: false, reason: `Tool "${toolName}" is not in the allowed list for role "${role}"` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Define a custom role.
|
|
251
|
+
*/
|
|
252
|
+
defineRole(name, config) {
|
|
253
|
+
this.roles[name] = { name: config.name || name, ...config };
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get all available roles.
|
|
259
|
+
*/
|
|
260
|
+
getRoles() {
|
|
261
|
+
return Object.entries(this.roles).map(([key, val]) => ({ key, ...val }));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// =========================================================================
|
|
266
|
+
// Debug Mode
|
|
267
|
+
// =========================================================================
|
|
268
|
+
|
|
269
|
+
class DebugShield {
|
|
270
|
+
constructor(options = {}) {
|
|
271
|
+
this.shield = new AgentShield(options);
|
|
272
|
+
this.traces = [];
|
|
273
|
+
this.enabled = options.debug !== false;
|
|
274
|
+
this.maxTraces = options.maxTraces || 1000;
|
|
275
|
+
this.verbose = options.verbose || false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Scan with full debug trace.
|
|
280
|
+
*/
|
|
281
|
+
scan(text, options = {}) {
|
|
282
|
+
const startTime = this.enabled ? process.hrtime.bigint() : null;
|
|
283
|
+
|
|
284
|
+
// Scan
|
|
285
|
+
const result = this.shield.scan(text, options);
|
|
286
|
+
|
|
287
|
+
// Only build trace if debug is enabled
|
|
288
|
+
let trace = null;
|
|
289
|
+
if (this.enabled) {
|
|
290
|
+
const endTime = process.hrtime.bigint();
|
|
291
|
+
const elapsedMs = Number(endTime - startTime) / 1e6;
|
|
292
|
+
|
|
293
|
+
trace = {
|
|
294
|
+
id: `trace_${Date.now()}_${Math.random().toString(36).slice(2, 8).padEnd(6, '0')}`,
|
|
295
|
+
timestamp: new Date().toISOString(),
|
|
296
|
+
input: text.substring(0, 500),
|
|
297
|
+
inputLength: text.length,
|
|
298
|
+
options,
|
|
299
|
+
steps: [
|
|
300
|
+
{
|
|
301
|
+
step: 'input_received',
|
|
302
|
+
time: 0,
|
|
303
|
+
detail: { length: text.length, hasUnicode: /[^\x00-\x7F]/.test(text) }
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
step: 'pattern_matching',
|
|
307
|
+
time: elapsedMs,
|
|
308
|
+
detail: {
|
|
309
|
+
patternsChecked: this.shield.getPatterns().length,
|
|
310
|
+
threatsFound: result.threats.length,
|
|
311
|
+
threats: result.threats.map(t => ({
|
|
312
|
+
severity: t.severity,
|
|
313
|
+
category: t.category,
|
|
314
|
+
description: t.description,
|
|
315
|
+
confidence: t.confidence
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
step: 'decision',
|
|
321
|
+
time: elapsedMs,
|
|
322
|
+
detail: {
|
|
323
|
+
status: result.status,
|
|
324
|
+
blocked: result.blocked,
|
|
325
|
+
threatCount: result.threats.length
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
totalTimeMs: parseFloat(elapsedMs.toFixed(3)),
|
|
330
|
+
result: {
|
|
331
|
+
status: result.status,
|
|
332
|
+
blocked: result.blocked,
|
|
333
|
+
threatCount: result.threats.length
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
this.traces.push(trace);
|
|
338
|
+
while (this.traces.length > this.maxTraces) {
|
|
339
|
+
this.traces.shift();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (this.verbose) {
|
|
344
|
+
const ms = trace ? trace.totalTimeMs : 0;
|
|
345
|
+
console.log(`[Agent Shield] DEBUG Scan: ${text.substring(0, 50)}... → ${result.status} (${ms.toFixed(1)}ms, ${result.threats.length} threats)`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { ...result, _trace: trace };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get all traces.
|
|
353
|
+
*/
|
|
354
|
+
getTraces() {
|
|
355
|
+
return this.traces;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the last N traces.
|
|
360
|
+
*/
|
|
361
|
+
getRecentTraces(n = 10) {
|
|
362
|
+
return this.traces.slice(-n);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Export traces as JSON.
|
|
367
|
+
*/
|
|
368
|
+
exportTraces() {
|
|
369
|
+
return JSON.stringify(this.traces, null, 2);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Clear traces.
|
|
374
|
+
*/
|
|
375
|
+
clearTraces() {
|
|
376
|
+
this.traces = [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get timing statistics across all traces.
|
|
381
|
+
*/
|
|
382
|
+
getTimingStats() {
|
|
383
|
+
if (this.traces.length === 0) return null;
|
|
384
|
+
|
|
385
|
+
const times = this.traces.map(t => t.totalTimeMs);
|
|
386
|
+
times.sort((a, b) => a - b);
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
count: times.length,
|
|
390
|
+
min: times[0],
|
|
391
|
+
max: times[times.length - 1],
|
|
392
|
+
avg: parseFloat((times.reduce((a, b) => a + b, 0) / times.length).toFixed(3)),
|
|
393
|
+
median: times[Math.floor(times.length / 2)],
|
|
394
|
+
p95: times[Math.min(Math.floor(times.length * 0.95), times.length - 1)],
|
|
395
|
+
p99: times[Math.min(Math.floor(times.length * 0.99), times.length - 1)]
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
module.exports = {
|
|
401
|
+
MultiTenantShield,
|
|
402
|
+
RoleBasedPolicy,
|
|
403
|
+
DebugShield,
|
|
404
|
+
DEFAULT_ROLES
|
|
405
|
+
};
|