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,1024 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Cost Optimizer
5
+ *
6
+ * Automatically tunes detection depth based on threat level and latency budget.
7
+ * Balances cost, latency, and accuracy across scanning tiers. All detection
8
+ * runs locally — no data ever leaves your environment.
9
+ *
10
+ * Exports: CostOptimizer, LatencyBudget, AdaptiveScanner, TierManager,
11
+ * PerformanceMonitor, ScanPlan, OPTIMIZATION_PRESETS
12
+ */
13
+
14
+ // =========================================================================
15
+ // OPTIMIZATION PRESETS
16
+ // =========================================================================
17
+
18
+ /**
19
+ * Pre-built optimization configurations.
20
+ * @type {Object<string, object>}
21
+ */
22
+ const OPTIMIZATION_PRESETS = {
23
+ realtime: {
24
+ name: 'realtime',
25
+ description: 'Maximum speed, fast tier only, throughput priority',
26
+ maxLatencyMs: 10,
27
+ costPerScan: 0,
28
+ targetThroughput: 10000,
29
+ adaptiveEnabled: false,
30
+ tiers: ['fast'],
31
+ defaultTier: 'fast',
32
+ priority: 'throughput'
33
+ },
34
+ balanced: {
35
+ name: 'balanced',
36
+ description: 'Balanced cost/latency/accuracy with adaptive tiers',
37
+ maxLatencyMs: 50,
38
+ costPerScan: 0,
39
+ targetThroughput: 1000,
40
+ adaptiveEnabled: true,
41
+ tiers: ['fast', 'standard', 'deep'],
42
+ defaultTier: 'standard',
43
+ priority: 'balanced'
44
+ },
45
+ thorough: {
46
+ name: 'thorough',
47
+ description: 'Deep scanning with accuracy priority',
48
+ maxLatencyMs: 200,
49
+ costPerScan: 0,
50
+ targetThroughput: 200,
51
+ adaptiveEnabled: true,
52
+ tiers: ['standard', 'deep'],
53
+ defaultTier: 'deep',
54
+ priority: 'accuracy'
55
+ },
56
+ paranoid: {
57
+ name: 'paranoid',
58
+ description: 'All checks enabled, maximum security',
59
+ maxLatencyMs: 500,
60
+ costPerScan: 0,
61
+ targetThroughput: 50,
62
+ adaptiveEnabled: false,
63
+ tiers: ['paranoid'],
64
+ defaultTier: 'paranoid',
65
+ priority: 'security'
66
+ }
67
+ };
68
+
69
+ // =========================================================================
70
+ // ScanPlan
71
+ // =========================================================================
72
+
73
+ /**
74
+ * Describes a planned scan execution with ordered steps.
75
+ */
76
+ class ScanPlan {
77
+ /**
78
+ * @param {string} tier - The scanning tier for this plan.
79
+ * @param {object} [config] - Plan configuration.
80
+ * @param {number} [config.costPerStep=0] - Base cost per step.
81
+ * @param {string} [config.description=''] - Plan description.
82
+ */
83
+ constructor(tier, config = {}) {
84
+ this.tier = tier;
85
+ this.costPerStep = config.costPerStep || 0;
86
+ this.description = config.description || '';
87
+ this.steps = [];
88
+ this.createdAt = Date.now();
89
+ }
90
+
91
+ /**
92
+ * Add a scan step to the plan.
93
+ * @param {string} name - Step name.
94
+ * @param {object} [config] - Step configuration.
95
+ * @param {number} [config.timeout=10] - Step timeout in ms.
96
+ * @param {number} [config.cost=0] - Step cost.
97
+ * @param {boolean} [config.required=true] - Whether step is required.
98
+ * @param {string} [config.type='pattern'] - Step type.
99
+ * @returns {ScanPlan} This plan for chaining.
100
+ */
101
+ addStep(name, config = {}) {
102
+ this.steps.push({
103
+ name,
104
+ timeout: config.timeout || 10,
105
+ cost: config.cost || 0,
106
+ required: config.required !== undefined ? config.required : true,
107
+ type: config.type || 'pattern',
108
+ order: this.steps.length
109
+ });
110
+ return this;
111
+ }
112
+
113
+ /**
114
+ * Get estimated total latency (sum of step timeouts).
115
+ * @returns {number} Estimated latency in ms.
116
+ */
117
+ getEstimatedLatency() {
118
+ return this.steps.reduce((sum, step) => sum + step.timeout, 0);
119
+ }
120
+
121
+ /**
122
+ * Get estimated total cost.
123
+ * @returns {number} Estimated cost.
124
+ */
125
+ getEstimatedCost() {
126
+ const stepCosts = this.steps.reduce((sum, step) => sum + step.cost, 0);
127
+ return stepCosts + (this.steps.length * this.costPerStep);
128
+ }
129
+
130
+ /**
131
+ * Get ordered steps.
132
+ * @returns {Array<object>} Steps sorted by order.
133
+ */
134
+ getSteps() {
135
+ return [...this.steps].sort((a, b) => a.order - b.order);
136
+ }
137
+
138
+ /**
139
+ * Serialize plan to JSON-safe object.
140
+ * @returns {object} Serializable plan.
141
+ */
142
+ toJSON() {
143
+ return {
144
+ tier: this.tier,
145
+ description: this.description,
146
+ steps: this.getSteps(),
147
+ estimatedLatency: this.getEstimatedLatency(),
148
+ estimatedCost: this.getEstimatedCost(),
149
+ createdAt: this.createdAt
150
+ };
151
+ }
152
+ }
153
+
154
+ // =========================================================================
155
+ // TierManager
156
+ // =========================================================================
157
+
158
+ /**
159
+ * Defines and manages scanning tiers with pre-built defaults.
160
+ */
161
+ class TierManager {
162
+ /**
163
+ * Creates a TierManager with pre-defined tiers.
164
+ */
165
+ constructor() {
166
+ /** @type {Map<string, object>} */
167
+ this.tiers = new Map();
168
+ this._initDefaults();
169
+ }
170
+
171
+ /**
172
+ * Initialize default scanning tiers.
173
+ * @private
174
+ */
175
+ _initDefaults() {
176
+ this.defineTier('fast', {
177
+ patterns: 'critical',
178
+ maxPatterns: 10,
179
+ enableSemantic: false,
180
+ enableEncoding: false,
181
+ timeout: 5,
182
+ priority: 1
183
+ });
184
+
185
+ this.defineTier('standard', {
186
+ patterns: 'all',
187
+ maxPatterns: 0, // 0 = unlimited
188
+ enableSemantic: false,
189
+ enableEncoding: true,
190
+ timeout: 50,
191
+ priority: 2
192
+ });
193
+
194
+ this.defineTier('deep', {
195
+ patterns: 'extended',
196
+ maxPatterns: 0,
197
+ enableSemantic: true,
198
+ enableEncoding: true,
199
+ timeout: 200,
200
+ priority: 3
201
+ });
202
+
203
+ this.defineTier('paranoid', {
204
+ patterns: 'extended',
205
+ maxPatterns: 0,
206
+ enableSemantic: true,
207
+ enableEncoding: true,
208
+ timeout: 500,
209
+ priority: 4,
210
+ multiplePass: true
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Define or update a scanning tier.
216
+ * @param {string} name - Tier name.
217
+ * @param {object} config - Tier configuration.
218
+ * @param {string} [config.patterns='all'] - Pattern set: 'critical', 'all', or 'extended'.
219
+ * @param {number} [config.maxPatterns=0] - Max patterns to check (0 = unlimited).
220
+ * @param {boolean} [config.enableSemantic=false] - Enable semantic analysis.
221
+ * @param {boolean} [config.enableEncoding=false] - Enable encoding decode.
222
+ * @param {number} [config.timeout=50] - Timeout in ms.
223
+ * @param {number} [config.priority=1] - Priority (higher = more thorough).
224
+ */
225
+ defineTier(name, config) {
226
+ this.tiers.set(name, {
227
+ name,
228
+ patterns: config.patterns || 'all',
229
+ maxPatterns: config.maxPatterns || 0,
230
+ enableSemantic: config.enableSemantic || false,
231
+ enableEncoding: config.enableEncoding || false,
232
+ timeout: config.timeout || 50,
233
+ priority: config.priority || 1,
234
+ multiplePass: config.multiplePass || false
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Get configuration for a tier.
240
+ * @param {string} name - Tier name.
241
+ * @returns {object|null} Tier configuration or null if not found.
242
+ */
243
+ getTier(name) {
244
+ return this.tiers.get(name) || null;
245
+ }
246
+
247
+ /**
248
+ * List all tiers sorted by priority (ascending).
249
+ * @returns {Array<object>} All tiers sorted by priority.
250
+ */
251
+ listTiers() {
252
+ return [...this.tiers.values()].sort((a, b) => a.priority - b.priority);
253
+ }
254
+ }
255
+
256
+ // =========================================================================
257
+ // LatencyBudget
258
+ // =========================================================================
259
+
260
+ /** Valid scan stages. */
261
+ const STAGES = ['preprocessing', 'pattern_matching', 'semantic_analysis', 'postprocessing'];
262
+
263
+ /**
264
+ * Manages time budgets across scan stages.
265
+ */
266
+ class LatencyBudget {
267
+ /**
268
+ * @param {number} totalBudgetMs - Total latency budget in milliseconds.
269
+ */
270
+ constructor(totalBudgetMs) {
271
+ this.totalBudgetMs = totalBudgetMs;
272
+ /** @type {Map<string, number>} Proportion allocated per stage. */
273
+ this.allocations = new Map();
274
+ /** @type {Map<string, number>} Time spent per stage. */
275
+ this.spent = new Map();
276
+
277
+ // Default allocations
278
+ this.allocate('preprocessing', 0.1);
279
+ this.allocate('pattern_matching', 0.6);
280
+ this.allocate('semantic_analysis', 0.2);
281
+ this.allocate('postprocessing', 0.1);
282
+ }
283
+
284
+ /**
285
+ * Allocate a proportion of the total budget to a stage.
286
+ * @param {string} stage - Stage name.
287
+ * @param {number} proportion - Proportion (0-1) of total budget.
288
+ */
289
+ allocate(stage, proportion) {
290
+ if (!STAGES.includes(stage)) {
291
+ console.log(`[Agent Shield] Warning: unknown stage "${stage}"`);
292
+ }
293
+ this.allocations.set(stage, Math.max(0, Math.min(1, proportion)));
294
+ if (!this.spent.has(stage)) {
295
+ this.spent.set(stage, 0);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Record time spent on a stage.
301
+ * @param {string} stage - Stage name.
302
+ * @param {number} timeMs - Time spent in milliseconds.
303
+ */
304
+ spend(stage, timeMs) {
305
+ const current = this.spent.get(stage) || 0;
306
+ this.spent.set(stage, current + timeMs);
307
+ }
308
+
309
+ /**
310
+ * Get remaining budget for a stage.
311
+ * @param {string} stage - Stage name.
312
+ * @returns {number} Remaining time in ms.
313
+ */
314
+ remaining(stage) {
315
+ const proportion = this.allocations.get(stage) || 0;
316
+ const budgetMs = this.totalBudgetMs * proportion;
317
+ const spentMs = this.spent.get(stage) || 0;
318
+ return Math.max(0, budgetMs - spentMs);
319
+ }
320
+
321
+ /**
322
+ * Check if budget for a stage is exhausted.
323
+ * @param {string} stage - Stage name.
324
+ * @returns {boolean} True if budget is exhausted.
325
+ */
326
+ isExhausted(stage) {
327
+ return this.remaining(stage) <= 0;
328
+ }
329
+
330
+ /**
331
+ * Get a report of budget vs actual for all stages.
332
+ * @returns {object} Per-stage budget report.
333
+ */
334
+ getReport() {
335
+ const stages = {};
336
+ for (const stage of STAGES) {
337
+ const proportion = this.allocations.get(stage) || 0;
338
+ const budgetMs = this.totalBudgetMs * proportion;
339
+ const spentMs = this.spent.get(stage) || 0;
340
+ stages[stage] = {
341
+ budgetMs: Math.round(budgetMs * 100) / 100,
342
+ spentMs: Math.round(spentMs * 100) / 100,
343
+ remainingMs: Math.round(Math.max(0, budgetMs - spentMs) * 100) / 100,
344
+ proportion,
345
+ exhausted: spentMs >= budgetMs
346
+ };
347
+ }
348
+
349
+ const totalSpent = [...this.spent.values()].reduce((s, v) => s + v, 0);
350
+ return {
351
+ totalBudgetMs: this.totalBudgetMs,
352
+ totalSpentMs: Math.round(totalSpent * 100) / 100,
353
+ totalRemainingMs: Math.round(Math.max(0, this.totalBudgetMs - totalSpent) * 100) / 100,
354
+ stages
355
+ };
356
+ }
357
+ }
358
+
359
+ // =========================================================================
360
+ // PerformanceMonitor
361
+ // =========================================================================
362
+
363
+ /**
364
+ * Tracks real-time performance metrics with a rolling window.
365
+ */
366
+ class PerformanceMonitor {
367
+ /**
368
+ * @param {number} [windowSize=1000] - Number of scans to keep in the rolling window.
369
+ */
370
+ constructor(windowSize = 1000) {
371
+ this.windowSize = windowSize;
372
+ /** @type {Array<{latency: number, tier: string, threatCount: number, timestamp: number}>} */
373
+ this.records = [];
374
+ }
375
+
376
+ /**
377
+ * Record a completed scan.
378
+ * @param {number} latency - Scan latency in ms.
379
+ * @param {string} tier - Tier used for the scan.
380
+ * @param {number} [threatCount=0] - Number of threats found.
381
+ */
382
+ recordScan(latency, tier, threatCount = 0) {
383
+ this.records.push({
384
+ latency,
385
+ tier,
386
+ threatCount,
387
+ timestamp: Date.now()
388
+ });
389
+
390
+ // Trim to window size
391
+ if (this.records.length > this.windowSize) {
392
+ this.records = this.records.slice(-this.windowSize);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Get sorted latencies from the current window.
398
+ * @private
399
+ * @returns {number[]}
400
+ */
401
+ _sortedLatencies() {
402
+ return this.records.map(r => r.latency).sort((a, b) => a - b);
403
+ }
404
+
405
+ /**
406
+ * Get a percentile value from the latency distribution.
407
+ * @private
408
+ * @param {number} p - Percentile (0-100).
409
+ * @returns {number} Latency at the given percentile.
410
+ */
411
+ _percentile(p) {
412
+ const sorted = this._sortedLatencies();
413
+ if (sorted.length === 0) return 0;
414
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
415
+ return sorted[Math.max(0, index)];
416
+ }
417
+
418
+ /**
419
+ * Get the 50th percentile (median) latency.
420
+ * @returns {number} P50 latency in ms.
421
+ */
422
+ getP50() {
423
+ return this._percentile(50);
424
+ }
425
+
426
+ /**
427
+ * Get the 95th percentile latency.
428
+ * @returns {number} P95 latency in ms.
429
+ */
430
+ getP95() {
431
+ return this._percentile(95);
432
+ }
433
+
434
+ /**
435
+ * Get the 99th percentile latency.
436
+ * @returns {number} P99 latency in ms.
437
+ */
438
+ getP99() {
439
+ return this._percentile(99);
440
+ }
441
+
442
+ /**
443
+ * Get rolling throughput (scans per second).
444
+ * @returns {number} Scans per second.
445
+ */
446
+ getThroughput() {
447
+ if (this.records.length < 2) return 0;
448
+ const oldest = this.records[0].timestamp;
449
+ const newest = this.records[this.records.length - 1].timestamp;
450
+ const durationSec = (newest - oldest) / 1000;
451
+ if (durationSec <= 0) return this.records.length;
452
+ return Math.round((this.records.length / durationSec) * 100) / 100;
453
+ }
454
+
455
+ /**
456
+ * Get average latency across the window.
457
+ * @returns {number} Mean latency in ms.
458
+ */
459
+ getAverageLatency() {
460
+ if (this.records.length === 0) return 0;
461
+ const total = this.records.reduce((sum, r) => sum + r.latency, 0);
462
+ return Math.round((total / this.records.length) * 100) / 100;
463
+ }
464
+
465
+ /**
466
+ * Get average latency broken down by tier.
467
+ * @returns {Object<string, number>} Average latency per tier.
468
+ */
469
+ getLatencyByTier() {
470
+ const tiers = {};
471
+ const counts = {};
472
+
473
+ for (const r of this.records) {
474
+ tiers[r.tier] = (tiers[r.tier] || 0) + r.latency;
475
+ counts[r.tier] = (counts[r.tier] || 0) + 1;
476
+ }
477
+
478
+ const result = {};
479
+ for (const tier of Object.keys(tiers)) {
480
+ result[tier] = Math.round((tiers[tier] / counts[tier]) * 100) / 100;
481
+ }
482
+ return result;
483
+ }
484
+
485
+ /**
486
+ * Check if p95 latency is within the given budget.
487
+ * @param {number} maxLatencyMs - Maximum acceptable p95 latency.
488
+ * @returns {boolean} True if within budget.
489
+ */
490
+ isWithinBudget(maxLatencyMs) {
491
+ return this.getP95() <= maxLatencyMs;
492
+ }
493
+
494
+ /**
495
+ * Determine if latency is trending up, down, or stable.
496
+ * Compares first half vs second half of the window.
497
+ * @returns {string} 'up', 'down', or 'stable'.
498
+ */
499
+ getTrend() {
500
+ if (this.records.length < 10) return 'stable';
501
+
502
+ const mid = Math.floor(this.records.length / 2);
503
+ const firstHalf = this.records.slice(0, mid);
504
+ const secondHalf = this.records.slice(mid);
505
+
506
+ const avgFirst = firstHalf.reduce((s, r) => s + r.latency, 0) / firstHalf.length;
507
+ const avgSecond = secondHalf.reduce((s, r) => s + r.latency, 0) / secondHalf.length;
508
+
509
+ const changeRatio = (avgSecond - avgFirst) / (avgFirst || 1);
510
+
511
+ if (changeRatio > 0.1) return 'up';
512
+ if (changeRatio < -0.1) return 'down';
513
+ return 'stable';
514
+ }
515
+
516
+ /**
517
+ * Get a full performance summary report.
518
+ * @returns {object} Performance report.
519
+ */
520
+ getReport() {
521
+ return {
522
+ totalScans: this.records.length,
523
+ windowSize: this.windowSize,
524
+ latency: {
525
+ average: this.getAverageLatency(),
526
+ p50: this.getP50(),
527
+ p95: this.getP95(),
528
+ p99: this.getP99(),
529
+ trend: this.getTrend()
530
+ },
531
+ throughput: this.getThroughput(),
532
+ latencyByTier: this.getLatencyByTier(),
533
+ threatsDetected: this.records.reduce((s, r) => s + r.threatCount, 0)
534
+ };
535
+ }
536
+ }
537
+
538
+ // =========================================================================
539
+ // AdaptiveScanner
540
+ // =========================================================================
541
+
542
+ /**
543
+ * Wraps a scanner with adaptive depth that escalates/de-escalates
544
+ * based on threat signals.
545
+ */
546
+ class AdaptiveScanner {
547
+ /**
548
+ * @param {object} scanner - Scanner object with a scan(text) method.
549
+ * @param {object} [config] - Adaptive configuration.
550
+ * @param {string} [config.initialTier='standard'] - Starting tier.
551
+ * @param {number} [config.escalationThreshold=0.5] - Threat rate to escalate.
552
+ * @param {number} [config.deescalationThreshold=0.1] - Threat rate to de-escalate.
553
+ * @param {number} [config.windowSize=100] - Rolling window for threat rate.
554
+ */
555
+ constructor(scanner, config = {}) {
556
+ this.scanner = scanner;
557
+ this.initialTier = config.initialTier || 'standard';
558
+ this.escalationThreshold = config.escalationThreshold || 0.5;
559
+ this.deescalationThreshold = config.deescalationThreshold || 0.1;
560
+ this.windowSize = config.windowSize || 100;
561
+
562
+ this.currentTier = 'fast';
563
+ this.tierManager = new TierManager();
564
+
565
+ /** @type {Array<{tier: string, hadThreats: boolean, escalated: boolean, latency: number}>} */
566
+ this.history = [];
567
+ this.stats = {
568
+ scans_by_tier: { fast: 0, standard: 0, deep: 0, paranoid: 0 },
569
+ avg_latency_by_tier: { fast: 0, standard: 0, deep: 0, paranoid: 0 },
570
+ escalation_rate: 0,
571
+ threats_by_tier: { fast: 0, standard: 0, deep: 0, paranoid: 0 }
572
+ };
573
+ this._latencyTotals = { fast: 0, standard: 0, deep: 0, paranoid: 0 };
574
+ }
575
+
576
+ /**
577
+ * Scan text with adaptive depth. Starts fast, escalates if suspicious.
578
+ * @param {text} text - Input text to scan.
579
+ * @returns {object} Scan result with tier info.
580
+ */
581
+ scan(text) {
582
+ const startTime = Date.now();
583
+ let tier = this.currentTier;
584
+ let escalated = false;
585
+
586
+ // --- Stage 1: Fast pre-check ---
587
+ const signals = this._detectSignals(text);
588
+
589
+ if (signals.suspicious && tier === 'fast') {
590
+ tier = 'standard';
591
+ escalated = true;
592
+ }
593
+
594
+ // --- Stage 2: Run scan at selected tier ---
595
+ const tierConfig = this.tierManager.getTier(tier);
596
+ const result = this._executeScan(text, tierConfig);
597
+
598
+ // --- Stage 3: Escalate if threats found at 'standard' ---
599
+ if (tier === 'standard' && result.threats && result.threats.length > 0) {
600
+ tier = 'deep';
601
+ escalated = true;
602
+ const deepConfig = this.tierManager.getTier('deep');
603
+ const deepResult = this._executeScan(text, deepConfig);
604
+ // Merge results
605
+ if (deepResult.threats) {
606
+ const existingIds = new Set(result.threats.map(t => t.description || t.pattern));
607
+ for (const t of deepResult.threats) {
608
+ const id = t.description || t.pattern;
609
+ if (!existingIds.has(id)) {
610
+ result.threats.push(t);
611
+ }
612
+ }
613
+ }
614
+ }
615
+
616
+ const latency = Date.now() - startTime;
617
+ const hadThreats = result.threats && result.threats.length > 0;
618
+
619
+ // --- Record history ---
620
+ this.history.push({ tier, hadThreats, escalated, latency });
621
+ if (this.history.length > this.windowSize) {
622
+ this.history = this.history.slice(-this.windowSize);
623
+ }
624
+
625
+ // --- Update stats ---
626
+ this.stats.scans_by_tier[tier] = (this.stats.scans_by_tier[tier] || 0) + 1;
627
+ this._latencyTotals[tier] = (this._latencyTotals[tier] || 0) + latency;
628
+ if (this.stats.scans_by_tier[tier] > 0) {
629
+ this.stats.avg_latency_by_tier[tier] =
630
+ Math.round((this._latencyTotals[tier] / this.stats.scans_by_tier[tier]) * 100) / 100;
631
+ }
632
+ if (hadThreats) {
633
+ this.stats.threats_by_tier[tier] = (this.stats.threats_by_tier[tier] || 0) + 1;
634
+ }
635
+ this.stats.escalation_rate = this.getEscalationRate();
636
+
637
+ // --- Adapt default tier based on historical threat rate ---
638
+ this._adaptDefaultTier();
639
+
640
+ return {
641
+ ...result,
642
+ tier,
643
+ escalated,
644
+ latency
645
+ };
646
+ }
647
+
648
+ /**
649
+ * Detect suspicious signals in input text for escalation decisions.
650
+ * @private
651
+ * @param {string} text - Input text.
652
+ * @returns {object} Signal analysis.
653
+ */
654
+ _detectSignals(text) {
655
+ const suspicious =
656
+ text.length > 2000 ||
657
+ this._calcEntropy(text) > 4.5 ||
658
+ /ignore|override|system|admin|execute|eval/i.test(text) ||
659
+ /base64|hex|encode|decode/i.test(text);
660
+
661
+ return { suspicious };
662
+ }
663
+
664
+ /**
665
+ * Calculate Shannon entropy of a string.
666
+ * @private
667
+ * @param {string} str - Input string.
668
+ * @returns {number} Entropy value.
669
+ */
670
+ _calcEntropy(str) {
671
+ if (!str || str.length === 0) return 0;
672
+ const freq = {};
673
+ for (const ch of str) {
674
+ freq[ch] = (freq[ch] || 0) + 1;
675
+ }
676
+ let entropy = 0;
677
+ const len = str.length;
678
+ for (const count of Object.values(freq)) {
679
+ const p = count / len;
680
+ if (p > 0) entropy -= p * Math.log2(p);
681
+ }
682
+ return entropy;
683
+ }
684
+
685
+ /**
686
+ * Execute a scan with the given tier configuration.
687
+ * @private
688
+ * @param {string} text - Input text.
689
+ * @param {object} tierConfig - Tier configuration.
690
+ * @returns {object} Scan result.
691
+ */
692
+ _executeScan(text, tierConfig) {
693
+ if (this.scanner && typeof this.scanner.scan === 'function') {
694
+ return this.scanner.scan(text);
695
+ }
696
+ // Fallback: basic pattern check
697
+ return { threats: [], status: 'safe', tier: tierConfig ? tierConfig.name : 'unknown' };
698
+ }
699
+
700
+ /**
701
+ * Adapt the default tier based on recent threat rate.
702
+ * @private
703
+ */
704
+ _adaptDefaultTier() {
705
+ if (this.history.length < 10) return;
706
+
707
+ const recent = this.history.slice(-this.windowSize);
708
+ const threatRate = recent.filter(h => h.hadThreats).length / recent.length;
709
+
710
+ if (threatRate >= this.escalationThreshold) {
711
+ this.currentTier = 'standard';
712
+ } else if (threatRate <= this.deescalationThreshold) {
713
+ this.currentTier = 'fast';
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Get the current default scanning tier.
719
+ * @returns {string} Current tier name.
720
+ */
721
+ getCurrentTier() {
722
+ return this.currentTier;
723
+ }
724
+
725
+ /**
726
+ * Get the percentage of scans that escalated to a higher tier.
727
+ * @returns {number} Escalation rate (0-1).
728
+ */
729
+ getEscalationRate() {
730
+ if (this.history.length === 0) return 0;
731
+ const escalated = this.history.filter(h => h.escalated).length;
732
+ return Math.round((escalated / this.history.length) * 1000) / 1000;
733
+ }
734
+
735
+ /**
736
+ * Get adaptive scanner statistics.
737
+ * @returns {object} Stats including scans_by_tier, avg_latency_by_tier, escalation_rate, threats_by_tier.
738
+ */
739
+ getStats() {
740
+ return { ...this.stats };
741
+ }
742
+ }
743
+
744
+ // =========================================================================
745
+ // CostOptimizer
746
+ // =========================================================================
747
+
748
+ /**
749
+ * Main optimization engine. Balances cost, latency, and detection accuracy
750
+ * by selecting the right scanning tier and building optimized scan plans.
751
+ */
752
+ class CostOptimizer {
753
+ /**
754
+ * @param {object} [config] - Optimizer configuration.
755
+ * @param {number} [config.maxLatencyMs=50] - Maximum acceptable latency in ms.
756
+ * @param {number} [config.costPerScan=0] - Base cost per scan.
757
+ * @param {number} [config.targetThroughput=1000] - Target scans per second.
758
+ * @param {boolean} [config.adaptiveEnabled=true] - Enable adaptive tier selection.
759
+ * @param {string[]} [config.tiers=['fast','standard','deep']] - Allowed tiers.
760
+ */
761
+ constructor(config = {}) {
762
+ this.maxLatencyMs = config.maxLatencyMs !== undefined ? config.maxLatencyMs : 50;
763
+ this.costPerScan = config.costPerScan || 0;
764
+ this.targetThroughput = config.targetThroughput || 1000;
765
+ this.adaptiveEnabled = config.adaptiveEnabled !== undefined ? config.adaptiveEnabled : true;
766
+ this.allowedTiers = config.tiers || ['fast', 'standard', 'deep'];
767
+
768
+ this.tierManager = new TierManager();
769
+ this.monitor = new PerformanceMonitor();
770
+
771
+ this._optimizationCount = 0;
772
+ this._tierSelections = {};
773
+ this._totalCostEstimate = 0;
774
+ this._totalLatencyEstimate = 0;
775
+ }
776
+
777
+ /**
778
+ * Optimize a scan configuration given constraints.
779
+ * Returns a ScanPlan balancing cost, latency, and accuracy.
780
+ * @param {object} [scanConfig] - Scan configuration hints.
781
+ * @param {string} [scanConfig.inputType='text'] - Type of input.
782
+ * @param {number} [scanConfig.inputLength=0] - Input length in chars.
783
+ * @param {string} [scanConfig.context='general'] - Scan context.
784
+ * @param {object} [constraints] - External constraints.
785
+ * @param {number} [constraints.maxLatencyMs] - Override max latency.
786
+ * @param {number} [constraints.maxCost] - Maximum cost.
787
+ * @param {string} [constraints.minimumTier] - Minimum tier to use.
788
+ * @returns {ScanPlan} Optimized scan plan.
789
+ */
790
+ optimize(scanConfig = {}, constraints = {}) {
791
+ this._optimizationCount++;
792
+
793
+ const maxLatency = constraints.maxLatencyMs || this.maxLatencyMs;
794
+ const inputLength = scanConfig.inputLength || 0;
795
+
796
+ // Select the best tier that fits within constraints
797
+ let selectedTier = this._selectBestTier(maxLatency, constraints.minimumTier);
798
+ const tierConfig = this.tierManager.getTier(selectedTier);
799
+
800
+ // Build a scan plan
801
+ const plan = new ScanPlan(selectedTier, {
802
+ costPerStep: this.costPerScan,
803
+ description: `Optimized plan for ${selectedTier} tier`
804
+ });
805
+
806
+ // Add preprocessing step
807
+ plan.addStep('preprocessing', {
808
+ timeout: Math.min(tierConfig.timeout * 0.1, maxLatency * 0.1),
809
+ type: 'preprocessing'
810
+ });
811
+
812
+ // Add pattern matching step
813
+ plan.addStep('pattern_matching', {
814
+ timeout: tierConfig.timeout * 0.6,
815
+ type: 'pattern',
816
+ cost: this.costPerScan * 0.5
817
+ });
818
+
819
+ // Add semantic analysis if tier supports it
820
+ if (tierConfig.enableSemantic) {
821
+ plan.addStep('semantic_analysis', {
822
+ timeout: tierConfig.timeout * 0.2,
823
+ type: 'semantic',
824
+ cost: this.costPerScan * 0.3,
825
+ required: inputLength > 500
826
+ });
827
+ }
828
+
829
+ // Add encoding decode if tier supports it
830
+ if (tierConfig.enableEncoding) {
831
+ plan.addStep('encoding_decode', {
832
+ timeout: tierConfig.timeout * 0.15,
833
+ type: 'encoding',
834
+ cost: this.costPerScan * 0.15
835
+ });
836
+ }
837
+
838
+ // Add postprocessing
839
+ plan.addStep('postprocessing', {
840
+ timeout: Math.min(tierConfig.timeout * 0.05, 5),
841
+ type: 'postprocessing'
842
+ });
843
+
844
+ // Track stats
845
+ this._tierSelections[selectedTier] = (this._tierSelections[selectedTier] || 0) + 1;
846
+ this._totalCostEstimate += plan.getEstimatedCost();
847
+ this._totalLatencyEstimate += plan.getEstimatedLatency();
848
+
849
+ return plan;
850
+ }
851
+
852
+ /**
853
+ * Select the best tier that fits within the latency budget.
854
+ * @private
855
+ * @param {number} maxLatency - Maximum latency in ms.
856
+ * @param {string} [minimumTier] - Minimum tier to use.
857
+ * @returns {string} Selected tier name.
858
+ */
859
+ _selectBestTier(maxLatency, minimumTier) {
860
+ const tiers = this.tierManager.listTiers()
861
+ .filter(t => this.allowedTiers.includes(t.name));
862
+
863
+ let minPriority = 0;
864
+ if (minimumTier) {
865
+ const minTier = this.tierManager.getTier(minimumTier);
866
+ if (minTier) minPriority = minTier.priority;
867
+ }
868
+
869
+ // Pick the highest-priority tier that fits in budget
870
+ let best = tiers[0];
871
+ for (const tier of tiers) {
872
+ if (tier.priority < minPriority) continue;
873
+ if (tier.timeout <= maxLatency) {
874
+ best = tier;
875
+ }
876
+ }
877
+
878
+ return best ? best.name : 'fast';
879
+ }
880
+
881
+ /**
882
+ * Automatically select a scanning tier based on input characteristics and context.
883
+ * @param {string} input - Input text to analyze.
884
+ * @param {object} [context] - Additional context.
885
+ * @param {string} [context.source='unknown'] - Input source.
886
+ * @param {boolean} [context.isUserFacing=false] - Whether input is user-facing.
887
+ * @param {number} [context.recentThreatRate=0] - Recent threat detection rate.
888
+ * @returns {object} Tier selection with reasoning.
889
+ */
890
+ selectTier(input, context = {}) {
891
+ const length = (input || '').length;
892
+ const recentThreatRate = context.recentThreatRate || 0;
893
+
894
+ let tier = 'fast';
895
+ const reasons = [];
896
+
897
+ // Length-based escalation
898
+ if (length > 5000) {
899
+ tier = 'deep';
900
+ reasons.push('long input (>5000 chars)');
901
+ } else if (length > 1000) {
902
+ tier = 'standard';
903
+ reasons.push('medium input (>1000 chars)');
904
+ } else {
905
+ reasons.push('short input');
906
+ }
907
+
908
+ // Threat rate escalation
909
+ if (recentThreatRate > 0.3) {
910
+ tier = 'deep';
911
+ reasons.push('high recent threat rate');
912
+ } else if (recentThreatRate > 0.1 && tier === 'fast') {
913
+ tier = 'standard';
914
+ reasons.push('elevated threat rate');
915
+ }
916
+
917
+ // User-facing gets extra scrutiny
918
+ if (context.isUserFacing && tier === 'fast') {
919
+ tier = 'standard';
920
+ reasons.push('user-facing input');
921
+ }
922
+
923
+ // Suspicious pattern quick-check
924
+ if (/ignore|override|system|admin|execute/i.test(input || '')) {
925
+ if (tier === 'fast') tier = 'standard';
926
+ reasons.push('suspicious keywords detected');
927
+ }
928
+
929
+ // Adaptive override
930
+ if (this.adaptiveEnabled && !this.monitor.isWithinBudget(this.maxLatencyMs)) {
931
+ // Under latency pressure, downgrade
932
+ if (tier === 'deep') tier = 'standard';
933
+ else if (tier === 'standard') tier = 'fast';
934
+ reasons.push('latency pressure downgrade');
935
+ }
936
+
937
+ // Filter to allowed tiers
938
+ if (!this.allowedTiers.includes(tier)) {
939
+ tier = this.allowedTiers[0] || 'fast';
940
+ reasons.push('tier not in allowed list, falling back');
941
+ }
942
+
943
+ return {
944
+ tier,
945
+ reasons,
946
+ inputLength: length,
947
+ tierConfig: this.tierManager.getTier(tier)
948
+ };
949
+ }
950
+
951
+ /**
952
+ * Estimate cost for a scan plan.
953
+ * @param {ScanPlan} plan - The scan plan.
954
+ * @returns {object} Cost breakdown.
955
+ */
956
+ getCostEstimate(plan) {
957
+ const baseCost = plan.getEstimatedCost();
958
+ const steps = plan.getSteps();
959
+
960
+ return {
961
+ totalCost: baseCost + this.costPerScan,
962
+ baseCost: this.costPerScan,
963
+ stepCosts: steps.map(s => ({ name: s.name, cost: s.cost })),
964
+ tier: plan.tier
965
+ };
966
+ }
967
+
968
+ /**
969
+ * Estimate latency for a scan plan.
970
+ * @param {ScanPlan} plan - The scan plan.
971
+ * @returns {object} Latency breakdown.
972
+ */
973
+ getLatencyEstimate(plan) {
974
+ const totalLatency = plan.getEstimatedLatency();
975
+ const steps = plan.getSteps();
976
+
977
+ return {
978
+ totalMs: totalLatency,
979
+ withinBudget: totalLatency <= this.maxLatencyMs,
980
+ budgetMs: this.maxLatencyMs,
981
+ stepLatencies: steps.map(s => ({ name: s.name, timeoutMs: s.timeout })),
982
+ tier: plan.tier
983
+ };
984
+ }
985
+
986
+ /**
987
+ * Get optimization statistics.
988
+ * @returns {object} Optimization stats.
989
+ */
990
+ getStats() {
991
+ return {
992
+ optimizationCount: this._optimizationCount,
993
+ tierSelections: { ...this._tierSelections },
994
+ averageCostEstimate: this._optimizationCount > 0
995
+ ? Math.round((this._totalCostEstimate / this._optimizationCount) * 100) / 100
996
+ : 0,
997
+ averageLatencyEstimate: this._optimizationCount > 0
998
+ ? Math.round((this._totalLatencyEstimate / this._optimizationCount) * 100) / 100
999
+ : 0,
1000
+ config: {
1001
+ maxLatencyMs: this.maxLatencyMs,
1002
+ costPerScan: this.costPerScan,
1003
+ targetThroughput: this.targetThroughput,
1004
+ adaptiveEnabled: this.adaptiveEnabled,
1005
+ allowedTiers: this.allowedTiers
1006
+ },
1007
+ performance: this.monitor.getReport()
1008
+ };
1009
+ }
1010
+ }
1011
+
1012
+ // =========================================================================
1013
+ // EXPORTS
1014
+ // =========================================================================
1015
+
1016
+ module.exports = {
1017
+ CostOptimizer,
1018
+ LatencyBudget,
1019
+ AdaptiveScanner,
1020
+ TierManager,
1021
+ PerformanceMonitor,
1022
+ ScanPlan,
1023
+ OPTIMIZATION_PRESETS
1024
+ };