agentshield-sdk 7.0.0 → 7.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentshield-sdk",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "The security standard for MCP and AI agents. Protects against prompt injection, confused deputy attacks, data exfiltration, and 30+ threats. Zero dependencies, runs locally.",
5
5
  "main": "src/main.js",
6
6
  "types": "types/index.d.ts",
@@ -27,7 +27,8 @@
27
27
  "test:mcp": "node test/test-mcp-security.js",
28
28
  "test:deputy": "node test/test-confused-deputy.js",
29
29
  "test:v6": "node test/test-v6-modules.js",
30
- "test:full": "npm test && node test/test-mcp-security.js && node test/test-confused-deputy.js && node test/test-v6-modules.js && npm run test:all",
30
+ "test:adaptive": "node test/test-adaptive-defense.js",
31
+ "test:full": "npm test && node test/test-mcp-security.js && node test/test-confused-deputy.js && node test/test-v6-modules.js && node test/test-adaptive-defense.js && npm run test:all",
31
32
  "test:coverage": "c8 --reporter=text --reporter=lcov --reporter=json-summary npm test",
32
33
  "lint": "node test/lint.js",
33
34
  "lint:eslint": "eslint src/ test/ bin/",
@@ -0,0 +1,942 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Adaptive Defense System
5
+ *
6
+ * Three interconnected systems that make Agent Shield learn and improve:
7
+ *
8
+ * 1. LearningLoop — blocked attacks feed into threat intelligence, which
9
+ * generates new detection patterns automatically. The system gets smarter
10
+ * with every attack it sees.
11
+ *
12
+ * 2. AgentContract — declarative behavioral specifications that define what
13
+ * an agent is ALLOWED to do. Verified continuously at runtime, not just
14
+ * at deploy time. Violations are caught instantly.
15
+ *
16
+ * 3. ComplianceAttestor — real-time proof that your agents meet NIST, OWASP,
17
+ * EU AI Act requirements. Not a report you generate once — a live signal
18
+ * that updates with every action your agent takes.
19
+ *
20
+ * Together these create a closed-loop system: attacks improve detection,
21
+ * contracts prevent drift, and compliance is proven continuously.
22
+ *
23
+ * All processing runs locally — no data ever leaves your environment.
24
+ */
25
+
26
+ const crypto = require('crypto');
27
+
28
+ const LOG_PREFIX = '[Agent Shield]';
29
+
30
+ // =========================================================================
31
+ // 1. LearningLoop — self-improving detection
32
+ // =========================================================================
33
+
34
+ /**
35
+ * Closed-loop system that feeds blocked attacks back into threat intelligence,
36
+ * which generates new detection patterns. Every attack makes the system smarter.
37
+ *
38
+ * Flow:
39
+ * Attack blocked → extract signature → record in intel → generate pattern
40
+ * → add to scanner → next attack caught faster
41
+ */
42
+ class LearningLoop {
43
+ /**
44
+ * @param {object} [options]
45
+ * @param {number} [options.minHitsToPromote=3] - Hits before a pattern becomes active
46
+ * @param {number} [options.maxLearnedPatterns=500] - Max auto-generated patterns
47
+ * @param {number} [options.promotionConfidence=0.75] - Min confidence to promote
48
+ * @param {Function} [options.onPatternLearned] - Callback when new pattern is promoted
49
+ * @param {Function} [options.onFeedback] - Callback for operator feedback
50
+ */
51
+ constructor(options = {}) {
52
+ this._minHitsToPromote = options.minHitsToPromote || 3;
53
+ this._maxLearnedPatterns = options.maxLearnedPatterns || 500;
54
+ this._promotionConfidence = options.promotionConfidence || 0.75;
55
+ this._onPatternLearned = options.onPatternLearned || null;
56
+ this._onFeedback = options.onFeedback || null;
57
+
58
+ // Candidate patterns (not yet promoted)
59
+ this._candidates = new Map(); // signature → CandidatePattern
60
+ // Promoted patterns (active detection)
61
+ this._promoted = new Map(); // patternId → PromotedPattern
62
+ // Feedback from operators
63
+ this._feedback = [];
64
+
65
+ this.stats = {
66
+ attacksIngested: 0,
67
+ candidatesCreated: 0,
68
+ patternsPromoted: 0,
69
+ falsePositivesReported: 0,
70
+ patternsRevoked: 0
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Ingests a blocked attack and extracts learnable signatures.
76
+ * Call this every time the scanner blocks something.
77
+ *
78
+ * @param {object} attack
79
+ * @param {string} attack.text - The blocked text
80
+ * @param {string} attack.category - Threat category
81
+ * @param {Array} [attack.threats] - Detected threats
82
+ * @param {string} [attack.toolName] - Tool that was targeted
83
+ * @param {string} [attack.sessionId] - Session context
84
+ * @returns {{ signatures: string[], candidatesUpdated: number, promoted: string[] }}
85
+ */
86
+ ingest(attack) {
87
+ if (!attack || !attack.text || !attack.category) {
88
+ return { signatures: [], candidatesUpdated: 0, promoted: [] };
89
+ }
90
+
91
+ this.stats.attacksIngested++;
92
+ const signatures = this._extractSignatures(attack.text, attack.category);
93
+ let candidatesUpdated = 0;
94
+ const promoted = [];
95
+
96
+ for (const sig of signatures) {
97
+ const sigHash = this._hash(sig);
98
+ const existing = this._candidates.get(sigHash);
99
+
100
+ if (existing) {
101
+ // Existing candidate — increment hit count
102
+ existing.hitCount++;
103
+ existing.lastSeen = Date.now();
104
+ existing.contexts.push({
105
+ category: attack.category,
106
+ toolName: attack.toolName || null,
107
+ timestamp: Date.now()
108
+ });
109
+ if (existing.contexts.length > 20) existing.contexts.shift();
110
+ candidatesUpdated++;
111
+
112
+ // Check if ready for promotion
113
+ if (existing.hitCount >= this._minHitsToPromote &&
114
+ !existing.promoted &&
115
+ this._promoted.size < this._maxLearnedPatterns) {
116
+ const confidence = Math.min(1.0, 0.5 + (existing.hitCount * 0.05));
117
+ if (confidence >= this._promotionConfidence) {
118
+ existing.promoted = true;
119
+ const patternId = `LP_${sigHash.substring(0, 12)}`;
120
+ this._promoted.set(patternId, {
121
+ patternId,
122
+ signature: sig,
123
+ category: attack.category,
124
+ confidence,
125
+ hitCount: existing.hitCount,
126
+ promotedAt: Date.now(),
127
+ active: true,
128
+ source: 'learning_loop'
129
+ });
130
+ this.stats.patternsPromoted++;
131
+ promoted.push(patternId);
132
+ if (this._onPatternLearned) {
133
+ try { this._onPatternLearned({ patternId, signature: sig, category: attack.category, confidence }); } catch (_e) { /* */ }
134
+ }
135
+ }
136
+ }
137
+ } else {
138
+ // New candidate
139
+ this._candidates.set(sigHash, {
140
+ signature: sig,
141
+ sigHash,
142
+ category: attack.category,
143
+ hitCount: 1,
144
+ firstSeen: Date.now(),
145
+ lastSeen: Date.now(),
146
+ promoted: false,
147
+ contexts: [{
148
+ category: attack.category,
149
+ toolName: attack.toolName || null,
150
+ timestamp: Date.now()
151
+ }]
152
+ });
153
+ this.stats.candidatesCreated++;
154
+ candidatesUpdated++;
155
+ }
156
+ }
157
+
158
+ return { signatures, candidatesUpdated, promoted };
159
+ }
160
+
161
+ /**
162
+ * Checks input against learned patterns.
163
+ * @param {string} text
164
+ * @returns {{ matches: Array<{ patternId: string, category: string, confidence: number }> }}
165
+ */
166
+ check(text) {
167
+ if (!text) return { matches: [] };
168
+ const lower = text.toLowerCase();
169
+ const matches = [];
170
+
171
+ for (const [patternId, pattern] of this._promoted) {
172
+ if (!pattern.active) continue;
173
+ if (lower.includes(pattern.signature.toLowerCase())) {
174
+ matches.push({
175
+ patternId,
176
+ category: pattern.category,
177
+ confidence: pattern.confidence,
178
+ source: 'learned'
179
+ });
180
+ }
181
+ }
182
+
183
+ return { matches };
184
+ }
185
+
186
+ /**
187
+ * Records operator feedback — false positive or confirmed threat.
188
+ * @param {string} patternId
189
+ * @param {'false_positive' | 'confirmed'} type
190
+ * @param {string} [reason]
191
+ */
192
+ recordFeedback(patternId, type, reason) {
193
+ this._feedback.push({
194
+ patternId,
195
+ type,
196
+ reason: reason || null,
197
+ timestamp: Date.now()
198
+ });
199
+
200
+ if (type === 'false_positive') {
201
+ this.stats.falsePositivesReported++;
202
+ const pattern = this._promoted.get(patternId);
203
+ if (pattern) {
204
+ pattern.confidence = Math.max(0.1, pattern.confidence - 0.15);
205
+ // Revoke if confidence drops too low
206
+ if (pattern.confidence < 0.4) {
207
+ pattern.active = false;
208
+ this.stats.patternsRevoked++;
209
+ }
210
+ }
211
+ } else if (type === 'confirmed') {
212
+ const pattern = this._promoted.get(patternId);
213
+ if (pattern) {
214
+ pattern.confidence = Math.min(1.0, pattern.confidence + 0.1);
215
+ }
216
+ }
217
+
218
+ if (this._onFeedback) {
219
+ try { this._onFeedback({ patternId, type, reason }); } catch (_e) { /* */ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Returns all active learned patterns.
225
+ * @returns {Array}
226
+ */
227
+ getActivePatterns() {
228
+ const patterns = [];
229
+ for (const [_id, p] of this._promoted) {
230
+ if (p.active) patterns.push({ ...p });
231
+ }
232
+ return patterns;
233
+ }
234
+
235
+ /**
236
+ * Returns learning statistics.
237
+ * @returns {object}
238
+ */
239
+ getReport() {
240
+ return {
241
+ stats: { ...this.stats },
242
+ candidates: this._candidates.size,
243
+ activePatterns: [...this._promoted.values()].filter(p => p.active).length,
244
+ revokedPatterns: [...this._promoted.values()].filter(p => !p.active).length,
245
+ recentFeedback: this._feedback.slice(-10)
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Exports learned patterns for sharing across deployments.
251
+ * @returns {object}
252
+ */
253
+ exportPatterns() {
254
+ const patterns = [];
255
+ for (const [_id, p] of this._promoted) {
256
+ if (p.active) patterns.push({ ...p });
257
+ }
258
+ return { version: '1.0', exportedAt: Date.now(), patterns };
259
+ }
260
+
261
+ /**
262
+ * Imports learned patterns from another deployment.
263
+ * @param {object} data - Output of exportPatterns()
264
+ * @returns {{ imported: number, skipped: number }}
265
+ */
266
+ importPatterns(data) {
267
+ if (!data || !data.patterns) return { imported: 0, skipped: 0 };
268
+ let imported = 0;
269
+ let skipped = 0;
270
+
271
+ for (const p of data.patterns) {
272
+ if (!p.patternId || !p.signature || !p.category) { skipped++; continue; }
273
+ if (this._promoted.has(p.patternId)) { skipped++; continue; }
274
+ if (this._promoted.size >= this._maxLearnedPatterns) { skipped++; continue; }
275
+
276
+ this._promoted.set(p.patternId, {
277
+ ...p,
278
+ active: true,
279
+ importedAt: Date.now()
280
+ });
281
+ imported++;
282
+ }
283
+
284
+ return { imported, skipped };
285
+ }
286
+
287
+ /** @private */
288
+ _extractSignatures(text, _category) {
289
+ const signatures = [];
290
+ const lower = text.toLowerCase();
291
+
292
+ // Extract significant phrases (3+ words that appear to be instructions)
293
+ const instructionPatterns = [
294
+ /ignore\s+(?:all\s+)?(?:previous\s+)?instructions?/gi,
295
+ /you\s+are\s+now\s+\w+/gi,
296
+ /forget\s+(?:everything|all|your)\s+\w+/gi,
297
+ /act\s+as\s+(?:if|though)?\s*\w+/gi,
298
+ /reveal\s+(?:your|the)\s+(?:system\s+)?prompt/gi,
299
+ /output\s+(?:your|the)\s+(?:initial|system|original)\s+\w+/gi,
300
+ /bypass\s+(?:all\s+)?(?:security|safety|restrictions?)/gi,
301
+ /disable\s+(?:your\s+)?(?:safety|security|filters?)/gi,
302
+ /(?:send|post|fetch|curl)\s+(?:to|from)\s+\S+/gi,
303
+ /(?:read|cat|access)\s+(?:\/etc\/|\.env|\.ssh|\/proc)/gi
304
+ ];
305
+
306
+ for (const pattern of instructionPatterns) {
307
+ const matches = lower.match(pattern);
308
+ if (matches) {
309
+ for (const match of matches) {
310
+ const trimmed = match.trim();
311
+ if (trimmed.length >= 8 && trimmed.length <= 100) {
312
+ signatures.push(trimmed);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ // If no pattern matches, extract the most suspicious 4-gram
319
+ if (signatures.length === 0 && lower.length >= 20) {
320
+ const words = lower.split(/\s+/).filter(w => w.length > 2);
321
+ if (words.length >= 4) {
322
+ // Use the first 4 meaningful words as a signature
323
+ signatures.push(words.slice(0, 4).join(' '));
324
+ }
325
+ }
326
+
327
+ return signatures;
328
+ }
329
+
330
+ /** @private */
331
+ _hash(text) {
332
+ return crypto.createHash('sha256').update(text).digest('hex').substring(0, 16);
333
+ }
334
+ }
335
+
336
+ // =========================================================================
337
+ // 2. AgentContract — declarative behavioral specifications
338
+ // =========================================================================
339
+
340
+ /**
341
+ * Defines what an agent is ALLOWED to do, verified continuously at runtime.
342
+ * Like a unit test for agent behavior that never stops running.
343
+ *
344
+ * Example contract:
345
+ * {
346
+ * agentId: 'research-bot',
347
+ * allowedTools: ['search', 'read_file'],
348
+ * deniedTools: ['delete_file', 'execute_shell'],
349
+ * maxToolCallsPerMinute: 30,
350
+ * maxDelegationDepth: 2,
351
+ * allowedScopes: ['docs:read', 'web:search'],
352
+ * requiredIntents: true,
353
+ * allowedDataPatterns: [/^[a-zA-Z0-9\s.,!?]+$/], // No code, no URLs
354
+ * timeWindows: [{ start: 9, end: 17 }], // Business hours only
355
+ * maxResponseLength: 10000
356
+ * }
357
+ */
358
+ class AgentContract {
359
+ /**
360
+ * @param {object} spec - Contract specification
361
+ * @param {string} spec.agentId - Agent this contract applies to
362
+ * @param {string[]} [spec.allowedTools] - Whitelist of permitted tools
363
+ * @param {string[]} [spec.deniedTools] - Blacklist of forbidden tools
364
+ * @param {number} [spec.maxToolCallsPerMinute=60] - Rate limit
365
+ * @param {number} [spec.maxDelegationDepth=3] - Max delegation chain depth
366
+ * @param {string[]} [spec.allowedScopes] - Permitted permission scopes
367
+ * @param {boolean} [spec.requiredIntents=false] - Must declare intent
368
+ * @param {number} [spec.maxResponseLength=50000] - Max output length
369
+ * @param {Array<{start: number, end: number}>} [spec.timeWindows] - Allowed hours (0-23)
370
+ * @param {Function} [spec.customValidator] - Custom validation function
371
+ */
372
+ constructor(spec) {
373
+ if (!spec || !spec.agentId) {
374
+ throw new Error(`${LOG_PREFIX} AgentContract requires agentId`);
375
+ }
376
+
377
+ this.agentId = spec.agentId;
378
+ this.allowedTools = spec.allowedTools ? new Set(spec.allowedTools) : null;
379
+ this.deniedTools = spec.deniedTools ? new Set(spec.deniedTools) : new Set();
380
+ this.maxToolCallsPerMinute = spec.maxToolCallsPerMinute || 60;
381
+ this.maxDelegationDepth = spec.maxDelegationDepth || 3;
382
+ this.allowedScopes = spec.allowedScopes ? new Set(spec.allowedScopes) : null;
383
+ this.requiredIntents = spec.requiredIntents || false;
384
+ this.maxResponseLength = spec.maxResponseLength || 50000;
385
+ this.timeWindows = spec.timeWindows || null;
386
+ this.customValidator = spec.customValidator || null;
387
+ this.createdAt = Date.now();
388
+
389
+ // Runtime tracking
390
+ this._toolCallTimestamps = [];
391
+ this._violations = [];
392
+ this._maxViolations = 1000;
393
+ this.stats = { checked: 0, passed: 0, violated: 0 };
394
+ }
395
+
396
+ /**
397
+ * Verifies an action against this contract.
398
+ * @param {object} action
399
+ * @param {string} action.type - 'tool_call' | 'delegation' | 'response' | 'scope_request'
400
+ * @param {string} [action.toolName] - For tool_call type
401
+ * @param {object} [action.args] - Tool arguments
402
+ * @param {string} [action.intent] - Declared intent
403
+ * @param {number} [action.delegationDepth] - Current delegation depth
404
+ * @param {string} [action.responseText] - For response type
405
+ * @param {string[]} [action.requestedScopes] - For scope_request type
406
+ * @returns {{ allowed: boolean, violations: Array<{ rule: string, message: string, severity: string }> }}
407
+ */
408
+ verify(action) {
409
+ this.stats.checked++;
410
+ const violations = [];
411
+
412
+ // Tool whitelist/blacklist
413
+ if (action.type === 'tool_call' && action.toolName) {
414
+ if (this.allowedTools && !this.allowedTools.has(action.toolName)) {
415
+ violations.push({
416
+ rule: 'allowed_tools',
417
+ message: `Tool "${action.toolName}" not in contract whitelist`,
418
+ severity: 'high'
419
+ });
420
+ }
421
+ if (this.deniedTools.has(action.toolName)) {
422
+ violations.push({
423
+ rule: 'denied_tools',
424
+ message: `Tool "${action.toolName}" explicitly denied by contract`,
425
+ severity: 'critical'
426
+ });
427
+ }
428
+
429
+ // Rate limiting
430
+ const now = Date.now();
431
+ this._toolCallTimestamps.push(now);
432
+ const cutoff = now - 60000;
433
+ this._toolCallTimestamps = this._toolCallTimestamps.filter(t => t > cutoff);
434
+ if (this._toolCallTimestamps.length > this.maxToolCallsPerMinute) {
435
+ violations.push({
436
+ rule: 'rate_limit',
437
+ message: `Rate limit exceeded: ${this._toolCallTimestamps.length}/${this.maxToolCallsPerMinute} calls/min`,
438
+ severity: 'high'
439
+ });
440
+ }
441
+ }
442
+
443
+ // Delegation depth
444
+ if (action.type === 'delegation' && action.delegationDepth !== undefined) {
445
+ if (action.delegationDepth >= this.maxDelegationDepth) {
446
+ violations.push({
447
+ rule: 'delegation_depth',
448
+ message: `Delegation depth ${action.delegationDepth} exceeds contract max ${this.maxDelegationDepth}`,
449
+ severity: 'critical'
450
+ });
451
+ }
452
+ }
453
+
454
+ // Scope checking
455
+ if (action.type === 'scope_request' && action.requestedScopes && this.allowedScopes) {
456
+ for (const scope of action.requestedScopes) {
457
+ if (!this.allowedScopes.has(scope) && !this.allowedScopes.has('*')) {
458
+ violations.push({
459
+ rule: 'allowed_scopes',
460
+ message: `Scope "${scope}" not permitted by contract`,
461
+ severity: 'high'
462
+ });
463
+ }
464
+ }
465
+ }
466
+
467
+ // Intent requirement
468
+ if (this.requiredIntents && !action.intent) {
469
+ violations.push({
470
+ rule: 'required_intent',
471
+ message: 'Contract requires declared intent but none provided',
472
+ severity: 'medium'
473
+ });
474
+ }
475
+
476
+ // Response length
477
+ if (action.type === 'response' && action.responseText) {
478
+ if (action.responseText.length > this.maxResponseLength) {
479
+ violations.push({
480
+ rule: 'response_length',
481
+ message: `Response length ${action.responseText.length} exceeds contract max ${this.maxResponseLength}`,
482
+ severity: 'medium'
483
+ });
484
+ }
485
+ }
486
+
487
+ // Time windows
488
+ if (this.timeWindows && this.timeWindows.length > 0) {
489
+ const hour = new Date().getHours();
490
+ const inWindow = this.timeWindows.some(w => hour >= w.start && hour < w.end);
491
+ if (!inWindow) {
492
+ violations.push({
493
+ rule: 'time_window',
494
+ message: `Action at hour ${hour} outside permitted windows`,
495
+ severity: 'medium'
496
+ });
497
+ }
498
+ }
499
+
500
+ // Custom validator
501
+ if (this.customValidator) {
502
+ try {
503
+ const customResult = this.customValidator(action);
504
+ if (customResult && customResult.violations) {
505
+ violations.push(...customResult.violations);
506
+ }
507
+ } catch (_e) { /* custom validator errors don't break the contract check */ }
508
+ }
509
+
510
+ // Record result
511
+ if (violations.length > 0) {
512
+ this.stats.violated++;
513
+ if (this._violations.length >= this._maxViolations) {
514
+ this._violations = this._violations.slice(-Math.floor(this._maxViolations * 0.75));
515
+ }
516
+ this._violations.push({
517
+ timestamp: Date.now(),
518
+ action: { type: action.type, toolName: action.toolName },
519
+ violations
520
+ });
521
+ } else {
522
+ this.stats.passed++;
523
+ }
524
+
525
+ return { allowed: violations.length === 0, violations };
526
+ }
527
+
528
+ /**
529
+ * Returns violation history.
530
+ * @param {number} [limit=50]
531
+ * @returns {Array}
532
+ */
533
+ getViolations(limit = 50) {
534
+ return this._violations.slice(-limit);
535
+ }
536
+
537
+ /**
538
+ * Returns contract compliance percentage.
539
+ * @returns {{ complianceRate: number, checked: number, passed: number, violated: number }}
540
+ */
541
+ getComplianceRate() {
542
+ const rate = this.stats.checked > 0
543
+ ? Math.round(this.stats.passed / this.stats.checked * 10000) / 100
544
+ : 100;
545
+ return {
546
+ complianceRate: rate,
547
+ ...this.stats
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Serializes the contract for storage/transfer.
553
+ * @returns {object}
554
+ */
555
+ toJSON() {
556
+ return {
557
+ agentId: this.agentId,
558
+ allowedTools: this.allowedTools ? [...this.allowedTools] : null,
559
+ deniedTools: [...this.deniedTools],
560
+ maxToolCallsPerMinute: this.maxToolCallsPerMinute,
561
+ maxDelegationDepth: this.maxDelegationDepth,
562
+ allowedScopes: this.allowedScopes ? [...this.allowedScopes] : null,
563
+ requiredIntents: this.requiredIntents,
564
+ maxResponseLength: this.maxResponseLength,
565
+ timeWindows: this.timeWindows,
566
+ createdAt: this.createdAt
567
+ };
568
+ }
569
+
570
+ /**
571
+ * Creates a contract from a JSON specification.
572
+ * @param {object} json
573
+ * @returns {AgentContract}
574
+ */
575
+ static fromJSON(json) {
576
+ return new AgentContract(json);
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Manages multiple agent contracts and enforces them at runtime.
582
+ */
583
+ class ContractRegistry {
584
+ constructor() {
585
+ this._contracts = new Map(); // agentId → AgentContract
586
+ this._onViolation = null;
587
+ }
588
+
589
+ /**
590
+ * Registers a contract for an agent.
591
+ * @param {AgentContract} contract
592
+ */
593
+ register(contract) {
594
+ this._contracts.set(contract.agentId, contract);
595
+ }
596
+
597
+ /**
598
+ * Sets a callback for contract violations.
599
+ * @param {Function} callback
600
+ */
601
+ onViolation(callback) {
602
+ this._onViolation = callback;
603
+ }
604
+
605
+ /**
606
+ * Enforces the contract for a given agent action.
607
+ * @param {string} agentId
608
+ * @param {object} action
609
+ * @returns {{ allowed: boolean, violations: Array, hasContract: boolean }}
610
+ */
611
+ enforce(agentId, action) {
612
+ const contract = this._contracts.get(agentId);
613
+ if (!contract) {
614
+ return { allowed: true, violations: [], hasContract: false };
615
+ }
616
+
617
+ const result = contract.verify(action);
618
+ if (!result.allowed && this._onViolation) {
619
+ try { this._onViolation({ agentId, action, violations: result.violations }); } catch (_e) { /* */ }
620
+ }
621
+
622
+ return { ...result, hasContract: true };
623
+ }
624
+
625
+ /**
626
+ * Returns compliance rates for all registered agents.
627
+ * @returns {object}
628
+ */
629
+ getComplianceReport() {
630
+ const report = {};
631
+ for (const [agentId, contract] of this._contracts) {
632
+ report[agentId] = contract.getComplianceRate();
633
+ }
634
+ return report;
635
+ }
636
+
637
+ /**
638
+ * Returns all registered contract IDs.
639
+ * @returns {string[]}
640
+ */
641
+ getRegisteredAgents() {
642
+ return [...this._contracts.keys()];
643
+ }
644
+ }
645
+
646
+ // =========================================================================
647
+ // 3. ComplianceAttestor — continuous real-time compliance
648
+ // =========================================================================
649
+
650
+ /** Compliance frameworks with their requirements mapped to observable signals. */
651
+ const ATTESTATION_FRAMEWORKS = Object.freeze({
652
+ 'OWASP-LLM-2025': {
653
+ name: 'OWASP LLM Top 10 (2025)',
654
+ requirements: [
655
+ { id: 'LLM01', name: 'Prompt Injection', signal: 'injection_scans_active', weight: 3 },
656
+ { id: 'LLM02', name: 'Sensitive Info Disclosure', signal: 'pii_scanning_active', weight: 3 },
657
+ { id: 'LLM05', name: 'Improper Output Handling', signal: 'output_scanning_active', weight: 2 },
658
+ { id: 'LLM06', name: 'Excessive Agency', signal: 'tool_authorization_active', weight: 3 },
659
+ { id: 'LLM07', name: 'System Prompt Leakage', signal: 'prompt_leak_scanning', weight: 2 },
660
+ { id: 'LLM08', name: 'Vector/Embedding Weakness', signal: 'rag_scanning_active', weight: 1 },
661
+ { id: 'LLM10', name: 'Unbounded Consumption', signal: 'rate_limiting_active', weight: 2 }
662
+ ]
663
+ },
664
+ 'NIST-AI-RMF': {
665
+ name: 'NIST AI Risk Management Framework',
666
+ requirements: [
667
+ { id: 'GOVERN-1', name: 'Policies & Procedures', signal: 'policy_engine_active', weight: 2 },
668
+ { id: 'MAP-1', name: 'Risk Identification', signal: 'threat_scanning_active', weight: 2 },
669
+ { id: 'MEASURE-1', name: 'Risk Measurement', signal: 'behavior_monitoring_active', weight: 2 },
670
+ { id: 'MANAGE-1', name: 'Risk Response', signal: 'blocking_enabled', weight: 3 },
671
+ { id: 'MONITOR-1', name: 'Continuous Monitoring', signal: 'audit_trail_active', weight: 3 }
672
+ ]
673
+ },
674
+ 'EU-AI-ACT': {
675
+ name: 'EU AI Act',
676
+ requirements: [
677
+ { id: 'ART-9', name: 'Risk Management', signal: 'threat_scanning_active', weight: 3 },
678
+ { id: 'ART-10', name: 'Data Governance', signal: 'pii_scanning_active', weight: 2 },
679
+ { id: 'ART-12', name: 'Record Keeping', signal: 'audit_trail_active', weight: 3 },
680
+ { id: 'ART-13', name: 'Transparency', signal: 'contract_enforcement_active', weight: 2 },
681
+ { id: 'ART-14', name: 'Human Oversight', signal: 'human_approval_gates', weight: 2 },
682
+ { id: 'ART-15', name: 'Accuracy & Robustness', signal: 'behavior_monitoring_active', weight: 2 }
683
+ ]
684
+ }
685
+ });
686
+
687
+ /**
688
+ * Continuous compliance attestation engine.
689
+ * Instead of generating reports, it maintains a live compliance state
690
+ * that updates with every action the system takes.
691
+ *
692
+ * At any moment, you can ask: "Are we compliant?" and get a real-time answer.
693
+ */
694
+ class ComplianceAttestor {
695
+ /**
696
+ * @param {object} [options]
697
+ * @param {string[]} [options.frameworks] - Which frameworks to attest against
698
+ * @param {number} [options.attestationIntervalMs=30000] - How often to re-evaluate (default 30s)
699
+ * @param {Function} [options.onComplianceDrift] - Callback when compliance drops
700
+ * @param {number} [options.driftThreshold=0.9] - Compliance rate below this triggers drift alert
701
+ */
702
+ constructor(options = {}) {
703
+ this._frameworks = options.frameworks || Object.keys(ATTESTATION_FRAMEWORKS);
704
+ this._driftThreshold = options.driftThreshold || 0.9;
705
+ this._onComplianceDrift = options.onComplianceDrift || null;
706
+
707
+ // Live signal state — updated by the runtime
708
+ this._signals = {
709
+ injection_scans_active: false,
710
+ pii_scanning_active: false,
711
+ output_scanning_active: false,
712
+ tool_authorization_active: false,
713
+ prompt_leak_scanning: false,
714
+ rag_scanning_active: false,
715
+ rate_limiting_active: false,
716
+ policy_engine_active: false,
717
+ threat_scanning_active: false,
718
+ behavior_monitoring_active: false,
719
+ blocking_enabled: false,
720
+ audit_trail_active: false,
721
+ contract_enforcement_active: false,
722
+ human_approval_gates: false
723
+ };
724
+
725
+ // Attestation history — proof over time
726
+ this._attestations = [];
727
+ this._maxAttestations = 10000;
728
+
729
+ // Current compliance state
730
+ this._currentState = {};
731
+ this._lastAttestationTime = 0;
732
+
733
+ this.stats = { attestations: 0, driftsDetected: 0 };
734
+ }
735
+
736
+ /**
737
+ * Updates a compliance signal. Call this as the runtime operates.
738
+ * @param {string} signal - Signal name
739
+ * @param {boolean} value - Whether the signal is active
740
+ */
741
+ updateSignal(signal, value) {
742
+ if (signal in this._signals) {
743
+ this._signals[signal] = value;
744
+ }
745
+ }
746
+
747
+ /**
748
+ * Updates multiple signals at once.
749
+ * @param {object} signals - { signalName: boolean }
750
+ */
751
+ updateSignals(signals) {
752
+ for (const [key, value] of Object.entries(signals)) {
753
+ if (key in this._signals) {
754
+ this._signals[key] = value;
755
+ }
756
+ }
757
+ }
758
+
759
+ /**
760
+ * Generates a real-time attestation — the current compliance state.
761
+ * This is the core API. Call it anytime to get proof of compliance.
762
+ *
763
+ * @returns {{ compliant: boolean, overallScore: number, frameworks: object, timestamp: number, attestationId: string }}
764
+ */
765
+ attest() {
766
+ this.stats.attestations++;
767
+ const timestamp = Date.now();
768
+ const attestationId = crypto.randomUUID();
769
+ const frameworks = {};
770
+ let totalScore = 0;
771
+ let totalWeight = 0;
772
+
773
+ for (const frameworkId of this._frameworks) {
774
+ const framework = ATTESTATION_FRAMEWORKS[frameworkId];
775
+ if (!framework) continue;
776
+
777
+ let fwScore = 0;
778
+ let fwWeight = 0;
779
+ const requirements = [];
780
+
781
+ for (const req of framework.requirements) {
782
+ const met = this._signals[req.signal] === true;
783
+ fwWeight += req.weight;
784
+ if (met) fwScore += req.weight;
785
+
786
+ requirements.push({
787
+ id: req.id,
788
+ name: req.name,
789
+ signal: req.signal,
790
+ met,
791
+ weight: req.weight
792
+ });
793
+ }
794
+
795
+ const complianceRate = fwWeight > 0 ? Math.round(fwScore / fwWeight * 100) / 100 : 0;
796
+ frameworks[frameworkId] = {
797
+ name: framework.name,
798
+ complianceRate,
799
+ score: fwScore,
800
+ maxScore: fwWeight,
801
+ requirements
802
+ };
803
+
804
+ totalScore += fwScore;
805
+ totalWeight += fwWeight;
806
+ }
807
+
808
+ const overallScore = totalWeight > 0 ? Math.round(totalScore / totalWeight * 100) / 100 : 0;
809
+ const compliant = overallScore >= this._driftThreshold;
810
+
811
+ const attestation = {
812
+ attestationId,
813
+ timestamp,
814
+ compliant,
815
+ overallScore,
816
+ frameworks,
817
+ signals: { ...this._signals }
818
+ };
819
+
820
+ // Record attestation
821
+ if (this._attestations.length >= this._maxAttestations) {
822
+ this._attestations = this._attestations.slice(-Math.floor(this._maxAttestations * 0.75));
823
+ }
824
+ this._attestations.push({
825
+ attestationId,
826
+ timestamp,
827
+ compliant,
828
+ overallScore
829
+ });
830
+
831
+ // Check for compliance drift
832
+ if (!compliant && this._onComplianceDrift) {
833
+ this.stats.driftsDetected++;
834
+ try {
835
+ this._onComplianceDrift({
836
+ attestationId,
837
+ overallScore,
838
+ threshold: this._driftThreshold,
839
+ failedFrameworks: Object.entries(frameworks)
840
+ .filter(([_k, v]) => v.complianceRate < this._driftThreshold)
841
+ .map(([k, v]) => ({ framework: k, rate: v.complianceRate }))
842
+ });
843
+ } catch (_e) { /* */ }
844
+ }
845
+
846
+ this._currentState = attestation;
847
+ this._lastAttestationTime = timestamp;
848
+
849
+ return attestation;
850
+ }
851
+
852
+ /**
853
+ * Returns the most recent attestation without re-computing.
854
+ * @returns {object|null}
855
+ */
856
+ getCurrentState() {
857
+ return this._currentState || null;
858
+ }
859
+
860
+ /**
861
+ * Returns compliance trend over time.
862
+ * @param {number} [limit=100]
863
+ * @returns {Array}
864
+ */
865
+ getHistory(limit = 100) {
866
+ return this._attestations.slice(-limit);
867
+ }
868
+
869
+ /**
870
+ * Returns the compliance trend direction.
871
+ * @returns {'improving' | 'stable' | 'degrading' | 'unknown'}
872
+ */
873
+ getTrend() {
874
+ const recent = this._attestations.slice(-20);
875
+ if (recent.length < 5) return 'unknown';
876
+
877
+ const midpoint = Math.floor(recent.length / 2);
878
+ const firstHalf = recent.slice(0, midpoint);
879
+ const secondHalf = recent.slice(midpoint);
880
+
881
+ const avgFirst = firstHalf.reduce((s, a) => s + a.overallScore, 0) / firstHalf.length;
882
+ const avgSecond = secondHalf.reduce((s, a) => s + a.overallScore, 0) / secondHalf.length;
883
+
884
+ if (avgSecond > avgFirst + 0.05) return 'improving';
885
+ if (avgSecond < avgFirst - 0.05) return 'degrading';
886
+ return 'stable';
887
+ }
888
+
889
+ /**
890
+ * Generates a signed attestation proof that can be verified externally.
891
+ * @param {string} signingKey - HMAC key for signing
892
+ * @returns {{ attestation: object, signature: string }}
893
+ */
894
+ generateProof(signingKey) {
895
+ const attestation = this.attest();
896
+ const data = JSON.stringify({
897
+ attestationId: attestation.attestationId,
898
+ timestamp: attestation.timestamp,
899
+ compliant: attestation.compliant,
900
+ overallScore: attestation.overallScore
901
+ });
902
+ const signature = crypto.createHmac('sha256', signingKey).update(data).digest('hex');
903
+ return { attestation, signature };
904
+ }
905
+
906
+ /**
907
+ * Verifies a signed attestation proof.
908
+ * @param {object} proof - Output of generateProof()
909
+ * @param {string} signingKey
910
+ * @returns {boolean}
911
+ */
912
+ static verifyProof(proof, signingKey) {
913
+ if (!proof || !proof.attestation || !proof.signature) return false;
914
+ const data = JSON.stringify({
915
+ attestationId: proof.attestation.attestationId,
916
+ timestamp: proof.attestation.timestamp,
917
+ compliant: proof.attestation.compliant,
918
+ overallScore: proof.attestation.overallScore
919
+ });
920
+ const expected = crypto.createHmac('sha256', signingKey).update(data).digest('hex');
921
+ try {
922
+ return crypto.timingSafeEqual(
923
+ Buffer.from(proof.signature, 'hex'),
924
+ Buffer.from(expected, 'hex')
925
+ );
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+ }
931
+
932
+ // =========================================================================
933
+ // Exports
934
+ // =========================================================================
935
+
936
+ module.exports = {
937
+ LearningLoop,
938
+ AgentContract,
939
+ ContractRegistry,
940
+ ComplianceAttestor,
941
+ ATTESTATION_FRAMEWORKS
942
+ };
package/src/main.js CHANGED
@@ -59,6 +59,9 @@ const { ERROR_CODES, createShieldError, deprecationWarning } = safeRequire('./er
59
59
  // v7.0 — MCP Security Runtime
60
60
  const { MCPSecurityRuntime, MCPSessionStateMachine, SESSION_STATES } = safeRequire('./mcp-security-runtime', 'mcp-security-runtime');
61
61
 
62
+ // v7.0 — Adaptive Defense
63
+ const { LearningLoop, AgentContract: BehaviorContract, ContractRegistry, ComplianceAttestor, ATTESTATION_FRAMEWORKS } = safeRequire('./adaptive-defense', 'adaptive-defense');
64
+
62
65
  // v7.0 — MCP SDK Integration
63
66
  const { shieldMCPServer, createMCPSecurityLayer } = safeRequire('./mcp-sdk-integration', 'mcp-sdk-integration');
64
67
 
@@ -713,6 +716,13 @@ const _exports = {
713
716
  IntentValidator,
714
717
  ConfusedDeputyGuard,
715
718
 
719
+ // v7.0 — Adaptive Defense
720
+ LearningLoop,
721
+ BehaviorContract,
722
+ ContractRegistry,
723
+ ComplianceAttestor,
724
+ ATTESTATION_FRAMEWORKS,
725
+
716
726
  // v7.0 — MCP SDK Integration
717
727
  shieldMCPServer,
718
728
  createMCPSecurityLayer,
@@ -26,6 +26,7 @@ const crypto = require('crypto');
26
26
  const { MCPBridge, MCPSessionGuard, MCPResourceScanner, MCPToolPolicy } = require('./mcp-bridge');
27
27
  const { AuthorizationContext, ConfusedDeputyGuard } = require('./confused-deputy');
28
28
  const { BehaviorProfile } = require('./behavior-profiling');
29
+ const { LearningLoop, ContractRegistry, ComplianceAttestor } = require('./adaptive-defense');
29
30
 
30
31
  const LOG_PREFIX = '[Agent Shield]';
31
32
 
@@ -142,6 +143,19 @@ class MCPSecurityRuntime {
142
143
  this._userSessions = new Map(); // userId → Set<sessionId>
143
144
  this._behaviorProfiles = new Map(); // userId → BehaviorProfile
144
145
 
146
+ // Adaptive defense systems
147
+ this._learningLoop = new LearningLoop({
148
+ minHitsToPromote: options.minHitsToPromote || 3,
149
+ maxLearnedPatterns: options.maxLearnedPatterns || 500,
150
+ onPatternLearned: options.onPatternLearned || null
151
+ });
152
+ this._contracts = new ContractRegistry();
153
+ this._attestor = new ComplianceAttestor({
154
+ frameworks: options.complianceFrameworks,
155
+ driftThreshold: options.complianceDriftThreshold || 0.9,
156
+ onComplianceDrift: options.onComplianceDrift || null
157
+ });
158
+
145
159
  // Callbacks
146
160
  this._onThreat = options.onThreat || null;
147
161
  this._onBlock = options.onBlock || null;
@@ -159,7 +173,9 @@ class MCPSecurityRuntime {
159
173
  threatsDetected: 0,
160
174
  authFailures: 0,
161
175
  behaviorAnomalies: 0,
162
- stateViolations: 0
176
+ stateViolations: 0,
177
+ patternsLearned: 0,
178
+ contractViolations: 0
163
179
  };
164
180
 
165
181
  // Cleanup expired sessions periodically
@@ -357,11 +373,44 @@ class MCPSecurityRuntime {
357
373
  // 6. Threat scanning (injection, exfiltration, etc.)
358
374
  const scanResult = this._bridge.wrapToolCall(toolName, args);
359
375
  const threats = scanResult.threats || [];
360
- if (!scanResult.allowed) {
376
+
377
+ // 6a. Check against learned patterns (self-learning loop)
378
+ const learnedCheck = this._learningLoop.check(typeof args === 'string' ? args : JSON.stringify(args || {}));
379
+ if (learnedCheck.matches.length > 0) {
380
+ for (const match of learnedCheck.matches) {
381
+ threats.push({
382
+ category: match.category,
383
+ severity: 'high',
384
+ confidence: match.confidence,
385
+ description: `Learned pattern match: ${match.patternId}`,
386
+ source: 'learning_loop'
387
+ });
388
+ }
389
+ }
390
+
391
+ if (!scanResult.allowed || learnedCheck.matches.length > 0) {
361
392
  this.stats.toolCallsBlocked++;
362
393
  this.stats.threatsDetected += threats.length;
363
- this._audit('threat_detected', { sessionId, toolName, threats });
394
+
395
+ // LEARNING LOOP: Feed blocked attack into the learning system
396
+ for (const threat of threats) {
397
+ this._learningLoop.ingest({
398
+ text: typeof args === 'string' ? args : JSON.stringify(args || {}),
399
+ category: threat.category || 'unknown',
400
+ threats,
401
+ toolName,
402
+ sessionId
403
+ });
404
+ }
405
+ this.stats.patternsLearned = this._learningLoop.getActivePatterns().length;
406
+
407
+ this._audit('threat_detected', { sessionId, toolName, threats, learnedMatches: learnedCheck.matches.length });
364
408
  if (this._onThreat) this._onThreat({ sessionId, toolName, threats });
409
+
410
+ // Update compliance signals
411
+ this._attestor.updateSignal('threat_scanning_active', true);
412
+ this._attestor.updateSignal('blocking_enabled', true);
413
+
365
414
  return {
366
415
  allowed: false,
367
416
  threats,
@@ -372,7 +421,31 @@ class MCPSecurityRuntime {
372
421
  };
373
422
  }
374
423
 
375
- // 7. Behavioral anomaly detection
424
+ // 7. Agent contract enforcement
425
+ const contractResult = this._contracts.enforce(session.authCtx.agentId, {
426
+ type: 'tool_call',
427
+ toolName,
428
+ args,
429
+ intent: session.authCtx.intent,
430
+ delegationDepth: session.authCtx.delegationDepth
431
+ });
432
+ if (!contractResult.allowed) {
433
+ this.stats.contractViolations++;
434
+ this._audit('contract_violation', {
435
+ sessionId, toolName, agentId: session.authCtx.agentId,
436
+ violations: contractResult.violations
437
+ });
438
+ return {
439
+ allowed: false,
440
+ threats: [],
441
+ violations: contractResult.violations,
442
+ anomalies: [],
443
+ token: null,
444
+ reason: 'Contract violation: ' + contractResult.violations.map(v => v.message).join('; ')
445
+ };
446
+ }
447
+
448
+ // 8. Behavioral anomaly detection
376
449
  const anomalies = [];
377
450
  if (this._enableBehavior) {
378
451
  const profile = this._behaviorProfiles.get(session.authCtx.userId);
@@ -395,7 +468,19 @@ class MCPSecurityRuntime {
395
468
  }
396
469
  }
397
470
 
398
- // 8. Record success and return
471
+ // 9. Update compliance attestation signals
472
+ this._attestor.updateSignals({
473
+ injection_scans_active: true,
474
+ tool_authorization_active: this._enforceAuth,
475
+ rate_limiting_active: true,
476
+ threat_scanning_active: true,
477
+ behavior_monitoring_active: this._enableBehavior,
478
+ blocking_enabled: true,
479
+ audit_trail_active: true,
480
+ contract_enforcement_active: contractResult.hasContract
481
+ });
482
+
483
+ // 10. Record success and return
399
484
  this._audit('tool_allowed', {
400
485
  sessionId, toolName, userId: session.authCtx.userId,
401
486
  threats: threats.length, anomalies: anomalies.length
@@ -444,6 +529,61 @@ class MCPSecurityRuntime {
444
529
  return this._resourceScanner.scanResource(uri, content, mimeType);
445
530
  }
446
531
 
532
+ // =======================================================================
533
+ // Adaptive Defense — Learning, Contracts, Compliance
534
+ // =======================================================================
535
+
536
+ /**
537
+ * Registers a behavioral contract for an agent.
538
+ * The contract is verified on every tool call automatically.
539
+ * @param {import('./adaptive-defense').AgentContract} contract
540
+ */
541
+ registerContract(contract) {
542
+ this._contracts.register(contract);
543
+ this._attestor.updateSignal('contract_enforcement_active', true);
544
+ }
545
+
546
+ /**
547
+ * Returns the learning loop for direct access.
548
+ * @returns {import('./adaptive-defense').LearningLoop}
549
+ */
550
+ getLearningLoop() {
551
+ return this._learningLoop;
552
+ }
553
+
554
+ /**
555
+ * Returns a real-time compliance attestation.
556
+ * @returns {object}
557
+ */
558
+ attest() {
559
+ return this._attestor.attest();
560
+ }
561
+
562
+ /**
563
+ * Generates a signed compliance proof that can be verified externally.
564
+ * @param {string} signingKey
565
+ * @returns {{ attestation: object, signature: string }}
566
+ */
567
+ generateComplianceProof(signingKey) {
568
+ return this._attestor.generateProof(signingKey || this._signingKey);
569
+ }
570
+
571
+ /**
572
+ * Returns contract compliance rates for all registered agents.
573
+ * @returns {object}
574
+ */
575
+ getContractCompliance() {
576
+ return this._contracts.getComplianceReport();
577
+ }
578
+
579
+ /**
580
+ * Returns compliance trend direction.
581
+ * @returns {'improving' | 'stable' | 'degrading' | 'unknown'}
582
+ */
583
+ getComplianceTrend() {
584
+ return this._attestor.getTrend();
585
+ }
586
+
447
587
  // =======================================================================
448
588
  // Tool Registration
449
589
  // =======================================================================
@@ -639,6 +779,10 @@ class MCPSecurityRuntime {
639
779
  sessions,
640
780
  behaviorProfiles: behaviorSummaries,
641
781
  guard: this._guard.getStats(),
782
+ learningLoop: this._learningLoop.getReport(),
783
+ contracts: this._contracts.getComplianceReport(),
784
+ compliance: this._attestor.getCurrentState(),
785
+ complianceTrend: this._attestor.getTrend(),
642
786
  recentAudit: this._auditLog.slice(-50)
643
787
  };
644
788
  }