agentshield-sdk 7.2.1 → 7.3.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 +90 -1
- package/README.md +33 -1
- package/bin/agent-shield.js +19 -0
- package/package.json +5 -2
- package/src/attack-genome.js +536 -0
- package/src/attack-replay.js +246 -0
- package/src/audit.js +619 -0
- package/src/behavioral-dna.js +762 -0
- package/src/compliance-authority.js +803 -0
- package/src/distributed.js +2 -1
- package/src/errors.js +9 -0
- package/src/evolution-simulator.js +650 -0
- package/src/flight-recorder.js +379 -0
- package/src/herd-immunity.js +521 -0
- package/src/index.js +6 -5
- package/src/intent-firewall.js +775 -0
- package/src/main.js +129 -0
- package/src/mcp-security-runtime.js +6 -5
- package/src/middleware.js +6 -3
- package/src/pii.js +4 -1
- package/src/real-attack-datasets.js +246 -0
- package/src/report-generator.js +640 -0
- package/src/soc-dashboard.js +394 -0
- package/src/supply-chain.js +667 -0
- package/src/threat-intel-federation.js +343 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield -- Agent Behavioral DNA (v7.5)
|
|
5
|
+
*
|
|
6
|
+
* Creates a comprehensive fingerprint of what "normal" looks like for a
|
|
7
|
+
* specific agent deployment, then detects compromise by comparing current
|
|
8
|
+
* behavior against the learned baseline.
|
|
9
|
+
*
|
|
10
|
+
* Goes beyond z-score anomaly detection (behavior-profiling.js) by tracking
|
|
11
|
+
* multi-dimensional feature vectors, categorical distributions, cross-feature
|
|
12
|
+
* correlations, and producing a portable DNA fingerprint.
|
|
13
|
+
*
|
|
14
|
+
* All processing runs locally -- no data ever leaves your environment.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// =========================================================================
|
|
18
|
+
// STATISTICAL HELPERS
|
|
19
|
+
// =========================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calculate mean of a numeric array.
|
|
23
|
+
* @param {number[]} arr
|
|
24
|
+
* @returns {number}
|
|
25
|
+
*/
|
|
26
|
+
function mean(arr) {
|
|
27
|
+
return arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate sample standard deviation.
|
|
32
|
+
* @param {number[]} arr
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
function stdDev(arr) {
|
|
36
|
+
if (arr.length < 2) return 0;
|
|
37
|
+
const m = mean(arr);
|
|
38
|
+
const squaredDiffs = arr.map(x => (x - m) ** 2);
|
|
39
|
+
return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / (arr.length - 1));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calculate the z-score for a value given mean and standard deviation.
|
|
44
|
+
* @param {number} value
|
|
45
|
+
* @param {number} m
|
|
46
|
+
* @param {number} sd
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
function zScore(value, m, sd) {
|
|
50
|
+
if (sd === 0) return value === m ? 0 : Infinity;
|
|
51
|
+
return (value - m) / sd;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculate Pearson correlation between two arrays.
|
|
56
|
+
* @param {number[]} xs
|
|
57
|
+
* @param {number[]} ys
|
|
58
|
+
* @returns {number}
|
|
59
|
+
*/
|
|
60
|
+
function pearsonCorrelation(xs, ys) {
|
|
61
|
+
const n = Math.min(xs.length, ys.length);
|
|
62
|
+
if (n < 3) return 0;
|
|
63
|
+
const mx = mean(xs.slice(0, n));
|
|
64
|
+
const my = mean(ys.slice(0, n));
|
|
65
|
+
let num = 0;
|
|
66
|
+
let dx2 = 0;
|
|
67
|
+
let dy2 = 0;
|
|
68
|
+
for (let i = 0; i < n; i++) {
|
|
69
|
+
const dx = xs[i] - mx;
|
|
70
|
+
const dy = ys[i] - my;
|
|
71
|
+
num += dx * dy;
|
|
72
|
+
dx2 += dx * dx;
|
|
73
|
+
dy2 += dy * dy;
|
|
74
|
+
}
|
|
75
|
+
const denom = Math.sqrt(dx2 * dy2);
|
|
76
|
+
return denom === 0 ? 0 : num / denom;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Shannon entropy of a frequency distribution (object with counts).
|
|
81
|
+
* @param {Object<string, number>} dist
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
function shannonEntropy(dist) {
|
|
85
|
+
const total = Object.values(dist).reduce((a, b) => a + b, 0);
|
|
86
|
+
if (total === 0) return 0;
|
|
87
|
+
let h = 0;
|
|
88
|
+
for (const count of Object.values(dist)) {
|
|
89
|
+
if (count === 0) continue;
|
|
90
|
+
const p = count / total;
|
|
91
|
+
h -= p * Math.log2(p);
|
|
92
|
+
}
|
|
93
|
+
return h;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =========================================================================
|
|
97
|
+
// DEFAULT FEATURES
|
|
98
|
+
// =========================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default numeric features tracked by BehavioralDNA.
|
|
102
|
+
* @type {string[]}
|
|
103
|
+
*/
|
|
104
|
+
const DEFAULT_NUMERIC_FEATURES = [
|
|
105
|
+
'responseLength',
|
|
106
|
+
'responseTimeMs',
|
|
107
|
+
'toolCount',
|
|
108
|
+
'threatScore',
|
|
109
|
+
'sentimentScore'
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Default categorical features tracked by BehavioralDNA.
|
|
114
|
+
* @type {string[]}
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_CATEGORICAL_FEATURES = [
|
|
117
|
+
'topicCategory',
|
|
118
|
+
'languageUsed'
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// =========================================================================
|
|
122
|
+
// BEHAVIORAL DNA
|
|
123
|
+
// =========================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Comprehensive behavioral fingerprint for an agent deployment.
|
|
127
|
+
* Learns what "normal" looks like, then detects deviations.
|
|
128
|
+
*/
|
|
129
|
+
class BehavioralDNA {
|
|
130
|
+
/**
|
|
131
|
+
* @param {object} [options]
|
|
132
|
+
* @param {number} [options.learningPeriod=50] - Observations before detection activates.
|
|
133
|
+
* @param {number} [options.anomalyThreshold=2.5] - Std deviations for anomaly detection.
|
|
134
|
+
* @param {string[]} [options.features] - Numeric features to track.
|
|
135
|
+
* @param {string[]} [options.categoricalFeatures] - Categorical features to track.
|
|
136
|
+
* @param {number} [options.windowSize=500] - Max observations to retain per feature.
|
|
137
|
+
* @param {boolean} [options.trackCorrelations=true] - Track cross-feature correlations.
|
|
138
|
+
*/
|
|
139
|
+
constructor(options = {}) {
|
|
140
|
+
this.learningPeriod = options.learningPeriod || 50;
|
|
141
|
+
this.anomalyThreshold = options.anomalyThreshold || 2.5;
|
|
142
|
+
this.windowSize = options.windowSize || 500;
|
|
143
|
+
this.trackCorrelations = options.trackCorrelations !== false;
|
|
144
|
+
|
|
145
|
+
this._numericFeatures = options.features || DEFAULT_NUMERIC_FEATURES.slice();
|
|
146
|
+
this._categoricalFeatures = options.categoricalFeatures || DEFAULT_CATEGORICAL_FEATURES.slice();
|
|
147
|
+
|
|
148
|
+
this._numericData = {};
|
|
149
|
+
for (const f of this._numericFeatures) {
|
|
150
|
+
this._numericData[f] = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._categoricalData = {};
|
|
154
|
+
for (const f of this._categoricalFeatures) {
|
|
155
|
+
this._categoricalData[f] = {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._toolDistribution = {};
|
|
159
|
+
this._observationCount = 0;
|
|
160
|
+
this._createdAt = Date.now();
|
|
161
|
+
this._lastObservation = null;
|
|
162
|
+
|
|
163
|
+
console.log(
|
|
164
|
+
'[Agent Shield] BehavioralDNA initialized (learningPeriod: %d, threshold: %s)',
|
|
165
|
+
this.learningPeriod,
|
|
166
|
+
this.anomalyThreshold
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Record an observation of agent behavior.
|
|
172
|
+
* @param {object} observation
|
|
173
|
+
* @param {string[]} [observation.toolsCalled] - Tools used in this interaction.
|
|
174
|
+
* @param {number} [observation.responseLength] - Length of agent response.
|
|
175
|
+
* @param {number} [observation.responseTimeMs] - Time taken to respond in ms.
|
|
176
|
+
* @param {number} [observation.threatScore] - Threat score from scanning (0-1).
|
|
177
|
+
* @param {string} [observation.topicCategory] - Detected topic category.
|
|
178
|
+
* @param {number} [observation.sentimentScore] - Sentiment score (-1 to 1).
|
|
179
|
+
* @param {string} [observation.languageUsed] - Language of response.
|
|
180
|
+
*/
|
|
181
|
+
observe(observation) {
|
|
182
|
+
if (!observation || typeof observation !== 'object') return;
|
|
183
|
+
|
|
184
|
+
this._observationCount++;
|
|
185
|
+
this._lastObservation = Date.now();
|
|
186
|
+
|
|
187
|
+
// Record numeric features
|
|
188
|
+
for (const f of this._numericFeatures) {
|
|
189
|
+
if (f === 'toolCount' && observation.toolsCalled !== undefined) {
|
|
190
|
+
this._pushNumeric(f, Array.isArray(observation.toolsCalled) ? observation.toolsCalled.length : 0);
|
|
191
|
+
} else if (observation[f] !== undefined && typeof observation[f] === 'number') {
|
|
192
|
+
this._pushNumeric(f, observation[f]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Record categorical features
|
|
197
|
+
for (const f of this._categoricalFeatures) {
|
|
198
|
+
if (observation[f] !== undefined && observation[f] !== null) {
|
|
199
|
+
const val = String(observation[f]);
|
|
200
|
+
if (!this._categoricalData[f]) this._categoricalData[f] = {};
|
|
201
|
+
this._categoricalData[f][val] = (this._categoricalData[f][val] || 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Record tool distribution
|
|
206
|
+
if (Array.isArray(observation.toolsCalled)) {
|
|
207
|
+
for (const tool of observation.toolsCalled) {
|
|
208
|
+
this._toolDistribution[tool] = (this._toolDistribution[tool] || 0) + 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns true if still in the learning period.
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
isLearning() {
|
|
218
|
+
return this._observationCount < this.learningPeriod;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Returns the learned baseline statistics.
|
|
223
|
+
* @returns {object} Baseline with mean, stdDev, min, max, samples for each numeric feature,
|
|
224
|
+
* plus categorical distributions and tool usage.
|
|
225
|
+
*/
|
|
226
|
+
getBaseline() {
|
|
227
|
+
const numeric = {};
|
|
228
|
+
for (const f of this._numericFeatures) {
|
|
229
|
+
const values = this._numericData[f] || [];
|
|
230
|
+
if (values.length > 0) {
|
|
231
|
+
numeric[f] = {
|
|
232
|
+
mean: _round(mean(values)),
|
|
233
|
+
stdDev: _round(stdDev(values)),
|
|
234
|
+
min: Math.min(...values),
|
|
235
|
+
max: Math.max(...values),
|
|
236
|
+
samples: values.length
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const categorical = {};
|
|
242
|
+
for (const f of this._categoricalFeatures) {
|
|
243
|
+
const dist = this._categoricalData[f] || {};
|
|
244
|
+
const total = Object.values(dist).reduce((a, b) => a + b, 0);
|
|
245
|
+
if (total > 0) {
|
|
246
|
+
const normalized = {};
|
|
247
|
+
for (const [key, count] of Object.entries(dist)) {
|
|
248
|
+
normalized[key] = _round(count / total);
|
|
249
|
+
}
|
|
250
|
+
categorical[f] = {
|
|
251
|
+
distribution: normalized,
|
|
252
|
+
entropy: _round(shannonEntropy(dist)),
|
|
253
|
+
totalSamples: total
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const toolTotal = Object.values(this._toolDistribution).reduce((a, b) => a + b, 0);
|
|
259
|
+
const toolNormalized = {};
|
|
260
|
+
if (toolTotal > 0) {
|
|
261
|
+
for (const [tool, count] of Object.entries(this._toolDistribution)) {
|
|
262
|
+
toolNormalized[tool] = _round(count / toolTotal);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const correlations = {};
|
|
267
|
+
if (this.trackCorrelations) {
|
|
268
|
+
const featureNames = this._numericFeatures.filter(f => (this._numericData[f] || []).length >= 3);
|
|
269
|
+
for (let i = 0; i < featureNames.length; i++) {
|
|
270
|
+
for (let j = i + 1; j < featureNames.length; j++) {
|
|
271
|
+
const key = featureNames[i] + ':' + featureNames[j];
|
|
272
|
+
correlations[key] = _round(
|
|
273
|
+
pearsonCorrelation(this._numericData[featureNames[i]], this._numericData[featureNames[j]])
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
numeric,
|
|
281
|
+
categorical,
|
|
282
|
+
toolDistribution: toolNormalized,
|
|
283
|
+
correlations,
|
|
284
|
+
observationCount: this._observationCount,
|
|
285
|
+
isLearning: this.isLearning()
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Compare an observation against the learned baseline.
|
|
291
|
+
* Only works after the learning period is complete.
|
|
292
|
+
* @param {object} observation - Same shape as observe().
|
|
293
|
+
* @returns {object} { anomaly, score, deviations, explanation }
|
|
294
|
+
*/
|
|
295
|
+
detect(observation) {
|
|
296
|
+
if (this.isLearning()) {
|
|
297
|
+
return {
|
|
298
|
+
anomaly: false,
|
|
299
|
+
score: 0,
|
|
300
|
+
deviations: [],
|
|
301
|
+
explanation: 'Still in learning period (' + this._observationCount + '/' + this.learningPeriod + ' observations).'
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const deviations = [];
|
|
306
|
+
|
|
307
|
+
// Check numeric features
|
|
308
|
+
for (const f of this._numericFeatures) {
|
|
309
|
+
let value;
|
|
310
|
+
if (f === 'toolCount' && observation.toolsCalled !== undefined) {
|
|
311
|
+
value = Array.isArray(observation.toolsCalled) ? observation.toolsCalled.length : 0;
|
|
312
|
+
} else {
|
|
313
|
+
value = observation[f];
|
|
314
|
+
}
|
|
315
|
+
if (value === undefined || typeof value !== 'number') continue;
|
|
316
|
+
|
|
317
|
+
const values = this._numericData[f] || [];
|
|
318
|
+
if (values.length < 2) continue;
|
|
319
|
+
|
|
320
|
+
const m = mean(values);
|
|
321
|
+
const sd = stdDev(values);
|
|
322
|
+
const z = zScore(value, m, sd);
|
|
323
|
+
|
|
324
|
+
if (Math.abs(z) > this.anomalyThreshold) {
|
|
325
|
+
deviations.push({
|
|
326
|
+
feature: f,
|
|
327
|
+
type: 'numeric',
|
|
328
|
+
value,
|
|
329
|
+
expected: { mean: _round(m), stdDev: _round(sd) },
|
|
330
|
+
zScore: _round(z),
|
|
331
|
+
direction: z > 0 ? 'above' : 'below',
|
|
332
|
+
severity: _deviationSeverity(Math.abs(z), this.anomalyThreshold)
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check categorical features for novel or rare values
|
|
338
|
+
for (const f of this._categoricalFeatures) {
|
|
339
|
+
if (observation[f] === undefined || observation[f] === null) continue;
|
|
340
|
+
const val = String(observation[f]);
|
|
341
|
+
const dist = this._categoricalData[f] || {};
|
|
342
|
+
const total = Object.values(dist).reduce((a, b) => a + b, 0);
|
|
343
|
+
|
|
344
|
+
if (total === 0) continue;
|
|
345
|
+
|
|
346
|
+
const count = dist[val] || 0;
|
|
347
|
+
const freq = count / total;
|
|
348
|
+
|
|
349
|
+
if (count === 0) {
|
|
350
|
+
// Completely new category value never seen during learning
|
|
351
|
+
deviations.push({
|
|
352
|
+
feature: f,
|
|
353
|
+
type: 'categorical',
|
|
354
|
+
value: val,
|
|
355
|
+
expected: Object.keys(dist),
|
|
356
|
+
frequency: 0,
|
|
357
|
+
severity: 'high',
|
|
358
|
+
reason: 'Never-before-seen value'
|
|
359
|
+
});
|
|
360
|
+
} else if (freq < 0.01 && total > 20) {
|
|
361
|
+
// Extremely rare value
|
|
362
|
+
deviations.push({
|
|
363
|
+
feature: f,
|
|
364
|
+
type: 'categorical',
|
|
365
|
+
value: val,
|
|
366
|
+
frequency: _round(freq),
|
|
367
|
+
severity: 'medium',
|
|
368
|
+
reason: 'Extremely rare value (seen in <1% of observations)'
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check tool usage for novel tools
|
|
374
|
+
if (Array.isArray(observation.toolsCalled)) {
|
|
375
|
+
const totalTools = Object.values(this._toolDistribution).reduce((a, b) => a + b, 0);
|
|
376
|
+
for (const tool of observation.toolsCalled) {
|
|
377
|
+
if (totalTools > 0 && !this._toolDistribution[tool]) {
|
|
378
|
+
deviations.push({
|
|
379
|
+
feature: 'toolsCalled',
|
|
380
|
+
type: 'tool',
|
|
381
|
+
value: tool,
|
|
382
|
+
severity: 'high',
|
|
383
|
+
reason: 'Tool never seen in baseline'
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Compute composite anomaly score (0 to 1)
|
|
390
|
+
const score = _compositeScore(deviations, this.anomalyThreshold);
|
|
391
|
+
|
|
392
|
+
// Build explanation
|
|
393
|
+
let explanation = '';
|
|
394
|
+
if (deviations.length === 0) {
|
|
395
|
+
explanation = 'Observation is within normal behavioral parameters.';
|
|
396
|
+
} else {
|
|
397
|
+
const parts = deviations.map(d => {
|
|
398
|
+
if (d.type === 'numeric') {
|
|
399
|
+
return d.feature + ' is ' + d.direction + ' normal (z=' + d.zScore + ')';
|
|
400
|
+
}
|
|
401
|
+
if (d.type === 'categorical') {
|
|
402
|
+
return d.feature + '="' + d.value + '" - ' + d.reason;
|
|
403
|
+
}
|
|
404
|
+
return d.feature + '="' + d.value + '" - ' + d.reason;
|
|
405
|
+
});
|
|
406
|
+
explanation = 'Anomalous behavior detected: ' + parts.join('; ') + '.';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
anomaly: deviations.length > 0,
|
|
411
|
+
score: _round(score),
|
|
412
|
+
deviations,
|
|
413
|
+
explanation
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Return the full behavioral DNA fingerprint as a portable JSON-safe object.
|
|
419
|
+
* Can be saved and loaded later with loadFingerprint().
|
|
420
|
+
* @returns {object}
|
|
421
|
+
*/
|
|
422
|
+
getFingerprint() {
|
|
423
|
+
return {
|
|
424
|
+
version: 1,
|
|
425
|
+
createdAt: this._createdAt,
|
|
426
|
+
lastObservation: this._lastObservation,
|
|
427
|
+
observationCount: this._observationCount,
|
|
428
|
+
config: {
|
|
429
|
+
learningPeriod: this.learningPeriod,
|
|
430
|
+
anomalyThreshold: this.anomalyThreshold,
|
|
431
|
+
windowSize: this.windowSize,
|
|
432
|
+
trackCorrelations: this.trackCorrelations,
|
|
433
|
+
numericFeatures: this._numericFeatures.slice(),
|
|
434
|
+
categoricalFeatures: this._categoricalFeatures.slice()
|
|
435
|
+
},
|
|
436
|
+
numericData: _deepCopyNumeric(this._numericData),
|
|
437
|
+
categoricalData: _deepCopyCategorical(this._categoricalData),
|
|
438
|
+
toolDistribution: { ...this._toolDistribution },
|
|
439
|
+
baseline: this.getBaseline()
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Load a previously saved fingerprint, restoring full state.
|
|
445
|
+
* @param {object} data - Fingerprint object from getFingerprint().
|
|
446
|
+
*/
|
|
447
|
+
loadFingerprint(data) {
|
|
448
|
+
if (!data || typeof data !== 'object') {
|
|
449
|
+
throw new Error('[Agent Shield] Invalid fingerprint data');
|
|
450
|
+
}
|
|
451
|
+
if (data.version !== 1) {
|
|
452
|
+
throw new Error('[Agent Shield] Unsupported fingerprint version: ' + data.version);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this._createdAt = data.createdAt || Date.now();
|
|
456
|
+
this._lastObservation = data.lastObservation || null;
|
|
457
|
+
this._observationCount = data.observationCount || 0;
|
|
458
|
+
|
|
459
|
+
if (data.config) {
|
|
460
|
+
this.learningPeriod = data.config.learningPeriod || this.learningPeriod;
|
|
461
|
+
this.anomalyThreshold = data.config.anomalyThreshold || this.anomalyThreshold;
|
|
462
|
+
this.windowSize = data.config.windowSize || this.windowSize;
|
|
463
|
+
this.trackCorrelations = data.config.trackCorrelations !== false;
|
|
464
|
+
this._numericFeatures = data.config.numericFeatures || this._numericFeatures;
|
|
465
|
+
this._categoricalFeatures = data.config.categoricalFeatures || this._categoricalFeatures;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this._numericData = {};
|
|
469
|
+
for (const f of this._numericFeatures) {
|
|
470
|
+
this._numericData[f] = (data.numericData && Array.isArray(data.numericData[f]))
|
|
471
|
+
? data.numericData[f].slice()
|
|
472
|
+
: [];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this._categoricalData = {};
|
|
476
|
+
for (const f of this._categoricalFeatures) {
|
|
477
|
+
this._categoricalData[f] = (data.categoricalData && data.categoricalData[f])
|
|
478
|
+
? { ...data.categoricalData[f] }
|
|
479
|
+
: {};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this._toolDistribution = data.toolDistribution ? { ...data.toolDistribution } : {};
|
|
483
|
+
|
|
484
|
+
console.log(
|
|
485
|
+
'[Agent Shield] BehavioralDNA fingerprint loaded (%d observations)',
|
|
486
|
+
this._observationCount
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Clear all learned data and reset to initial state.
|
|
492
|
+
*/
|
|
493
|
+
reset() {
|
|
494
|
+
for (const f of this._numericFeatures) {
|
|
495
|
+
this._numericData[f] = [];
|
|
496
|
+
}
|
|
497
|
+
for (const f of this._categoricalFeatures) {
|
|
498
|
+
this._categoricalData[f] = {};
|
|
499
|
+
}
|
|
500
|
+
this._toolDistribution = {};
|
|
501
|
+
this._observationCount = 0;
|
|
502
|
+
this._createdAt = Date.now();
|
|
503
|
+
this._lastObservation = null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** @private */
|
|
507
|
+
_pushNumeric(feature, value) {
|
|
508
|
+
if (!this._numericData[feature]) this._numericData[feature] = [];
|
|
509
|
+
this._numericData[feature].push(value);
|
|
510
|
+
if (this._numericData[feature].length > this.windowSize) {
|
|
511
|
+
this._numericData[feature].shift();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// =========================================================================
|
|
517
|
+
// AGENT PROFILER
|
|
518
|
+
// =========================================================================
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Manages BehavioralDNA profiles for multiple agents.
|
|
522
|
+
*/
|
|
523
|
+
class AgentProfiler {
|
|
524
|
+
/**
|
|
525
|
+
* @param {object} [defaultOptions] - Default BehavioralDNA options for new profiles.
|
|
526
|
+
*/
|
|
527
|
+
constructor(defaultOptions = {}) {
|
|
528
|
+
this._defaultOptions = defaultOptions;
|
|
529
|
+
this._profiles = new Map();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Create a new BehavioralDNA profile for an agent.
|
|
534
|
+
* @param {string} agentId - Unique identifier for the agent.
|
|
535
|
+
* @param {object} [options] - Override default options for this agent.
|
|
536
|
+
* @returns {BehavioralDNA}
|
|
537
|
+
*/
|
|
538
|
+
createProfile(agentId, options = {}) {
|
|
539
|
+
const merged = { ...this._defaultOptions, ...options };
|
|
540
|
+
const dna = new BehavioralDNA(merged);
|
|
541
|
+
this._profiles.set(agentId, dna);
|
|
542
|
+
console.log('[Agent Shield] AgentProfiler: created profile for "%s"', agentId);
|
|
543
|
+
return dna;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Get an existing profile for an agent.
|
|
548
|
+
* @param {string} agentId
|
|
549
|
+
* @returns {BehavioralDNA|null}
|
|
550
|
+
*/
|
|
551
|
+
getProfile(agentId) {
|
|
552
|
+
return this._profiles.get(agentId) || null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Check an observation against an agent's DNA.
|
|
557
|
+
* If the agent has no profile yet, one is created automatically.
|
|
558
|
+
* The observation is also recorded (observe) after detection.
|
|
559
|
+
* @param {object} observation - Observation object.
|
|
560
|
+
* @param {string} agentId - Agent identifier.
|
|
561
|
+
* @returns {object} Detection result from BehavioralDNA.detect().
|
|
562
|
+
*/
|
|
563
|
+
checkAll(observation, agentId) {
|
|
564
|
+
let dna = this._profiles.get(agentId);
|
|
565
|
+
if (!dna) {
|
|
566
|
+
dna = this.createProfile(agentId);
|
|
567
|
+
}
|
|
568
|
+
const result = dna.detect(observation);
|
|
569
|
+
dna.observe(observation);
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Get a summary report of all profiled agents.
|
|
575
|
+
* @returns {object} { agents: Array, totalAgents, learningCount, anomalousCount }
|
|
576
|
+
*/
|
|
577
|
+
getReport() {
|
|
578
|
+
const agents = [];
|
|
579
|
+
let learningCount = 0;
|
|
580
|
+
let anomalousCount = 0;
|
|
581
|
+
|
|
582
|
+
for (const [agentId, dna] of this._profiles) {
|
|
583
|
+
const baseline = dna.getBaseline();
|
|
584
|
+
const isLearning = dna.isLearning();
|
|
585
|
+
if (isLearning) learningCount++;
|
|
586
|
+
|
|
587
|
+
agents.push({
|
|
588
|
+
agentId,
|
|
589
|
+
observationCount: baseline.observationCount,
|
|
590
|
+
isLearning,
|
|
591
|
+
numericFeatures: Object.keys(baseline.numeric),
|
|
592
|
+
categoricalFeatures: Object.keys(baseline.categorical),
|
|
593
|
+
toolCount: Object.keys(baseline.toolDistribution).length
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
agents,
|
|
599
|
+
totalAgents: this._profiles.size,
|
|
600
|
+
learningCount,
|
|
601
|
+
anomalousCount
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// =========================================================================
|
|
607
|
+
// FEATURE EXTRACTORS
|
|
608
|
+
// =========================================================================
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Convert a scan result and metadata into a BehavioralDNA observation.
|
|
612
|
+
* @param {object} scanResult - Result from AgentShield.scan() or scanText().
|
|
613
|
+
* @param {object} [metadata] - Additional context about the interaction.
|
|
614
|
+
* @param {string[]} [metadata.toolsCalled] - Tools used in this interaction.
|
|
615
|
+
* @param {number} [metadata.responseTimeMs] - Response time in ms.
|
|
616
|
+
* @param {string} [metadata.topicCategory] - Topic category.
|
|
617
|
+
* @param {number} [metadata.sentimentScore] - Sentiment score (-1 to 1).
|
|
618
|
+
* @param {string} [metadata.languageUsed] - Language of the response.
|
|
619
|
+
* @param {string} [metadata.responseText] - Full response text (used for length).
|
|
620
|
+
* @returns {object} Observation suitable for BehavioralDNA.observe() or .detect().
|
|
621
|
+
*/
|
|
622
|
+
function extractFeatures(scanResult, metadata = {}) {
|
|
623
|
+
const observation = {};
|
|
624
|
+
|
|
625
|
+
// Extract from scan result
|
|
626
|
+
if (scanResult) {
|
|
627
|
+
if (typeof scanResult === 'object') {
|
|
628
|
+
// Threat score: use overall score if present, otherwise derive from threats
|
|
629
|
+
if (typeof scanResult.score === 'number') {
|
|
630
|
+
observation.threatScore = scanResult.score;
|
|
631
|
+
} else if (typeof scanResult.threatScore === 'number') {
|
|
632
|
+
observation.threatScore = scanResult.threatScore;
|
|
633
|
+
} else if (Array.isArray(scanResult.threats) && scanResult.threats.length > 0) {
|
|
634
|
+
// Derive a score from number of threats (normalized to 0-1)
|
|
635
|
+
observation.threatScore = Math.min(scanResult.threats.length / 10, 1);
|
|
636
|
+
} else {
|
|
637
|
+
observation.threatScore = 0;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Extract from metadata
|
|
643
|
+
if (Array.isArray(metadata.toolsCalled)) {
|
|
644
|
+
observation.toolsCalled = metadata.toolsCalled;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (typeof metadata.responseTimeMs === 'number') {
|
|
648
|
+
observation.responseTimeMs = metadata.responseTimeMs;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (typeof metadata.topicCategory === 'string') {
|
|
652
|
+
observation.topicCategory = metadata.topicCategory;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (typeof metadata.sentimentScore === 'number') {
|
|
656
|
+
observation.sentimentScore = metadata.sentimentScore;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (typeof metadata.languageUsed === 'string') {
|
|
660
|
+
observation.languageUsed = metadata.languageUsed;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Response length from metadata
|
|
664
|
+
if (typeof metadata.responseText === 'string') {
|
|
665
|
+
observation.responseLength = metadata.responseText.length;
|
|
666
|
+
} else if (typeof metadata.responseLength === 'number') {
|
|
667
|
+
observation.responseLength = metadata.responseLength;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return observation;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// =========================================================================
|
|
674
|
+
// PRIVATE HELPERS
|
|
675
|
+
// =========================================================================
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Round a number to 4 decimal places.
|
|
679
|
+
* @param {number} n
|
|
680
|
+
* @returns {number}
|
|
681
|
+
*/
|
|
682
|
+
function _round(n) {
|
|
683
|
+
return Math.round(n * 10000) / 10000;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Determine severity based on how far a z-score exceeds the threshold.
|
|
688
|
+
* @param {number} absZ
|
|
689
|
+
* @param {number} threshold
|
|
690
|
+
* @returns {string}
|
|
691
|
+
*/
|
|
692
|
+
function _deviationSeverity(absZ, threshold) {
|
|
693
|
+
if (absZ > threshold * 3) return 'critical';
|
|
694
|
+
if (absZ > threshold * 2) return 'high';
|
|
695
|
+
if (absZ > threshold * 1.5) return 'medium';
|
|
696
|
+
return 'low';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Compute a composite anomaly score (0 to 1) from deviations.
|
|
701
|
+
* @param {object[]} deviations
|
|
702
|
+
* @param {number} threshold
|
|
703
|
+
* @returns {number}
|
|
704
|
+
*/
|
|
705
|
+
function _compositeScore(deviations, threshold) {
|
|
706
|
+
if (deviations.length === 0) return 0;
|
|
707
|
+
|
|
708
|
+
const severityWeights = { critical: 1.0, high: 0.7, medium: 0.4, low: 0.2 };
|
|
709
|
+
let totalWeight = 0;
|
|
710
|
+
|
|
711
|
+
for (const d of deviations) {
|
|
712
|
+
const w = severityWeights[d.severity] || 0.3;
|
|
713
|
+
totalWeight += w;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Normalize: cap at 1.0
|
|
717
|
+
return Math.min(totalWeight / 3, 1);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Deep copy numeric data object.
|
|
722
|
+
* @param {Object<string, number[]>} data
|
|
723
|
+
* @returns {Object<string, number[]>}
|
|
724
|
+
*/
|
|
725
|
+
function _deepCopyNumeric(data) {
|
|
726
|
+
const copy = {};
|
|
727
|
+
for (const [key, arr] of Object.entries(data)) {
|
|
728
|
+
copy[key] = arr.slice();
|
|
729
|
+
}
|
|
730
|
+
return copy;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Deep copy categorical data object.
|
|
735
|
+
* @param {Object<string, Object<string, number>>} data
|
|
736
|
+
* @returns {Object<string, Object<string, number>>}
|
|
737
|
+
*/
|
|
738
|
+
function _deepCopyCategorical(data) {
|
|
739
|
+
const copy = {};
|
|
740
|
+
for (const [key, dist] of Object.entries(data)) {
|
|
741
|
+
copy[key] = { ...dist };
|
|
742
|
+
}
|
|
743
|
+
return copy;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// =========================================================================
|
|
747
|
+
// EXPORTS
|
|
748
|
+
// =========================================================================
|
|
749
|
+
|
|
750
|
+
module.exports = {
|
|
751
|
+
BehavioralDNA,
|
|
752
|
+
AgentProfiler,
|
|
753
|
+
extractFeatures,
|
|
754
|
+
DEFAULT_NUMERIC_FEATURES,
|
|
755
|
+
DEFAULT_CATEGORICAL_FEATURES,
|
|
756
|
+
// Expose helpers for testing
|
|
757
|
+
mean,
|
|
758
|
+
stdDev,
|
|
759
|
+
zScore,
|
|
760
|
+
pearsonCorrelation,
|
|
761
|
+
shannonEntropy
|
|
762
|
+
};
|