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.
- package/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- 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
|
+
};
|