agentshield-sdk 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/README.md +975 -0
  4. package/bin/agent-shield.js +680 -0
  5. package/package.json +118 -0
  6. package/src/adaptive.js +330 -0
  7. package/src/agent-protocol.js +998 -0
  8. package/src/alert-tuning.js +480 -0
  9. package/src/allowlist.js +603 -0
  10. package/src/audit-immutable.js +914 -0
  11. package/src/audit-streaming.js +469 -0
  12. package/src/badges.js +196 -0
  13. package/src/behavior-profiling.js +289 -0
  14. package/src/benchmark-harness.js +804 -0
  15. package/src/canary.js +271 -0
  16. package/src/certification.js +563 -0
  17. package/src/circuit-breaker.js +321 -0
  18. package/src/compliance.js +617 -0
  19. package/src/confidence-tuning.js +324 -0
  20. package/src/confused-deputy.js +624 -0
  21. package/src/context-scoring.js +360 -0
  22. package/src/conversation.js +494 -0
  23. package/src/cost-optimizer.js +1024 -0
  24. package/src/ctf.js +462 -0
  25. package/src/detector-core.js +1999 -0
  26. package/src/distributed.js +359 -0
  27. package/src/document-scanner.js +795 -0
  28. package/src/embedding.js +307 -0
  29. package/src/encoding.js +429 -0
  30. package/src/enterprise.js +405 -0
  31. package/src/errors.js +100 -0
  32. package/src/eu-ai-act.js +523 -0
  33. package/src/fuzzer.js +764 -0
  34. package/src/honeypot.js +328 -0
  35. package/src/i18n-patterns.js +523 -0
  36. package/src/index.js +430 -0
  37. package/src/integrations.js +528 -0
  38. package/src/llm-redteam.js +670 -0
  39. package/src/main.js +741 -0
  40. package/src/main.mjs +38 -0
  41. package/src/mcp-bridge.js +542 -0
  42. package/src/mcp-certification.js +846 -0
  43. package/src/mcp-sdk-integration.js +355 -0
  44. package/src/mcp-security-runtime.js +741 -0
  45. package/src/mcp-server.js +740 -0
  46. package/src/middleware.js +208 -0
  47. package/src/model-finetuning.js +884 -0
  48. package/src/model-fingerprint.js +1042 -0
  49. package/src/multi-agent-trust.js +453 -0
  50. package/src/multi-agent.js +404 -0
  51. package/src/multimodal.js +296 -0
  52. package/src/nist-mapping.js +505 -0
  53. package/src/observability.js +330 -0
  54. package/src/openclaw.js +450 -0
  55. package/src/otel.js +544 -0
  56. package/src/owasp-2025.js +483 -0
  57. package/src/pii.js +390 -0
  58. package/src/plugin-marketplace.js +628 -0
  59. package/src/plugin-system.js +349 -0
  60. package/src/policy-dsl.js +775 -0
  61. package/src/policy-extended.js +635 -0
  62. package/src/policy.js +443 -0
  63. package/src/presets.js +409 -0
  64. package/src/production.js +557 -0
  65. package/src/prompt-leakage.js +321 -0
  66. package/src/rag-vulnerability.js +579 -0
  67. package/src/redteam.js +475 -0
  68. package/src/response-handler.js +429 -0
  69. package/src/scanners.js +357 -0
  70. package/src/self-healing.js +363 -0
  71. package/src/semantic.js +339 -0
  72. package/src/shield-score.js +250 -0
  73. package/src/sso-saml.js +897 -0
  74. package/src/stream-scanner.js +806 -0
  75. package/src/testing.js +505 -0
  76. package/src/threat-encyclopedia.js +629 -0
  77. package/src/threat-intel-network.js +1017 -0
  78. package/src/token-analysis.js +467 -0
  79. package/src/tool-guard.js +412 -0
  80. package/src/tool-output-validator.js +354 -0
  81. package/src/utils.js +83 -0
  82. package/src/watermark.js +235 -0
  83. package/src/worker-scanner.js +601 -0
  84. package/types/index.d.ts +2088 -0
