agentshield-sdk 7.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,557 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Production Features
5
+ *
6
+ * - Sampling mode
7
+ * - Dry run / shadow comparison
8
+ * - Graceful degradation
9
+ * - Threat replay
10
+ * - Attack attribution chains
11
+ * - Diff reports
12
+ * - Security posture tracking over time
13
+ */
14
+
15
+ const { scanText } = require('./detector-core');
16
+
17
+ // =========================================================================
18
+ // Sampling Mode
19
+ // =========================================================================
20
+
21
+ class SamplingScanner {
22
+ constructor(options = {}) {
23
+ this.sampleRate = options.sampleRate !== undefined ? options.sampleRate : 0.1; // 10% default
24
+ this.scanFn = options.scanFn || ((text) => scanText(text, options.sensitivity || 'high'));
25
+ this.stats = { total: 0, sampled: 0, threats: 0, extrapolatedThreats: 0 };
26
+ }
27
+
28
+ /**
29
+ * Scan with sampling — only scans a percentage of inputs.
30
+ */
31
+ scan(text) {
32
+ this.stats.total++;
33
+ const shouldScan = Math.random() < this.sampleRate;
34
+
35
+ if (!shouldScan) {
36
+ return { sampled: false, status: 'skipped', threats: [] };
37
+ }
38
+
39
+ this.stats.sampled++;
40
+ const result = this.scanFn(text);
41
+
42
+ if (result.threats && result.threats.length > 0) {
43
+ this.stats.threats += result.threats.length;
44
+ }
45
+
46
+ // Extrapolate
47
+ this.stats.extrapolatedThreats = Math.round(this.stats.threats / this.sampleRate);
48
+
49
+ return { sampled: true, ...result };
50
+ }
51
+
52
+ /**
53
+ * Get extrapolated statistics.
54
+ */
55
+ getStats() {
56
+ return {
57
+ ...this.stats,
58
+ sampleRate: `${(this.sampleRate * 100).toFixed(1)}%`,
59
+ estimatedTotalThreats: this.stats.extrapolatedThreats,
60
+ confidence: this.stats.sampled > 30 ? 'high' : this.stats.sampled > 10 ? 'medium' : 'low'
61
+ };
62
+ }
63
+
64
+ setSampleRate(rate) {
65
+ this.sampleRate = Math.max(0, Math.min(1, rate));
66
+ }
67
+ }
68
+
69
+ // =========================================================================
70
+ // Dry Run / Shadow Comparison
71
+ // =========================================================================
72
+
73
+ class ShadowComparison {
74
+ constructor(options = {}) {
75
+ this.primaryScanFn = options.primary || ((text) => scanText(text, 'high'));
76
+ this.candidateScanFn = options.candidate || ((text) => scanText(text, 'high'));
77
+ this.results = [];
78
+ this.maxResults = options.maxResults || 5000;
79
+ }
80
+
81
+ /**
82
+ * Run both policies and compare results.
83
+ */
84
+ compare(text) {
85
+ const primaryResult = this.primaryScanFn(text);
86
+ const candidateResult = this.candidateScanFn(text);
87
+
88
+ const primaryBlocked = primaryResult.threats && primaryResult.threats.length > 0;
89
+ const candidateBlocked = candidateResult.threats && candidateResult.threats.length > 0;
90
+
91
+ let diff = 'same';
92
+ if (primaryBlocked && !candidateBlocked) diff = 'candidate_would_allow';
93
+ else if (!primaryBlocked && candidateBlocked) diff = 'candidate_would_block';
94
+ else if (primaryBlocked && candidateBlocked) {
95
+ // Check if threats differ
96
+ const pCats = new Set(primaryResult.threats.map(t => t.category));
97
+ const cCats = new Set(candidateResult.threats.map(t => t.category));
98
+ const sameCats = [...pCats].every(c => cCats.has(c)) && [...cCats].every(c => pCats.has(c));
99
+ if (!sameCats) diff = 'different_threats';
100
+ }
101
+
102
+ const entry = {
103
+ text: text.substring(0, 200),
104
+ diff,
105
+ primary: { status: primaryResult.status, threats: (primaryResult.threats || []).length },
106
+ candidate: { status: candidateResult.status, threats: (candidateResult.threats || []).length },
107
+ timestamp: Date.now()
108
+ };
109
+
110
+ this.results.push(entry);
111
+ while (this.results.length > this.maxResults) {
112
+ this.results.shift();
113
+ }
114
+
115
+ return { ...entry, primaryResult, candidateResult };
116
+ }
117
+
118
+ /**
119
+ * Generate a diff report.
120
+ */
121
+ generateReport() {
122
+ const total = this.results.length;
123
+ if (total === 0) return { status: 'no_data', total: 0 };
124
+
125
+ const same = this.results.filter(r => r.diff === 'same').length;
126
+ const candidateWouldAllow = this.results.filter(r => r.diff === 'candidate_would_allow').length;
127
+ const candidateWouldBlock = this.results.filter(r => r.diff === 'candidate_would_block').length;
128
+ const differentThreats = this.results.filter(r => r.diff === 'different_threats').length;
129
+
130
+ return {
131
+ total,
132
+ same,
133
+ candidateWouldAllow,
134
+ candidateWouldBlock,
135
+ differentThreats,
136
+ agreementRate: `${((same / total) * 100).toFixed(1)}%`,
137
+ newBlocks: candidateWouldBlock,
138
+ removedBlocks: candidateWouldAllow,
139
+ examples: {
140
+ wouldAllow: this.results.filter(r => r.diff === 'candidate_would_allow').slice(0, 5),
141
+ wouldBlock: this.results.filter(r => r.diff === 'candidate_would_block').slice(0, 5)
142
+ }
143
+ };
144
+ }
145
+
146
+ clear() { this.results = []; }
147
+ }
148
+
149
+ // =========================================================================
150
+ // Graceful Degradation
151
+ // =========================================================================
152
+
153
+ class GracefulScanner {
154
+ constructor(options = {}) {
155
+ this.scanFn = options.scanFn || ((text) => scanText(text, options.sensitivity || 'high'));
156
+ this.fallbackPolicy = options.fallbackPolicy || 'allow'; // 'allow' or 'block'
157
+ this.timeoutMs = options.timeoutMs || 100;
158
+ this.onError = options.onError || null;
159
+ this.onTimeout = options.onTimeout || null;
160
+ this.stats = { scans: 0, successes: 0, errors: 0, timeouts: 0, fallbacks: 0 };
161
+ }
162
+
163
+ /**
164
+ * Scan with graceful error handling and timeout.
165
+ */
166
+ scan(text) {
167
+ this.stats.scans++;
168
+ const start = Date.now();
169
+
170
+ try {
171
+ const result = this.scanFn(text);
172
+ const elapsed = Date.now() - start;
173
+
174
+ if (elapsed > this.timeoutMs) {
175
+ this.stats.timeouts++;
176
+ if (this.onTimeout) this.onTimeout({ elapsed, text: text.substring(0, 100) });
177
+ return this._fallback('timeout', elapsed);
178
+ }
179
+
180
+ this.stats.successes++;
181
+ return result;
182
+ } catch (error) {
183
+ this.stats.errors++;
184
+ this.stats.fallbacks++;
185
+ if (this.onError) this.onError({ error: error.message, text: text.substring(0, 100) });
186
+ return this._fallback('error', 0, error.message);
187
+ }
188
+ }
189
+
190
+ _fallback(reason, elapsed = 0, errorMessage = null) {
191
+ this.stats.fallbacks++;
192
+ const blocked = this.fallbackPolicy === 'block';
193
+
194
+ return {
195
+ status: blocked ? 'danger' : 'safe',
196
+ blocked,
197
+ threats: blocked ? [{
198
+ severity: 'medium',
199
+ category: 'scanner_fallback',
200
+ description: `Scanner ${reason}: using fallback policy (${this.fallbackPolicy})`,
201
+ confidence: 0
202
+ }] : [],
203
+ _fallback: true,
204
+ _fallbackReason: reason,
205
+ _elapsed: elapsed,
206
+ _error: errorMessage
207
+ };
208
+ }
209
+
210
+ getStats() {
211
+ return {
212
+ ...this.stats,
213
+ successRate: this.stats.scans > 0
214
+ ? `${((this.stats.successes / this.stats.scans) * 100).toFixed(1)}%`
215
+ : '0%',
216
+ fallbackPolicy: this.fallbackPolicy
217
+ };
218
+ }
219
+ }
220
+
221
+ // =========================================================================
222
+ // Threat Replay
223
+ // =========================================================================
224
+
225
+ class ThreatReplay {
226
+ constructor(options = {}) {
227
+ this.recordings = [];
228
+ this.maxRecordings = options.maxRecordings || 1000;
229
+ }
230
+
231
+ /**
232
+ * Record a blocked/detected request for later replay.
233
+ */
234
+ record(text, scanResult, metadata = {}) {
235
+ const entry = {
236
+ id: `replay_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
237
+ text,
238
+ originalResult: {
239
+ status: scanResult.status,
240
+ blocked: scanResult.blocked,
241
+ threats: (scanResult.threats || []).map(t => ({
242
+ severity: t.severity,
243
+ category: t.category,
244
+ description: t.description
245
+ }))
246
+ },
247
+ metadata,
248
+ recordedAt: new Date().toISOString()
249
+ };
250
+
251
+ this.recordings.push(entry);
252
+ while (this.recordings.length > this.maxRecordings) {
253
+ this.recordings.shift();
254
+ }
255
+
256
+ return entry.id;
257
+ }
258
+
259
+ /**
260
+ * Replay all recordings against a scan function to compare results.
261
+ */
262
+ replay(scanFn) {
263
+ const results = [];
264
+
265
+ for (const rec of this.recordings) {
266
+ const newResult = scanFn(rec.text);
267
+ const originalBlocked = rec.originalResult.blocked;
268
+ const newBlocked = newResult.threats && newResult.threats.length > 0;
269
+
270
+ let diff = 'same';
271
+ if (originalBlocked && !newBlocked) diff = 'now_allowed';
272
+ else if (!originalBlocked && newBlocked) diff = 'now_blocked';
273
+
274
+ results.push({
275
+ id: rec.id,
276
+ text: rec.text.substring(0, 100),
277
+ diff,
278
+ original: rec.originalResult,
279
+ replayed: {
280
+ status: newResult.status,
281
+ blocked: newBlocked,
282
+ threats: (newResult.threats || []).length
283
+ }
284
+ });
285
+ }
286
+
287
+ const nowAllowed = results.filter(r => r.diff === 'now_allowed');
288
+ const nowBlocked = results.filter(r => r.diff === 'now_blocked');
289
+
290
+ return {
291
+ total: results.length,
292
+ unchanged: results.filter(r => r.diff === 'same').length,
293
+ nowAllowed: nowAllowed.length,
294
+ nowBlocked: nowBlocked.length,
295
+ regressions: nowAllowed,
296
+ improvements: nowBlocked,
297
+ results
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Replay a single recording.
303
+ */
304
+ replayOne(id, scanFn) {
305
+ const rec = this.recordings.find(r => r.id === id);
306
+ if (!rec) return null;
307
+
308
+ const newResult = scanFn(rec.text);
309
+ return {
310
+ original: rec.originalResult,
311
+ replayed: newResult,
312
+ text: rec.text.substring(0, 200)
313
+ };
314
+ }
315
+
316
+ getRecordings() { return this.recordings; }
317
+ clear() { this.recordings = []; }
318
+ }
319
+
320
+ // =========================================================================
321
+ // Attack Attribution Chain
322
+ // =========================================================================
323
+
324
+ class AttackAttributionChain {
325
+ constructor() {
326
+ this.conversations = new Map();
327
+ }
328
+
329
+ /**
330
+ * Record a message in a conversation.
331
+ */
332
+ recordMessage(conversationId, message, scanResult, metadata = {}) {
333
+ if (!this.conversations.has(conversationId)) {
334
+ this.conversations.set(conversationId, {
335
+ id: conversationId,
336
+ messages: [],
337
+ firstThreatAt: null,
338
+ killChain: []
339
+ });
340
+ }
341
+
342
+ const conv = this.conversations.get(conversationId);
343
+ const hasThreat = scanResult.threats && scanResult.threats.length > 0;
344
+
345
+ const entry = {
346
+ index: conv.messages.length,
347
+ text: message.substring(0, 300),
348
+ timestamp: new Date().toISOString(),
349
+ hasThreat,
350
+ threats: hasThreat ? scanResult.threats.map(t => ({ severity: t.severity, category: t.category, description: t.description })) : [],
351
+ blocked: scanResult.blocked || false,
352
+ ...metadata
353
+ };
354
+
355
+ conv.messages.push(entry);
356
+
357
+ if (hasThreat && !conv.firstThreatAt) {
358
+ conv.firstThreatAt = entry.index;
359
+ }
360
+
361
+ if (hasThreat) {
362
+ conv.killChain.push({
363
+ step: conv.killChain.length + 1,
364
+ messageIndex: entry.index,
365
+ categories: entry.threats.map(t => t.category),
366
+ blocked: entry.blocked
367
+ });
368
+ }
369
+
370
+ return entry;
371
+ }
372
+
373
+ /**
374
+ * Reconstruct the kill chain for a conversation.
375
+ */
376
+ getKillChain(conversationId) {
377
+ const conv = this.conversations.get(conversationId);
378
+ if (!conv) return null;
379
+
380
+ const totalMessages = conv.messages.length;
381
+ const threatMessages = conv.messages.filter(m => m.hasThreat);
382
+
383
+ return {
384
+ conversationId,
385
+ totalMessages,
386
+ firstThreatAt: conv.firstThreatAt,
387
+ threatMessages: threatMessages.length,
388
+ messagesBeforeFirstThreat: conv.firstThreatAt || totalMessages,
389
+ killChain: conv.killChain,
390
+ timeline: conv.messages.map(m => ({
391
+ index: m.index,
392
+ hasThreat: m.hasThreat,
393
+ blocked: m.blocked,
394
+ categories: m.threats.map(t => t.category),
395
+ text: m.text.substring(0, 80)
396
+ }))
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Get all conversations with detected threats.
402
+ */
403
+ getCompromisedConversations() {
404
+ const result = [];
405
+ for (const [id, conv] of this.conversations) {
406
+ if (conv.killChain.length > 0) {
407
+ result.push({
408
+ id,
409
+ messageCount: conv.messages.length,
410
+ threatCount: conv.killChain.length,
411
+ firstThreatAt: conv.firstThreatAt,
412
+ categories: [...new Set(conv.killChain.flatMap(k => k.categories))]
413
+ });
414
+ }
415
+ }
416
+ return result;
417
+ }
418
+
419
+ clear() { this.conversations.clear(); }
420
+ }
421
+
422
+ // =========================================================================
423
+ // Diff Reports
424
+ // =========================================================================
425
+
426
+ class DiffReporter {
427
+ constructor() {
428
+ this.snapshots = [];
429
+ }
430
+
431
+ /**
432
+ * Take a snapshot of current stats.
433
+ */
434
+ takeSnapshot(label, stats) {
435
+ this.snapshots.push({
436
+ label,
437
+ timestamp: new Date().toISOString(),
438
+ stats: JSON.parse(JSON.stringify(stats))
439
+ });
440
+ return this.snapshots.length - 1;
441
+ }
442
+
443
+ /**
444
+ * Compare two snapshots.
445
+ */
446
+ compare(indexA, indexB) {
447
+ const a = this.snapshots[indexA];
448
+ const b = this.snapshots[indexB || this.snapshots.length - 1];
449
+ if (!a || !b) return null;
450
+
451
+ const diff = {};
452
+ const allKeys = new Set([...Object.keys(a.stats), ...Object.keys(b.stats)]);
453
+
454
+ for (const key of allKeys) {
455
+ const valA = a.stats[key];
456
+ const valB = b.stats[key];
457
+
458
+ if (typeof valA === 'number' && typeof valB === 'number') {
459
+ const change = valB - valA;
460
+ const pctChange = valA > 0 ? ((change / valA) * 100).toFixed(1) : 'N/A';
461
+ diff[key] = { before: valA, after: valB, change, percentChange: pctChange };
462
+ } else {
463
+ diff[key] = { before: valA, after: valB };
464
+ }
465
+ }
466
+
467
+ return {
468
+ from: { label: a.label, timestamp: a.timestamp },
469
+ to: { label: b.label, timestamp: b.timestamp },
470
+ diff,
471
+ improved: Object.entries(diff).filter(([, v]) => typeof v.change === 'number' && v.change > 0).map(([k]) => k),
472
+ degraded: Object.entries(diff).filter(([, v]) => typeof v.change === 'number' && v.change < 0).map(([k]) => k)
473
+ };
474
+ }
475
+
476
+ getSnapshots() { return this.snapshots.map((s, i) => ({ index: i, label: s.label, timestamp: s.timestamp })); }
477
+ }
478
+
479
+ // =========================================================================
480
+ // Security Posture Tracker
481
+ // =========================================================================
482
+
483
+ class PostureTracker {
484
+ constructor(options = {}) {
485
+ this.history = [];
486
+ this.maxHistory = options.maxHistory || 365;
487
+ }
488
+
489
+ /**
490
+ * Record a posture measurement.
491
+ */
492
+ record(measurement) {
493
+ this.history.push({
494
+ timestamp: new Date().toISOString(),
495
+ date: new Date().toISOString().split('T')[0],
496
+ ...measurement
497
+ });
498
+
499
+ while (this.history.length > this.maxHistory) {
500
+ this.history.shift();
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Get the trend over a period.
506
+ */
507
+ getTrend(days = 30) {
508
+ const cutoff = Date.now() - (days * 86400000);
509
+ const recent = this.history.filter(h => new Date(h.timestamp).getTime() > cutoff);
510
+
511
+ if (recent.length < 2) return { status: 'insufficient_data', dataPoints: recent.length };
512
+
513
+ const first = recent[0];
514
+ const last = recent[recent.length - 1];
515
+
516
+ const scoreChange = (last.shieldScore || 0) - (first.shieldScore || 0);
517
+ const threatChange = (last.threatsDetected || 0) - (first.threatsDetected || 0);
518
+
519
+ return {
520
+ period: `${days} days`,
521
+ dataPoints: recent.length,
522
+ scoreChange,
523
+ scoreTrend: scoreChange > 0 ? 'improving' : scoreChange < 0 ? 'degrading' : 'stable',
524
+ threatChange,
525
+ first: { date: first.date, shieldScore: first.shieldScore },
526
+ last: { date: last.date, shieldScore: last.shieldScore },
527
+ history: recent
528
+ };
529
+ }
530
+
531
+ /**
532
+ * Get a summary message.
533
+ */
534
+ getSummary() {
535
+ if (this.history.length === 0) return 'No posture data recorded yet.';
536
+
537
+ const latest = this.history[this.history.length - 1];
538
+ const trend = this.getTrend(30);
539
+
540
+ if (trend.status === 'insufficient_data') {
541
+ return `Current Shield Score: ${latest.shieldScore || 'N/A'}. Need more data for trend analysis.`;
542
+ }
543
+
544
+ const direction = trend.scoreChange > 0 ? 'improved' : trend.scoreChange < 0 ? 'degraded' : 'unchanged';
545
+ return `Shield Score ${direction} by ${Math.abs(trend.scoreChange)} points over the last ${trend.period} (${trend.first.shieldScore} → ${trend.last.shieldScore}).`;
546
+ }
547
+ }
548
+
549
+ module.exports = {
550
+ SamplingScanner,
551
+ ShadowComparison,
552
+ GracefulScanner,
553
+ ThreatReplay,
554
+ AttackAttributionChain,
555
+ DiffReporter,
556
+ PostureTracker
557
+ };