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,494 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Conversation Tracking: Payload Fragmentation (#13), Language Switching (#35),
5
+ * Token Budget Analysis (#34), Instruction Hierarchy (#36),
6
+ * and Behavioral Fingerprinting (#38)
7
+ */
8
+
9
+ const { scanText } = require('./detector-core');
10
+
11
+ // =========================================================================
12
+ // PAYLOAD FRAGMENTATION DETECTOR
13
+ // =========================================================================
14
+
15
+ class FragmentationDetector {
16
+ /**
17
+ * Tracks messages over time and scans sliding windows to catch
18
+ * injections split across multiple messages.
19
+ *
20
+ * @param {object} [options]
21
+ * @param {number} [options.windowSize=5] - Number of messages in sliding window.
22
+ * @param {number} [options.maxHistory=50] - Maximum messages to retain.
23
+ * @param {Function} [options.onDetection] - Callback when fragmented injection detected.
24
+ */
25
+ constructor(options = {}) {
26
+ this.windowSize = options.windowSize || 5;
27
+ this.maxHistory = options.maxHistory || 50;
28
+ this.onDetection = options.onDetection || null;
29
+ this.messages = [];
30
+ }
31
+
32
+ /**
33
+ * Adds a message and scans sliding windows for fragmented injections.
34
+ *
35
+ * @param {string} text - The message text.
36
+ * @param {string} [source='user'] - Who sent the message.
37
+ * @returns {object} { fragmented: boolean, threats: Array }
38
+ */
39
+ addMessage(text, source = 'user') {
40
+ this.messages.push({ text, source, timestamp: Date.now() });
41
+ if (this.messages.length > this.maxHistory) {
42
+ this.messages.shift();
43
+ }
44
+
45
+ // Scan individual message
46
+ const singleResult = scanText(text, { source, sensitivity: 'high' });
47
+
48
+ // Scan sliding window (combine recent messages)
49
+ const windowMessages = this.messages.slice(-this.windowSize);
50
+ const combinedText = windowMessages.map(m => m.text).join(' ');
51
+ const windowResult = scanText(combinedText, { source: 'conversation_window', sensitivity: 'high' });
52
+
53
+ // Fragmented injection = window catches what individual messages don't
54
+ const newThreats = windowResult.threats.filter(wt =>
55
+ !singleResult.threats.some(st =>
56
+ st.category === wt.category && st.detail === wt.detail
57
+ )
58
+ );
59
+
60
+ const fragmentedThreats = newThreats.map(t => ({
61
+ ...t,
62
+ fragmented: true,
63
+ description: `Fragmented attack: ${t.description} (split across ${this.windowSize} messages)`,
64
+ windowSize: windowMessages.length
65
+ }));
66
+
67
+ if (fragmentedThreats.length > 0 && this.onDetection) {
68
+ try { this.onDetection({ threats: fragmentedThreats, window: windowMessages }); } catch (e) { console.error('[Agent Shield] onDetection callback error:', e.message); }
69
+ }
70
+
71
+ return {
72
+ fragmented: fragmentedThreats.length > 0,
73
+ threats: [...singleResult.threats, ...fragmentedThreats],
74
+ singleThreats: singleResult.threats,
75
+ fragmentedThreats
76
+ };
77
+ }
78
+
79
+ getHistory() {
80
+ return [...this.messages];
81
+ }
82
+
83
+ reset() {
84
+ this.messages = [];
85
+ }
86
+ }
87
+
88
+ // =========================================================================
89
+ // LANGUAGE SWITCHING DETECTOR
90
+ // =========================================================================
91
+
92
+ /**
93
+ * Unicode block ranges for detecting script changes.
94
+ */
95
+ const SCRIPT_RANGES = {
96
+ latin: /[\u0000-\u024F]/,
97
+ cyrillic: /[\u0400-\u04FF]/,
98
+ chinese: /[\u4E00-\u9FFF]/,
99
+ japanese_hiragana: /[\u3040-\u309F]/,
100
+ japanese_katakana: /[\u30A0-\u30FF]/,
101
+ korean: /[\uAC00-\uD7AF]/,
102
+ arabic: /[\u0600-\u06FF]/,
103
+ devanagari: /[\u0900-\u097F]/,
104
+ thai: /[\u0E00-\u0E7F]/
105
+ };
106
+
107
+ class LanguageSwitchDetector {
108
+ /**
109
+ * @param {object} [options]
110
+ * @param {Function} [options.onSwitch] - Callback when language switch detected.
111
+ */
112
+ constructor(options = {}) {
113
+ this.onSwitch = options.onSwitch || null;
114
+ this.history = [];
115
+ }
116
+
117
+ /**
118
+ * Analyzes text for the dominant script and tracks changes.
119
+ *
120
+ * @param {string} text
121
+ * @returns {object} { scripts: Array, switched: boolean, dominantScript: string, suspiciousSwitch: boolean }
122
+ */
123
+ analyze(text) {
124
+ if (!text) return { scripts: [], switched: false, dominantScript: null, suspiciousSwitch: false };
125
+
126
+ const scripts = this._detectScripts(text);
127
+ const dominantScript = scripts.length > 0 ? scripts[0].script : 'unknown';
128
+
129
+ this.history.push({ dominantScript, scripts, timestamp: Date.now() });
130
+ if (this.history.length > 50) this.history.shift();
131
+
132
+ // Check for switch from previous message
133
+ let switched = false;
134
+ let suspiciousSwitch = false;
135
+
136
+ if (this.history.length >= 2) {
137
+ const prev = this.history[this.history.length - 2];
138
+ switched = prev.dominantScript !== dominantScript && prev.dominantScript !== 'unknown' && dominantScript !== 'unknown';
139
+
140
+ // Suspicious: switching to a language with known injection patterns
141
+ if (switched) {
142
+ const suspiciousTargets = ['cyrillic', 'chinese', 'japanese_hiragana', 'japanese_katakana', 'arabic'];
143
+ suspiciousSwitch = suspiciousTargets.includes(dominantScript);
144
+
145
+ if (suspiciousSwitch && this.onSwitch) {
146
+ try {
147
+ this.onSwitch({
148
+ from: prev.dominantScript,
149
+ to: dominantScript,
150
+ text: text.substring(0, 200),
151
+ timestamp: Date.now()
152
+ });
153
+ } catch (e) { console.error('[Agent Shield] onSwitch callback error:', e.message); }
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check for mixed scripts within a single message (possible homoglyph attack)
159
+ const mixedScripts = scripts.length > 1 && scripts[0].percentage < 90;
160
+
161
+ return {
162
+ scripts,
163
+ switched,
164
+ dominantScript,
165
+ suspiciousSwitch,
166
+ mixedScripts,
167
+ multipleScripts: scripts.map(s => s.script)
168
+ };
169
+ }
170
+
171
+ /** @private */
172
+ _detectScripts(text) {
173
+ const counts = {};
174
+ let total = 0;
175
+
176
+ for (const char of text) {
177
+ if (/\s/.test(char)) continue;
178
+ total++;
179
+ for (const [script, range] of Object.entries(SCRIPT_RANGES)) {
180
+ if (range.test(char)) {
181
+ counts[script] = (counts[script] || 0) + 1;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+
187
+ if (total === 0) return [];
188
+
189
+ return Object.entries(counts)
190
+ .map(([script, count]) => ({ script, count, percentage: Math.round((count / total) * 100) }))
191
+ .sort((a, b) => b.count - a.count);
192
+ }
193
+
194
+ reset() {
195
+ this.history = [];
196
+ }
197
+ }
198
+
199
+ // =========================================================================
200
+ // TOKEN BUDGET ANALYZER
201
+ // =========================================================================
202
+
203
+ class TokenBudgetAnalyzer {
204
+ /**
205
+ * Monitors input sizes to detect padding/context-stuffing attacks.
206
+ *
207
+ * @param {object} [options]
208
+ * @param {number} [options.maxTokens=4096] - Expected max token budget.
209
+ * @param {number} [options.avgCharsPerToken=4] - Rough chars-per-token estimate.
210
+ * @param {number} [options.warningThreshold=0.7] - Warn at this % of budget.
211
+ * @param {number} [options.criticalThreshold=0.9] - Critical at this % of budget.
212
+ * @param {Function} [options.onWarning] - Callback on warning.
213
+ */
214
+ constructor(options = {}) {
215
+ this.maxTokens = options.maxTokens || 4096;
216
+ this.avgCharsPerToken = options.avgCharsPerToken || 4;
217
+ this.warningThreshold = options.warningThreshold || 0.7;
218
+ this.criticalThreshold = options.criticalThreshold || 0.9;
219
+ this.onWarning = options.onWarning || null;
220
+ this.totalCharsConsumed = 0;
221
+ }
222
+
223
+ /**
224
+ * Analyzes input size relative to token budget.
225
+ *
226
+ * @param {string} text - Input text.
227
+ * @returns {object} { estimatedTokens, budgetUsed, status, warning }
228
+ */
229
+ analyze(text) {
230
+ if (!text) return { estimatedTokens: 0, budgetUsed: 0, status: 'safe', warning: null };
231
+
232
+ const estimatedTokens = Math.ceil(text.length / this.avgCharsPerToken);
233
+ this.totalCharsConsumed += text.length;
234
+ const totalEstimatedTokens = Math.ceil(this.totalCharsConsumed / this.avgCharsPerToken);
235
+ const budgetUsed = totalEstimatedTokens / this.maxTokens;
236
+
237
+ let status = 'safe';
238
+ let warning = null;
239
+
240
+ if (budgetUsed >= this.criticalThreshold) {
241
+ status = 'critical';
242
+ warning = `Token budget ${Math.round(budgetUsed * 100)}% consumed. Possible context-stuffing attack.`;
243
+ } else if (budgetUsed >= this.warningThreshold) {
244
+ status = 'warning';
245
+ warning = `Token budget ${Math.round(budgetUsed * 100)}% consumed. Approaching limit.`;
246
+ }
247
+
248
+ // Detect suspiciously large single inputs
249
+ const singleInputRatio = estimatedTokens / this.maxTokens;
250
+ let paddingAttack = false;
251
+ if (singleInputRatio > 0.5) {
252
+ paddingAttack = true;
253
+ warning = `Single input uses ${Math.round(singleInputRatio * 100)}% of token budget. Possible padding attack.`;
254
+ status = 'critical';
255
+ }
256
+
257
+ if (warning && this.onWarning) {
258
+ try { this.onWarning({ status, warning, budgetUsed, estimatedTokens }); } catch (e) { console.error('[Agent Shield] onWarning callback error:', e.message); }
259
+ }
260
+
261
+ return {
262
+ estimatedTokens,
263
+ totalEstimatedTokens,
264
+ budgetUsed: Math.round(budgetUsed * 100) / 100,
265
+ status,
266
+ warning,
267
+ paddingAttack
268
+ };
269
+ }
270
+
271
+ reset() {
272
+ this.totalCharsConsumed = 0;
273
+ }
274
+ }
275
+
276
+ // =========================================================================
277
+ // INSTRUCTION HIERARCHY ENFORCER
278
+ // =========================================================================
279
+
280
+ class InstructionHierarchy {
281
+ /**
282
+ * Enforces a strict priority order: system > developer > user.
283
+ * Flags inputs that attempt to contradict higher-priority instructions.
284
+ *
285
+ * @param {object} [options]
286
+ * @param {Array<string>} [options.systemRules=[]] - Immutable system rules.
287
+ * @param {Array<string>} [options.developerRules=[]] - Developer-defined rules.
288
+ * @param {Function} [options.onViolation] - Callback on hierarchy violation.
289
+ */
290
+ constructor(options = {}) {
291
+ this.systemRules = options.systemRules || [];
292
+ this.developerRules = options.developerRules || [];
293
+ this.onViolation = options.onViolation || null;
294
+ }
295
+
296
+ /**
297
+ * Checks if user input contradicts system or developer rules.
298
+ *
299
+ * @param {string} text - User input.
300
+ * @returns {object} { allowed: boolean, violations: Array }
301
+ */
302
+ check(text) {
303
+ if (!text) return { allowed: true, violations: [] };
304
+
305
+ const violations = [];
306
+ const lower = text.toLowerCase();
307
+
308
+ // Check against system rules
309
+ for (const rule of this.systemRules) {
310
+ const negated = this._findNegation(lower, rule.toLowerCase());
311
+ if (negated) {
312
+ violations.push({
313
+ level: 'system',
314
+ rule,
315
+ severity: 'critical',
316
+ description: `User input contradicts system rule: "${rule}"`
317
+ });
318
+ }
319
+ }
320
+
321
+ // Check against developer rules
322
+ for (const rule of this.developerRules) {
323
+ const negated = this._findNegation(lower, rule.toLowerCase());
324
+ if (negated) {
325
+ violations.push({
326
+ level: 'developer',
327
+ rule,
328
+ severity: 'high',
329
+ description: `User input contradicts developer rule: "${rule}"`
330
+ });
331
+ }
332
+ }
333
+
334
+ if (violations.length > 0 && this.onViolation) {
335
+ try { this.onViolation({ violations, text: text.substring(0, 200) }); } catch (e) { console.error('[Agent Shield] onViolation callback error:', e.message); }
336
+ }
337
+
338
+ return { allowed: violations.length === 0, violations };
339
+ }
340
+
341
+ /**
342
+ * Checks if text contains a negation or contradiction of a rule.
343
+ * @private
344
+ */
345
+ _findNegation(text, rule) {
346
+ // Extract key phrases from the rule
347
+ const words = rule.split(/\s+/).filter(w => w.length > 3);
348
+ if (words.length === 0) return false;
349
+
350
+ // Escape regex special characters in keywords to prevent ReDoS
351
+ const escaped = words.slice(0, 5).map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
352
+ const ruleKeywords = escaped.join('|');
353
+
354
+ // Use .{0,80} instead of .* to prevent catastrophic backtracking
355
+ const negationPattern = new RegExp(
356
+ `(?:don'?t|do\\s+not|never|stop|disable|remove|ignore|skip|bypass|override)\\s+.{0,80}(?:${ruleKeywords})`,
357
+ 'i'
358
+ );
359
+
360
+ return negationPattern.test(text);
361
+ }
362
+ }
363
+
364
+ // =========================================================================
365
+ // BEHAVIORAL FINGERPRINT
366
+ // =========================================================================
367
+
368
+ class BehavioralFingerprint {
369
+ /**
370
+ * Learns normal behavior patterns and flags anomalies.
371
+ *
372
+ * @param {object} [options]
373
+ * @param {number} [options.learningPeriod=50] - Number of events before flagging anomalies.
374
+ * @param {number} [options.stdDevThreshold=2] - Standard deviations for anomaly threshold.
375
+ * @param {Function} [options.onAnomaly] - Callback on anomaly detection.
376
+ */
377
+ constructor(options = {}) {
378
+ this.learningPeriod = options.learningPeriod || 50;
379
+ this.stdDevThreshold = options.stdDevThreshold || 2;
380
+ this.onAnomaly = options.onAnomaly || null;
381
+
382
+ this.metrics = {
383
+ inputLengths: [],
384
+ responseTimesMs: [],
385
+ toolCallFrequency: {},
386
+ threatFrequency: []
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Records an event and checks for anomalies.
392
+ *
393
+ * @param {object} event
394
+ * @param {number} [event.inputLength] - Length of input text.
395
+ * @param {number} [event.responseTimeMs] - Response time in ms.
396
+ * @param {string} [event.toolName] - Tool that was called.
397
+ * @param {number} [event.threatCount=0] - Number of threats detected.
398
+ * @returns {object} { anomalies: Array, isLearning: boolean }
399
+ */
400
+ record(event) {
401
+ if (!event || typeof event !== 'object') {
402
+ return { anomalies: [], isLearning: true };
403
+ }
404
+ const anomalies = [];
405
+ const isLearning = this.metrics.inputLengths.length < this.learningPeriod;
406
+
407
+ if (event.inputLength !== undefined) {
408
+ this.metrics.inputLengths.push(event.inputLength);
409
+ if (!isLearning) {
410
+ const stats = this._calcStats(this.metrics.inputLengths.slice(0, -1));
411
+ if (Math.abs(event.inputLength - stats.mean) > stats.stdDev * this.stdDevThreshold) {
412
+ anomalies.push({
413
+ type: 'input_length',
414
+ value: event.inputLength,
415
+ expected: `${Math.round(stats.mean)} ± ${Math.round(stats.stdDev * this.stdDevThreshold)}`,
416
+ severity: 'medium',
417
+ description: `Unusual input length: ${event.inputLength} chars (normal: ~${Math.round(stats.mean)})`
418
+ });
419
+ }
420
+ }
421
+ }
422
+
423
+ if (event.responseTimeMs !== undefined) {
424
+ this.metrics.responseTimesMs.push(event.responseTimeMs);
425
+ }
426
+
427
+ if (event.toolName) {
428
+ this.metrics.toolCallFrequency[event.toolName] = (this.metrics.toolCallFrequency[event.toolName] || 0) + 1;
429
+ }
430
+
431
+ if (event.threatCount !== undefined) {
432
+ this.metrics.threatFrequency.push(event.threatCount);
433
+ if (!isLearning && event.threatCount > 0) {
434
+ const recentThreats = this.metrics.threatFrequency.slice(-10);
435
+ const avgThreats = recentThreats.reduce((a, b) => a + b, 0) / recentThreats.length;
436
+ if (avgThreats > 2) {
437
+ anomalies.push({
438
+ type: 'threat_spike',
439
+ value: avgThreats,
440
+ severity: 'high',
441
+ description: `Sustained threat spike: avg ${avgThreats.toFixed(1)} threats per input over last 10 inputs.`
442
+ });
443
+ }
444
+ }
445
+ }
446
+
447
+ // Cap stored metrics
448
+ const maxMetrics = 500;
449
+ if (this.metrics.inputLengths.length > maxMetrics) this.metrics.inputLengths = this.metrics.inputLengths.slice(-maxMetrics);
450
+ if (this.metrics.responseTimesMs.length > maxMetrics) this.metrics.responseTimesMs = this.metrics.responseTimesMs.slice(-maxMetrics);
451
+ if (this.metrics.threatFrequency.length > maxMetrics) this.metrics.threatFrequency = this.metrics.threatFrequency.slice(-maxMetrics);
452
+
453
+ if (anomalies.length > 0 && this.onAnomaly) {
454
+ try { this.onAnomaly({ anomalies, timestamp: Date.now() }); } catch (e) { console.error('[Agent Shield] onAnomaly callback error:', e.message); }
455
+ }
456
+
457
+ return { anomalies, isLearning };
458
+ }
459
+
460
+ /** @private */
461
+ _calcStats(values) {
462
+ if (values.length === 0) return { mean: 0, stdDev: 0 };
463
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
464
+ const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
465
+ return { mean, stdDev: Math.sqrt(variance) };
466
+ }
467
+
468
+ getProfile() {
469
+ const inputStats = this._calcStats(this.metrics.inputLengths);
470
+ const responseStats = this._calcStats(this.metrics.responseTimesMs);
471
+ return {
472
+ sampleSize: this.metrics.inputLengths.length,
473
+ isLearning: this.metrics.inputLengths.length < this.learningPeriod,
474
+ avgInputLength: Math.round(inputStats.mean),
475
+ avgResponseTimeMs: Math.round(responseStats.mean),
476
+ topTools: Object.entries(this.metrics.toolCallFrequency)
477
+ .sort((a, b) => b[1] - a[1])
478
+ .slice(0, 10)
479
+ .map(([tool, count]) => ({ tool, count }))
480
+ };
481
+ }
482
+
483
+ reset() {
484
+ this.metrics = { inputLengths: [], responseTimesMs: [], toolCallFrequency: {}, threatFrequency: [] };
485
+ }
486
+ }
487
+
488
+ module.exports = {
489
+ FragmentationDetector,
490
+ LanguageSwitchDetector,
491
+ TokenBudgetAnalyzer,
492
+ InstructionHierarchy,
493
+ BehavioralFingerprint
494
+ };