@@ -0,0 +1,846 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — MCP Security Certification & Trust Framework
5
+ *
6
+ * Three components that create a competitive moat:
7
+ *
8
+ * 1. AgentThreatIntelligence — continuously updated attack pattern corpus
9
+ * that gets better with every deployment. A data moat.
10
+ *
11
+ * 2. MCPCertification — "Agent Shield Certified" attestation for MCP servers.
12
+ * If every MCP server runs certification, Agent Shield becomes the standard.
13
+ *
14
+ * 3. CrossOrgAgentTrust — certificate authority for AI agents communicating
15
+ * across organizational boundaries. The "TLS for AI agents."
16
+ *
17
+ * All processing runs locally — no data ever leaves your environment.
18
+ */
19
+
20
+ const crypto = require('crypto');
21
+
22
+ const LOG_PREFIX = '[Agent Shield]';
23
+
24
+ // =========================================================================
25
+ // Agent Threat Intelligence — the data moat
26
+ // =========================================================================
27
+
28
+ /** Built-in threat categories with severity weights. */
29
+ const THREAT_CATEGORIES = Object.freeze({
30
+ prompt_injection: { severity: 'critical', weight: 1.0 },
31
+ data_exfiltration: { severity: 'critical', weight: 1.0 },
32
+ privilege_escalation: { severity: 'critical', weight: 0.95 },
33
+ confused_deputy: { severity: 'high', weight: 0.9 },
34
+ tool_abuse: { severity: 'high', weight: 0.85 },
35
+ session_hijack: { severity: 'high', weight: 0.85 },
36
+ delegation_attack: { severity: 'high', weight: 0.8 },
37
+ prompt_leakage: { severity: 'medium', weight: 0.7 },
38
+ rag_poisoning: { severity: 'medium', weight: 0.7 },
39
+ behavioral_anomaly: { severity: 'medium', weight: 0.6 },
40
+ resource_abuse: { severity: 'low', weight: 0.4 },
41
+ policy_violation: { severity: 'low', weight: 0.3 }
42
+ });
43
+
44
+ /**
45
+ * Local threat intelligence engine that learns from observed attacks.
46
+ * Maintains a pattern corpus that improves detection over time.
47
+ * All data stays local — nothing is ever transmitted.
48
+ */
49
+ class AgentThreatIntelligence {
50
+ /**
51
+ * @param {object} [options]
52
+ * @param {number} [options.maxPatterns=10000] - Max patterns to store
53
+ * @param {number} [options.decayHalfLifeMs=604800000] - Pattern relevance decay (default 7 days)
54
+ * @param {number} [options.minConfidence=0.6] - Min confidence for pattern to be active
55
+ */
56
+ constructor(options = {}) {
57
+ this._maxPatterns = options.maxPatterns || 10000;
58
+ this._decayHalfLife = options.decayHalfLifeMs || 604800000;
59
+ this._minConfidence = options.minConfidence || 0.6;
60
+
61
+ // Pattern corpus — the intelligence
62
+ this._patterns = new Map(); // patternId → PatternEntry
63
+ this._categoryStats = {};
64
+ for (const cat of Object.keys(THREAT_CATEGORIES)) {
65
+ this._categoryStats[cat] = { observed: 0, blocked: 0, bypassed: 0 };
66
+ }
67
+
68
+ // Attack timeline for trend analysis
69
+ this._timeline = [];
70
+ this._maxTimeline = 10000;
71
+
72
+ this.stats = { patternsLearned: 0, attacksObserved: 0, trendsGenerated: 0 };
73
+ }
74
+
75
+ /**
76
+ * Records an observed attack for intelligence gathering.
77
+ * @param {object} attack
78
+ * @param {string} attack.category - Threat category
79
+ * @param {string} attack.pattern - Attack pattern/signature
80
+ * @param {string} [attack.source] - Where the attack was detected
81
+ * @param {object} [attack.context] - Additional context (tool, session, etc.)
82
+ * @param {boolean} [attack.blocked=true] - Whether the attack was blocked
83
+ * @returns {{ patternId: string, isNew: boolean, confidence: number }}
84
+ */
85
+ recordAttack(attack) {
86
+ if (!attack.category || !attack.pattern) {
87
+ throw new Error(`${LOG_PREFIX} recordAttack requires category and pattern`);
88
+ }
89
+
90
+ this.stats.attacksObserved++;
91
+ const patternId = this._hashPattern(attack.category, attack.pattern);
92
+
93
+ // Update category stats
94
+ const catStats = this._categoryStats[attack.category];
95
+ if (catStats) {
96
+ catStats.observed++;
97
+ if (attack.blocked !== false) catStats.blocked++;
98
+ else catStats.bypassed++;
99
+ }
100
+
101
+ // Record in timeline
102
+ if (this._timeline.length >= this._maxTimeline) {
103
+ this._timeline = this._timeline.slice(-Math.floor(this._maxTimeline * 0.75));
104
+ }
105
+ this._timeline.push({
106
+ timestamp: Date.now(),
107
+ category: attack.category,
108
+ patternId,
109
+ blocked: attack.blocked !== false,
110
+ source: attack.source || 'unknown'
111
+ });
112
+
113
+ // Update or create pattern
114
+ const existing = this._patterns.get(patternId);
115
+ if (existing) {
116
+ existing.hitCount++;
117
+ existing.lastSeen = Date.now();
118
+ existing.confidence = Math.min(1.0, existing.confidence + 0.05);
119
+ if (attack.blocked === false) existing.bypassCount++;
120
+ return { patternId, isNew: false, confidence: existing.confidence };
121
+ }
122
+
123
+ // New pattern
124
+ const entry = {
125
+ patternId,
126
+ category: attack.category,
127
+ pattern: attack.pattern,
128
+ source: attack.source || 'unknown',
129
+ firstSeen: Date.now(),
130
+ lastSeen: Date.now(),
131
+ hitCount: 1,
132
+ bypassCount: attack.blocked === false ? 1 : 0,
133
+ confidence: 0.65,
134
+ context: attack.context || {}
135
+ };
136
+
137
+ this._patterns.set(patternId, entry);
138
+ this.stats.patternsLearned++;
139
+
140
+ // Evict oldest patterns if at capacity
141
+ if (this._patterns.size > this._maxPatterns) {
142
+ this._evictOldest();
143
+ }
144
+
145
+ return { patternId, isNew: true, confidence: entry.confidence };
146
+ }
147
+
148
+ /**
149
+ * Checks if input matches any known threat patterns.
150
+ * @param {string} input - Text to check
151
+ * @returns {{ matches: Array<{ patternId: string, category: string, confidence: number }>, riskScore: number }}
152
+ */
153
+ checkAgainstIntel(input) {
154
+ const matches = [];
155
+ const lowerInput = input.toLowerCase();
156
+
157
+ for (const [_id, entry] of this._patterns) {
158
+ if (entry.confidence < this._minConfidence) continue;
159
+ const decayedConfidence = this._applyDecay(entry);
160
+ if (decayedConfidence < this._minConfidence) continue;
161
+
162
+ // Check if pattern appears in input
163
+ if (lowerInput.includes(entry.pattern.toLowerCase())) {
164
+ matches.push({
165
+ patternId: entry.patternId,
166
+ category: entry.category,
167
+ confidence: decayedConfidence,
168
+ hitCount: entry.hitCount,
169
+ firstSeen: entry.firstSeen
170
+ });
171
+ }
172
+ }
173
+
174
+ // Calculate composite risk score
175
+ let riskScore = 0;
176
+ for (const match of matches) {
177
+ const catWeight = (THREAT_CATEGORIES[match.category] || { weight: 0.5 }).weight;
178
+ riskScore = Math.max(riskScore, match.confidence * catWeight);
179
+ }
180
+
181
+ return { matches, riskScore: Math.round(riskScore * 100) / 100 };
182
+ }
183
+
184
+ /**
185
+ * Generates trend analysis from observed attacks.
186
+ * @param {number} [windowMs=86400000] - Analysis window (default 24 hours)
187
+ * @returns {{ topCategories: Array, attackRate: number, trendDirection: string, bypassRate: number }}
188
+ */
189
+ getTrends(windowMs = 86400000) {
190
+ const cutoff = Date.now() - windowMs;
191
+ const recent = this._timeline.filter(e => e.timestamp > cutoff);
192
+ this.stats.trendsGenerated++;
193
+
194
+ // Category breakdown
195
+ const catCounts = {};
196
+ let blocked = 0;
197
+ let bypassed = 0;
198
+ for (const event of recent) {
199
+ catCounts[event.category] = (catCounts[event.category] || 0) + 1;
200
+ if (event.blocked) blocked++;
201
+ else bypassed++;
202
+ }
203
+
204
+ const topCategories = Object.entries(catCounts)
205
+ .sort((a, b) => b[1] - a[1])
206
+ .slice(0, 5)
207
+ .map(([category, count]) => ({ category, count, percentage: recent.length > 0 ? Math.round(count / recent.length * 100) : 0 }));
208
+
209
+ // Trend direction — compare first half vs second half
210
+ const midpoint = cutoff + windowMs / 2;
211
+ const firstHalf = recent.filter(e => e.timestamp < midpoint).length;
212
+ const secondHalf = recent.filter(e => e.timestamp >= midpoint).length;
213
+ let trendDirection = 'stable';
214
+ if (secondHalf > firstHalf * 1.5) trendDirection = 'increasing';
215
+ else if (secondHalf < firstHalf * 0.5) trendDirection = 'decreasing';
216
+
217
+ return {
218
+ topCategories,
219
+ attackRate: recent.length > 0 && windowMs > 0 ? Math.round(recent.length / Math.max(1, windowMs / 3600000) * 100) / 100 : 0,
220
+ trendDirection,
221
+ bypassRate: (blocked + bypassed) > 0 ? Math.round(bypassed / (blocked + bypassed) * 100) / 100 : 0,
222
+ totalObserved: recent.length,
223
+ window: { start: cutoff, end: Date.now() }
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Exports the intelligence corpus for backup/transfer.
229
+ * @returns {object} Serializable intelligence data
230
+ */
231
+ exportCorpus() {
232
+ const patterns = [];
233
+ for (const [_id, entry] of this._patterns) {
234
+ patterns.push({ ...entry });
235
+ }
236
+ return {
237
+ version: '1.0.0',
238
+ exportedAt: Date.now(),
239
+ stats: { ...this.stats },
240
+ categoryStats: { ...this._categoryStats },
241
+ patterns
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Imports intelligence corpus from another instance.
247
+ * @param {object} corpus - Output of exportCorpus()
248
+ * @returns {{ imported: number, merged: number, skipped: number }}
249
+ */
250
+ importCorpus(corpus) {
251
+ if (!corpus || !corpus.patterns) {
252
+ throw new Error(`${LOG_PREFIX} Invalid corpus format`);
253
+ }
254
+ let imported = 0;
255
+ let merged = 0;
256
+ let skipped = 0;
257
+
258
+ for (const entry of corpus.patterns) {
259
+ // Validate entry structure
260
+ if (!entry.patternId || !entry.category || !entry.pattern ||
261
+ typeof entry.confidence !== 'number' || typeof entry.hitCount !== 'number') {
262
+ skipped++;
263
+ continue;
264
+ }
265
+
266
+ const existing = this._patterns.get(entry.patternId);
267
+ if (existing) {
268
+ // Merge — take higher confidence and combined hit count
269
+ existing.hitCount += entry.hitCount;
270
+ existing.confidence = Math.max(existing.confidence, entry.confidence);
271
+ if (entry.lastSeen > existing.lastSeen) existing.lastSeen = entry.lastSeen;
272
+ merged++;
273
+ } else if (this._patterns.size < this._maxPatterns) {
274
+ this._patterns.set(entry.patternId, { ...entry });
275
+ imported++;
276
+ } else {
277
+ skipped++;
278
+ }
279
+ }
280
+
281
+ return { imported, merged, skipped };
282
+ }
283
+
284
+ /** @private */
285
+ _hashPattern(category, pattern) {
286
+ return crypto.createHash('sha256').update(`${category}:${pattern}`).digest('hex').substring(0, 16);
287
+ }
288
+
289
+ /** @private */
290
+ _applyDecay(entry) {
291
+ const age = Math.max(0, Date.now() - entry.lastSeen);
292
+ const halfLife = Math.max(1, this._decayHalfLife);
293
+ const decayFactor = Math.pow(0.5, age / halfLife);
294
+ return entry.confidence * decayFactor;
295
+ }
296
+
297
+ /** @private */
298
+ _evictOldest() {
299
+ let oldest = null;
300
+ let oldestTime = Infinity;
301
+ for (const [id, entry] of this._patterns) {
302
+ if (entry.lastSeen < oldestTime) {
303
+ oldestTime = entry.lastSeen;
304
+ oldest = id;
305
+ }
306
+ }
307
+ if (oldest) this._patterns.delete(oldest);
308
+ }
309
+ }
310
+
311
+ // =========================================================================
312
+ // MCP Certification — "Agent Shield Certified"
313
+ // =========================================================================
314
+
315
+ /** Certification requirements — what an MCP server must have. */
316
+ const CERTIFICATION_REQUIREMENTS = Object.freeze([
317
+ {
318
+ id: 'AUTH_001',
319
+ name: 'Per-User Authentication',
320
+ category: 'authentication',
321
+ severity: 'critical',
322
+ description: 'MCP server must authenticate individual users, not just agents',
323
+ check: (config) => config.enforceAuth !== false
324
+ },
325
+ {
326
+ id: 'AUTH_002',
327
+ name: 'Authorization Context Propagation',
328
+ category: 'authentication',
329
+ severity: 'critical',
330
+ description: 'Authorization context must flow through all tool calls',
331
+ check: (config) => config.contextPropagation !== false
332
+ },
333
+ {
334
+ id: 'AUTH_003',
335
+ name: 'Delegation Depth Limiting',
336
+ category: 'authentication',
337
+ severity: 'high',
338
+ description: 'Agent delegation chains must be depth-limited',
339
+ check: (config) => (config.maxDelegationDepth || 0) > 0 && (config.maxDelegationDepth || Infinity) <= 10
340
+ },
341
+ {
342
+ id: 'SCAN_001',
343
+ name: 'Tool Input Scanning',
344
+ category: 'scanning',
345
+ severity: 'critical',
346
+ description: 'All tool call arguments must be scanned for injection',
347
+ check: (config) => config.scanInputs !== false
348
+ },
349
+ {
350
+ id: 'SCAN_002',
351
+ name: 'Tool Output Scanning',
352
+ category: 'scanning',
353
+ severity: 'high',
354
+ description: 'Tool results must be scanned before returning to user',
355
+ check: (config) => config.scanOutputs !== false
356
+ },
357
+ {
358
+ id: 'SCAN_003',
359
+ name: 'Resource Content Scanning',
360
+ category: 'scanning',
361
+ severity: 'medium',
362
+ description: 'MCP resources should be scanned before exposure',
363
+ check: (config) => config.scanResources !== false
364
+ },
365
+ {
366
+ id: 'RATE_001',
367
+ name: 'Per-Session Rate Limiting',
368
+ category: 'rate_limiting',
369
+ severity: 'high',
370
+ description: 'Tool calls must be rate-limited per session',
371
+ check: (config) => (config.maxToolCallsPerSession || 0) > 0
372
+ },
373
+ {
374
+ id: 'RATE_002',
375
+ name: 'Token Budget Enforcement',
376
+ category: 'rate_limiting',
377
+ severity: 'medium',
378
+ description: 'Sessions must have token/cost budgets',
379
+ check: (config) => (config.maxTokenBudget || 0) > 0
380
+ },
381
+ {
382
+ id: 'AUDIT_001',
383
+ name: 'Audit Trail',
384
+ category: 'audit',
385
+ severity: 'critical',
386
+ description: 'All security events must be logged to an audit trail',
387
+ check: (config) => config.auditEnabled !== false
388
+ },
389
+ {
390
+ id: 'AUDIT_002',
391
+ name: 'Threat Event Callbacks',
392
+ category: 'audit',
393
+ severity: 'high',
394
+ description: 'Security team must be notified of threats in real-time',
395
+ check: (config) => typeof config.onThreat === 'function'
396
+ },
397
+ {
398
+ id: 'CRYPTO_001',
399
+ name: 'HMAC Context Signing',
400
+ category: 'cryptography',
401
+ severity: 'critical',
402
+ description: 'Authorization contexts must be HMAC-signed to prevent forgery',
403
+ check: (config) => config.signingKey && config.signingKey !== 'agent-shield-default-signing-key'
404
+ },
405
+ {
406
+ id: 'CRYPTO_002',
407
+ name: 'Ephemeral Token Credentials',
408
+ category: 'cryptography',
409
+ severity: 'high',
410
+ description: 'Tool access should use ephemeral tokens, not static credentials',
411
+ check: (config) => config.ephemeralTokens !== false
412
+ },
413
+ {
414
+ id: 'BEHAV_001',
415
+ name: 'Behavioral Monitoring',
416
+ category: 'monitoring',
417
+ severity: 'medium',
418
+ description: 'Agent behavior should be profiled for anomaly detection',
419
+ check: (config) => config.enableBehaviorMonitoring !== false
420
+ },
421
+ {
422
+ id: 'STATE_001',
423
+ name: 'Session State Machine',
424
+ category: 'session',
425
+ severity: 'high',
426
+ description: 'Sessions must enforce valid state transitions',
427
+ check: (config) => config.enableStateMachine !== false
428
+ },
429
+ {
430
+ id: 'POLICY_001',
431
+ name: 'Tool-Level Policies',
432
+ category: 'policy',
433
+ severity: 'high',
434
+ description: 'Individual tools must have declared security policies',
435
+ check: (config) => (config.registeredTools || 0) > 0
436
+ }
437
+ ]);
438
+
439
+ /** Certification levels based on score. */
440
+ const CERTIFICATION_LEVELS = Object.freeze({
441
+ PLATINUM: { minScore: 95, label: 'Platinum', badge: '🛡️ Agent Shield Certified — Platinum' },
442
+ GOLD: { minScore: 80, label: 'Gold', badge: '🛡️ Agent Shield Certified — Gold' },
443
+ SILVER: { minScore: 65, label: 'Silver', badge: '🛡️ Agent Shield Certified — Silver' },
444
+ BRONZE: { minScore: 50, label: 'Bronze', badge: '🛡️ Agent Shield Certified — Bronze' },
445
+ NONE: { minScore: 0, label: 'Not Certified', badge: '⚠️ Not Certified' }
446
+ });
447
+
448
+ /**
449
+ * Evaluates an MCP server configuration against Agent Shield certification requirements.
450
+ */
451
+ class MCPCertification {
452
+ /**
453
+ * Runs certification audit against the provided configuration.
454
+ * @param {object} config - MCP server security configuration
455
+ * @returns {{ certified: boolean, level: string, score: number, badge: string, results: Array, recommendations: Array }}
456
+ */
457
+ static evaluate(config = {}) {
458
+ const results = [];
459
+ let totalWeight = 0;
460
+ let earnedWeight = 0;
461
+
462
+ const severityWeights = { critical: 3, high: 2, medium: 1 };
463
+
464
+ for (const req of CERTIFICATION_REQUIREMENTS) {
465
+ const weight = severityWeights[req.severity] || 1;
466
+ totalWeight += weight;
467
+
468
+ let passed = false;
469
+ try {
470
+ passed = req.check(config);
471
+ } catch {
472
+ passed = false;
473
+ }
474
+
475
+ if (passed) earnedWeight += weight;
476
+
477
+ results.push({
478
+ id: req.id,
479
+ name: req.name,
480
+ category: req.category,
481
+ severity: req.severity,
482
+ passed,
483
+ description: req.description
484
+ });
485
+ }
486
+
487
+ const score = Math.round(earnedWeight / totalWeight * 100);
488
+
489
+ // Determine level
490
+ let level = CERTIFICATION_LEVELS.NONE;
491
+ for (const l of [CERTIFICATION_LEVELS.PLATINUM, CERTIFICATION_LEVELS.GOLD, CERTIFICATION_LEVELS.SILVER, CERTIFICATION_LEVELS.BRONZE]) {
492
+ if (score >= l.minScore) { level = l; break; }
493
+ }
494
+
495
+ // Generate recommendations for failed checks
496
+ const recommendations = results
497
+ .filter(r => !r.passed)
498
+ .sort((a, b) => (severityWeights[b.severity] || 0) - (severityWeights[a.severity] || 0))
499
+ .map(r => ({
500
+ id: r.id,
501
+ priority: r.severity,
502
+ action: r.description
503
+ }));
504
+
505
+ return {
506
+ certified: score >= CERTIFICATION_LEVELS.BRONZE.minScore,
507
+ level: level.label,
508
+ score,
509
+ badge: level.badge,
510
+ timestamp: Date.now(),
511
+ results,
512
+ recommendations,
513
+ summary: {
514
+ total: results.length,
515
+ passed: results.filter(r => r.passed).length,
516
+ failed: results.filter(r => !r.passed).length,
517
+ criticalFailures: results.filter(r => !r.passed && r.severity === 'critical').length
518
+ }
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Generates a certification report as formatted text.
524
+ * @param {object} evaluation - Output of evaluate()
525
+ * @returns {string}
526
+ */
527
+ static formatReport(evaluation) {
528
+ const lines = [
529
+ '',
530
+ '═'.repeat(60),
531
+ ' MCP Security Certification Report',
532
+ '═'.repeat(60),
533
+ '',
534
+ ` Level: ${evaluation.badge}`,
535
+ ` Score: ${evaluation.score}/100`,
536
+ ` Date: ${new Date(evaluation.timestamp).toISOString().substring(0, 10)}`,
537
+ '',
538
+ ` Results: ${evaluation.summary.passed}/${evaluation.summary.total} passed` +
539
+ (evaluation.summary.criticalFailures > 0 ? ` (${evaluation.summary.criticalFailures} critical failures)` : ''),
540
+ ''
541
+ ];
542
+
543
+ // Group by category
544
+ const categories = {};
545
+ for (const r of evaluation.results) {
546
+ if (!categories[r.category]) categories[r.category] = [];
547
+ categories[r.category].push(r);
548
+ }
549
+
550
+ for (const [cat, items] of Object.entries(categories)) {
551
+ lines.push(` ${cat.toUpperCase()}`);
552
+ for (const item of items) {
553
+ const icon = item.passed ? '✓' : '✗';
554
+ lines.push(` ${icon} [${item.id}] ${item.name}`);
555
+ }
556
+ lines.push('');
557
+ }
558
+
559
+ if (evaluation.recommendations.length > 0) {
560
+ lines.push(' RECOMMENDATIONS');
561
+ for (const rec of evaluation.recommendations) {
562
+ lines.push(` [${rec.priority.toUpperCase()}] ${rec.action}`);
563
+ }
564
+ lines.push('');
565
+ }
566
+
567
+ lines.push('═'.repeat(60));
568
+ return lines.join('\n');
569
+ }
570
+ }
571
+
572
+ // =========================================================================
573
+ // Cross-Organization Agent Trust — CA for AI agents
574
+ // =========================================================================
575
+
576
+ /**
577
+ * Certificate authority for AI agents crossing organizational boundaries.
578
+ * Issues, verifies, and revokes trust certificates for agents.
579
+ *
580
+ * When agents from different organizations need to interact via MCP,
581
+ * they present their trust certificate to prove identity and capabilities.
582
+ */
583
+ class CrossOrgAgentTrust {
584
+ /**
585
+ * @param {object} options
586
+ * @param {string} options.orgId - Organization identifier
587
+ * @param {string} options.signingKey - HMAC key for certificate signing
588
+ * @param {number} [options.certificateTtlMs=86400000] - Certificate lifetime (default 24 hours)
589
+ * @param {number} [options.maxCertificates=1000] - Max active certificates
590
+ */
591
+ constructor(options = {}) {
592
+ if (!options.orgId) throw new Error(`${LOG_PREFIX} CrossOrgAgentTrust requires orgId`);
593
+ if (!options.signingKey) throw new Error(`${LOG_PREFIX} CrossOrgAgentTrust requires signingKey`);
594
+
595
+ this._orgId = options.orgId;
596
+ this._signingKey = options.signingKey;
597
+ this._certificateTtlMs = options.certificateTtlMs || 86400000;
598
+ this._maxCertificates = options.maxCertificates || 1000;
599
+
600
+ // Active certificates
601
+ this._certificates = new Map(); // certId → certificate
602
+ this._revokedCerts = new Set();
603
+ this._trustedOrgs = new Map(); // orgId → { publicKey, trustLevel }
604
+
605
+ this.stats = { issued: 0, verified: 0, rejected: 0, revoked: 0 };
606
+ }
607
+
608
+ /**
609
+ * Issues a trust certificate for an agent.
610
+ * @param {object} params
611
+ * @param {string} params.agentId - Agent identity
612
+ * @param {string[]} params.capabilities - What the agent can do
613
+ * @param {string[]} [params.allowedOrgs] - Which orgs this agent can interact with ('*' = any)
614
+ * @param {number} [params.trustLevel=5] - Trust level 1-10
615
+ * @returns {object} Signed certificate
616
+ */
617
+ issueCertificate(params) {
618
+ if (!params.agentId) throw new Error(`${LOG_PREFIX} issueCertificate requires agentId`);
619
+
620
+ const certId = crypto.randomUUID();
621
+ const now = Date.now();
622
+
623
+ const certificate = {
624
+ certId,
625
+ version: '1.0',
626
+ issuer: this._orgId,
627
+ subject: {
628
+ agentId: params.agentId,
629
+ orgId: this._orgId
630
+ },
631
+ capabilities: Object.freeze([...(params.capabilities || [])]),
632
+ allowedOrgs: params.allowedOrgs || ['*'],
633
+ trustLevel: Math.min(10, Math.max(1, params.trustLevel || 5)),
634
+ issuedAt: now,
635
+ expiresAt: now + this._certificateTtlMs,
636
+ serialNumber: crypto.randomBytes(8).toString('hex')
637
+ };
638
+
639
+ // Sign the certificate
640
+ certificate.signature = this._signCertificate(certificate);
641
+
642
+ this._certificates.set(certId, certificate);
643
+ this.stats.issued++;
644
+
645
+ // Evict if at capacity
646
+ if (this._certificates.size > this._maxCertificates) {
647
+ this._evictExpired();
648
+ }
649
+
650
+ return { ...certificate };
651
+ }
652
+
653
+ /**
654
+ * Verifies a certificate's authenticity and validity.
655
+ * @param {object} certificate
656
+ * @returns {{ valid: boolean, reason?: string, trustLevel: number }}
657
+ */
658
+ verifyCertificate(certificate) {
659
+ this.stats.verified++;
660
+
661
+ // Check revocation
662
+ if (this._revokedCerts.has(certificate.certId)) {
663
+ this.stats.rejected++;
664
+ return { valid: false, reason: 'Certificate has been revoked', trustLevel: 0 };
665
+ }
666
+
667
+ // Check expiry
668
+ if (Date.now() > certificate.expiresAt) {
669
+ this.stats.rejected++;
670
+ return { valid: false, reason: 'Certificate has expired', trustLevel: 0 };
671
+ }
672
+
673
+ // Validate signature format
674
+ if (typeof certificate.signature !== 'string' || !/^[0-9a-f]{64}$/i.test(certificate.signature)) {
675
+ this.stats.rejected++;
676
+ return { valid: false, reason: 'Invalid signature format', trustLevel: 0 };
677
+ }
678
+
679
+ // Verify signature — if from our org, use our key
680
+ if (certificate.issuer === this._orgId) {
681
+ const expectedSig = this._signCertificate(certificate);
682
+ try {
683
+ const valid = crypto.timingSafeEqual(
684
+ Buffer.from(certificate.signature, 'hex'),
685
+ Buffer.from(expectedSig, 'hex')
686
+ );
687
+ if (!valid) {
688
+ this.stats.rejected++;
689
+ return { valid: false, reason: 'Invalid signature', trustLevel: 0 };
690
+ }
691
+ } catch {
692
+ this.stats.rejected++;
693
+ return { valid: false, reason: 'Signature verification failed', trustLevel: 0 };
694
+ }
695
+ } else {
696
+ // External certificate — check if we trust the issuer
697
+ const trustedOrg = this._trustedOrgs.get(certificate.issuer);
698
+ if (!trustedOrg) {
699
+ this.stats.rejected++;
700
+ return { valid: false, reason: `Unknown issuer: ${certificate.issuer}`, trustLevel: 0 };
701
+ }
702
+ // Verify with trusted org's key
703
+ const expectedSig = this._signCertificateWithKey(certificate, trustedOrg.publicKey);
704
+ try {
705
+ const valid = crypto.timingSafeEqual(
706
+ Buffer.from(certificate.signature, 'hex'),
707
+ Buffer.from(expectedSig, 'hex')
708
+ );
709
+ if (!valid) {
710
+ this.stats.rejected++;
711
+ return { valid: false, reason: 'External certificate signature invalid', trustLevel: 0 };
712
+ }
713
+ } catch {
714
+ this.stats.rejected++;
715
+ return { valid: false, reason: 'External signature verification failed', trustLevel: 0 };
716
+ }
717
+ }
718
+
719
+ // Check if certificate allows interaction with our org
720
+ const allowedOrgs = certificate.allowedOrgs || [];
721
+ if (!allowedOrgs.includes('*') && !allowedOrgs.includes(this._orgId)) {
722
+ this.stats.rejected++;
723
+ return { valid: false, reason: `Certificate does not authorize interaction with ${this._orgId}`, trustLevel: 0 };
724
+ }
725
+
726
+ return { valid: true, trustLevel: certificate.trustLevel || 5 };
727
+ }
728
+
729
+ /**
730
+ * Revokes a certificate.
731
+ * @param {string} certId
732
+ * @returns {boolean} True if certificate was found and revoked
733
+ */
734
+ revokeCertificate(certId) {
735
+ this._revokedCerts.add(certId);
736
+ const existed = this._certificates.delete(certId);
737
+ if (existed) this.stats.revoked++;
738
+ return existed;
739
+ }
740
+
741
+ /**
742
+ * Registers a trusted external organization.
743
+ * @param {string} orgId - Organization identifier
744
+ * @param {string} publicKey - Their signing key for certificate verification
745
+ * @param {number} [trustLevel=5] - How much we trust them (1-10)
746
+ */
747
+ trustOrganization(orgId, publicKey, trustLevel = 5) {
748
+ if (!publicKey || typeof publicKey !== 'string') {
749
+ throw new Error(`${LOG_PREFIX} trustOrganization requires a non-empty publicKey`);
750
+ }
751
+ this._trustedOrgs.set(orgId, {
752
+ publicKey,
753
+ trustLevel: Math.min(10, Math.max(1, trustLevel)),
754
+ trustedAt: Date.now()
755
+ });
756
+ }
757
+
758
+ /**
759
+ * Removes trust for an organization.
760
+ * @param {string} orgId
761
+ */
762
+ untrustOrganization(orgId) {
763
+ this._trustedOrgs.delete(orgId);
764
+ }
765
+
766
+ /**
767
+ * Returns trust status summary.
768
+ * @returns {object}
769
+ */
770
+ getTrustReport() {
771
+ const activeCerts = [];
772
+ for (const [_id, cert] of this._certificates) {
773
+ if (Date.now() <= cert.expiresAt) {
774
+ activeCerts.push({
775
+ certId: cert.certId,
776
+ agentId: cert.subject.agentId,
777
+ trustLevel: cert.trustLevel,
778
+ expiresIn: cert.expiresAt - Date.now()
779
+ });
780
+ }
781
+ }
782
+
783
+ const trustedOrgs = [];
784
+ for (const [orgId, info] of this._trustedOrgs) {
785
+ trustedOrgs.push({ orgId, trustLevel: info.trustLevel, trustedAt: info.trustedAt });
786
+ }
787
+
788
+ return {
789
+ orgId: this._orgId,
790
+ stats: { ...this.stats },
791
+ activeCertificates: activeCerts.length,
792
+ revokedCertificates: this._revokedCerts.size,
793
+ trustedOrganizations: trustedOrgs,
794
+ certificates: activeCerts
795
+ };
796
+ }
797
+
798
+ /** @private */
799
+ _signCertificate(cert) {
800
+ return this._signCertificateWithKey(cert, this._signingKey);
801
+ }
802
+
803
+ /** @private */
804
+ _signCertificateWithKey(cert, key) {
805
+ const data = `${cert.certId}:${cert.issuer}:${cert.subject.agentId}:${cert.subject.orgId}:${cert.capabilities.join(',')}:${cert.issuedAt}:${cert.expiresAt}:${cert.serialNumber}`;
806
+ return crypto.createHmac('sha256', key).update(data).digest('hex');
807
+ }
808
+
809
+ /** @private */
810
+ _evictExpired() {
811
+ const now = Date.now();
812
+ const expiredIds = [];
813
+ for (const [certId, cert] of this._certificates) {
814
+ if (now > cert.expiresAt) expiredIds.push(certId);
815
+ }
816
+ for (const certId of expiredIds) {
817
+ this._certificates.delete(certId);
818
+ this._revokedCerts.delete(certId);
819
+ }
820
+ // LRU fallback: if still over capacity, evict oldest valid certificate
821
+ if (this._certificates.size > this._maxCertificates) {
822
+ let oldestId = null;
823
+ let oldestTime = Infinity;
824
+ for (const [certId, cert] of this._certificates) {
825
+ if (cert.issuedAt < oldestTime) {
826
+ oldestTime = cert.issuedAt;
827
+ oldestId = certId;
828
+ }
829
+ }
830
+ if (oldestId) this._certificates.delete(oldestId);
831
+ }
832
+ }
833
+ }
834
+
835
+ // =========================================================================
836
+ // Exports
837
+ // =========================================================================
838
+
839
+ module.exports = {
840
+ AgentThreatIntelligence,
841
+ MCPCertification,
842
+ CrossOrgAgentTrust,
843
+ THREAT_CATEGORIES,
844
+ CERTIFICATION_REQUIREMENTS,
845
+ CERTIFICATION_LEVELS
846
+ };