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