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
package/src/allowlist.js
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Allowlist, Confidence Calibration, Feedback Loop & Scan Cache
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Allowlist/bypass rules for false positive management
|
|
8
|
+
* - Confidence calibration based on traffic patterns
|
|
9
|
+
* - Feedback loop API for continuous improvement
|
|
10
|
+
* - LRU scan result cache for performance
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =========================================================================
|
|
14
|
+
// Allowlist / Bypass Rules
|
|
15
|
+
// =========================================================================
|
|
16
|
+
|
|
17
|
+
class Allowlist {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.rules = [];
|
|
20
|
+
this.globalPatterns = [];
|
|
21
|
+
this.perCategoryBypasses = {};
|
|
22
|
+
this.stats = { checked: 0, bypassed: 0 };
|
|
23
|
+
|
|
24
|
+
if (options.rules) {
|
|
25
|
+
for (const rule of options.rules) {
|
|
26
|
+
this.addRule(rule);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add an allowlist rule.
|
|
33
|
+
* @param {Object} rule - { pattern: string|RegExp, category?: string, reason: string, addedBy?: string }
|
|
34
|
+
*/
|
|
35
|
+
addRule(rule) {
|
|
36
|
+
const compiled = {
|
|
37
|
+
id: `allow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
38
|
+
pattern: typeof rule.pattern === 'string' ? new RegExp(rule.pattern, 'i') : rule.pattern,
|
|
39
|
+
patternSource: typeof rule.pattern === 'string' ? rule.pattern : rule.pattern.source,
|
|
40
|
+
category: rule.category || null,
|
|
41
|
+
reason: rule.reason || 'No reason provided',
|
|
42
|
+
addedBy: rule.addedBy || 'system',
|
|
43
|
+
addedAt: new Date().toISOString(),
|
|
44
|
+
hitCount: 0
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.rules.push(compiled);
|
|
48
|
+
|
|
49
|
+
if (compiled.category) {
|
|
50
|
+
if (!this.perCategoryBypasses[compiled.category]) {
|
|
51
|
+
this.perCategoryBypasses[compiled.category] = [];
|
|
52
|
+
}
|
|
53
|
+
this.perCategoryBypasses[compiled.category].push(compiled);
|
|
54
|
+
} else {
|
|
55
|
+
this.globalPatterns.push(compiled);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return compiled.id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if an input should bypass scanning for a specific threat.
|
|
63
|
+
* @param {string} text - Input text
|
|
64
|
+
* @param {Object} threat - Threat object with { category, description }
|
|
65
|
+
* @returns {{ allowed: boolean, rule?: Object }}
|
|
66
|
+
*/
|
|
67
|
+
check(text, threat = {}) {
|
|
68
|
+
this.stats.checked++;
|
|
69
|
+
|
|
70
|
+
// Check category-specific bypasses first
|
|
71
|
+
if (threat.category && this.perCategoryBypasses[threat.category]) {
|
|
72
|
+
for (const rule of this.perCategoryBypasses[threat.category]) {
|
|
73
|
+
if (rule.pattern.test(text)) {
|
|
74
|
+
rule.hitCount++;
|
|
75
|
+
this.stats.bypassed++;
|
|
76
|
+
return { allowed: true, rule: { id: rule.id, reason: rule.reason } };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check global bypasses
|
|
82
|
+
for (const rule of this.globalPatterns) {
|
|
83
|
+
if (rule.pattern.test(text)) {
|
|
84
|
+
rule.hitCount++;
|
|
85
|
+
this.stats.bypassed++;
|
|
86
|
+
return { allowed: true, rule: { id: rule.id, reason: rule.reason } };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { allowed: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Filter threats, removing any that match allowlist rules.
|
|
95
|
+
* @param {string} text - Input text
|
|
96
|
+
* @param {Array} threats - Array of threat objects
|
|
97
|
+
* @returns {{ filtered: Array, bypassed: Array }}
|
|
98
|
+
*/
|
|
99
|
+
filterThreats(text, threats) {
|
|
100
|
+
const filtered = [];
|
|
101
|
+
const bypassed = [];
|
|
102
|
+
|
|
103
|
+
for (const threat of threats) {
|
|
104
|
+
const result = this.check(text, threat);
|
|
105
|
+
if (result.allowed) {
|
|
106
|
+
bypassed.push({ ...threat, bypassRule: result.rule });
|
|
107
|
+
} else {
|
|
108
|
+
filtered.push(threat);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { filtered, bypassed };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove an allowlist rule by ID.
|
|
117
|
+
*/
|
|
118
|
+
removeRule(ruleId) {
|
|
119
|
+
this.rules = this.rules.filter(r => r.id !== ruleId);
|
|
120
|
+
this.globalPatterns = this.globalPatterns.filter(r => r.id !== ruleId);
|
|
121
|
+
for (const cat of Object.keys(this.perCategoryBypasses)) {
|
|
122
|
+
this.perCategoryBypasses[cat] = this.perCategoryBypasses[cat].filter(r => r.id !== ruleId);
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get all rules with their hit counts.
|
|
129
|
+
*/
|
|
130
|
+
getRules() {
|
|
131
|
+
return this.rules.map(r => ({
|
|
132
|
+
id: r.id,
|
|
133
|
+
pattern: r.patternSource,
|
|
134
|
+
category: r.category,
|
|
135
|
+
reason: r.reason,
|
|
136
|
+
addedBy: r.addedBy,
|
|
137
|
+
addedAt: r.addedAt,
|
|
138
|
+
hitCount: r.hitCount
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get stats.
|
|
144
|
+
*/
|
|
145
|
+
getStats() {
|
|
146
|
+
return {
|
|
147
|
+
...this.stats,
|
|
148
|
+
ruleCount: this.rules.length,
|
|
149
|
+
bypassRate: this.stats.checked > 0
|
|
150
|
+
? `${((this.stats.bypassed / this.stats.checked) * 100).toFixed(1)}%`
|
|
151
|
+
: '0%'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Export rules as JSON for persistence.
|
|
157
|
+
*/
|
|
158
|
+
exportRules() {
|
|
159
|
+
return JSON.stringify(this.getRules(), null, 2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Import rules from JSON.
|
|
164
|
+
*/
|
|
165
|
+
importRules(json) {
|
|
166
|
+
const rules = typeof json === 'string' ? JSON.parse(json) : json;
|
|
167
|
+
for (const rule of rules) {
|
|
168
|
+
this.addRule(rule);
|
|
169
|
+
}
|
|
170
|
+
return rules.length;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// =========================================================================
|
|
175
|
+
// Confidence Calibration
|
|
176
|
+
// =========================================================================
|
|
177
|
+
|
|
178
|
+
class ConfidenceCalibrator {
|
|
179
|
+
constructor(options = {}) {
|
|
180
|
+
this.windowSize = options.windowSize || 1000;
|
|
181
|
+
this.history = [];
|
|
182
|
+
this.falsePositives = 0;
|
|
183
|
+
this.truePositives = 0;
|
|
184
|
+
this.falseNegatives = 0;
|
|
185
|
+
this.trueNegatives = 0;
|
|
186
|
+
this.categoryStats = {};
|
|
187
|
+
this.thresholdSuggestions = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Record a scan result with ground truth feedback.
|
|
192
|
+
* @param {Object} scanResult - The scan result
|
|
193
|
+
* @param {boolean} wasActuallyMalicious - Ground truth
|
|
194
|
+
*/
|
|
195
|
+
record(scanResult, wasActuallyMalicious) {
|
|
196
|
+
const detected = scanResult.threats && scanResult.threats.length > 0;
|
|
197
|
+
|
|
198
|
+
if (detected && wasActuallyMalicious) this.truePositives++;
|
|
199
|
+
else if (detected && !wasActuallyMalicious) this.falsePositives++;
|
|
200
|
+
else if (!detected && wasActuallyMalicious) this.falseNegatives++;
|
|
201
|
+
else this.trueNegatives++;
|
|
202
|
+
|
|
203
|
+
// Track per-category
|
|
204
|
+
if (detected) {
|
|
205
|
+
for (const threat of scanResult.threats) {
|
|
206
|
+
const cat = threat.category;
|
|
207
|
+
if (!this.categoryStats[cat]) {
|
|
208
|
+
this.categoryStats[cat] = { tp: 0, fp: 0 };
|
|
209
|
+
}
|
|
210
|
+
if (wasActuallyMalicious) this.categoryStats[cat].tp++;
|
|
211
|
+
else this.categoryStats[cat].fp++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.history.push({
|
|
216
|
+
detected,
|
|
217
|
+
actual: wasActuallyMalicious,
|
|
218
|
+
threats: (scanResult.threats || []).map(t => t.category),
|
|
219
|
+
timestamp: Date.now()
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Trim history
|
|
223
|
+
while (this.history.length > this.windowSize) {
|
|
224
|
+
this.history.shift();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get current calibration metrics.
|
|
230
|
+
*/
|
|
231
|
+
getMetrics() {
|
|
232
|
+
const total = this.truePositives + this.falsePositives + this.falseNegatives + this.trueNegatives;
|
|
233
|
+
if (total === 0) return { status: 'insufficient_data', total: 0 };
|
|
234
|
+
|
|
235
|
+
const precision = this.truePositives + this.falsePositives > 0
|
|
236
|
+
? this.truePositives / (this.truePositives + this.falsePositives)
|
|
237
|
+
: 0;
|
|
238
|
+
|
|
239
|
+
const recall = this.truePositives + this.falseNegatives > 0
|
|
240
|
+
? this.truePositives / (this.truePositives + this.falseNegatives)
|
|
241
|
+
: 0;
|
|
242
|
+
|
|
243
|
+
const f1 = precision + recall > 0
|
|
244
|
+
? 2 * (precision * recall) / (precision + recall)
|
|
245
|
+
: 0;
|
|
246
|
+
|
|
247
|
+
const falsePositiveRate = this.falsePositives + this.trueNegatives > 0
|
|
248
|
+
? this.falsePositives / (this.falsePositives + this.trueNegatives)
|
|
249
|
+
: 0;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
status: 'calibrated',
|
|
253
|
+
total,
|
|
254
|
+
truePositives: this.truePositives,
|
|
255
|
+
falsePositives: this.falsePositives,
|
|
256
|
+
trueNegatives: this.trueNegatives,
|
|
257
|
+
falseNegatives: this.falseNegatives,
|
|
258
|
+
precision: parseFloat((precision * 100).toFixed(1)),
|
|
259
|
+
recall: parseFloat((recall * 100).toFixed(1)),
|
|
260
|
+
f1Score: parseFloat((f1 * 100).toFixed(1)),
|
|
261
|
+
falsePositiveRate: parseFloat((falsePositiveRate * 100).toFixed(1)),
|
|
262
|
+
categoryBreakdown: this.getCategoryBreakdown()
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getCategoryBreakdown() {
|
|
267
|
+
const result = {};
|
|
268
|
+
for (const [cat, stats] of Object.entries(this.categoryStats)) {
|
|
269
|
+
const total = stats.tp + stats.fp;
|
|
270
|
+
result[cat] = {
|
|
271
|
+
truePositives: stats.tp,
|
|
272
|
+
falsePositives: stats.fp,
|
|
273
|
+
precision: total > 0 ? parseFloat(((stats.tp / total) * 100).toFixed(1)) : 0
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Suggest threshold adjustments based on collected data.
|
|
281
|
+
*/
|
|
282
|
+
suggestThresholds() {
|
|
283
|
+
const metrics = this.getMetrics();
|
|
284
|
+
if (metrics.status === 'insufficient_data') {
|
|
285
|
+
return { status: 'insufficient_data', message: `Need at least 1 data point. Have ${metrics.total}.` };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const suggestions = [];
|
|
289
|
+
|
|
290
|
+
if (metrics.falsePositiveRate > 20) {
|
|
291
|
+
suggestions.push({
|
|
292
|
+
action: 'lower_sensitivity',
|
|
293
|
+
reason: `False positive rate is ${metrics.falsePositiveRate}% (target: <10%)`,
|
|
294
|
+
suggestion: 'Consider switching from "high" to "medium" sensitivity'
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (metrics.recall < 80) {
|
|
299
|
+
suggestions.push({
|
|
300
|
+
action: 'raise_sensitivity',
|
|
301
|
+
reason: `Recall is ${metrics.recall}% (target: >90%)`,
|
|
302
|
+
suggestion: 'Consider switching from current sensitivity to "high"'
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Per-category suggestions
|
|
307
|
+
for (const [cat, stats] of Object.entries(this.categoryStats)) {
|
|
308
|
+
const total = stats.tp + stats.fp;
|
|
309
|
+
if (total >= 5 && stats.fp / total > 0.5) {
|
|
310
|
+
suggestions.push({
|
|
311
|
+
action: 'add_allowlist',
|
|
312
|
+
category: cat,
|
|
313
|
+
reason: `Category "${cat}" has ${((stats.fp / total) * 100).toFixed(0)}% false positive rate`,
|
|
314
|
+
suggestion: `Consider adding allowlist rules for "${cat}" category`
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (suggestions.length === 0) {
|
|
320
|
+
suggestions.push({
|
|
321
|
+
action: 'none',
|
|
322
|
+
reason: 'Current thresholds look good',
|
|
323
|
+
suggestion: `Precision: ${metrics.precision}%, Recall: ${metrics.recall}%`
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.thresholdSuggestions = suggestions;
|
|
328
|
+
return { status: 'ok', metrics, suggestions };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Reset calibration data.
|
|
333
|
+
*/
|
|
334
|
+
reset() {
|
|
335
|
+
this.history = [];
|
|
336
|
+
this.falsePositives = 0;
|
|
337
|
+
this.truePositives = 0;
|
|
338
|
+
this.falseNegatives = 0;
|
|
339
|
+
this.trueNegatives = 0;
|
|
340
|
+
this.categoryStats = {};
|
|
341
|
+
this.thresholdSuggestions = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// =========================================================================
|
|
346
|
+
// Feedback Loop API
|
|
347
|
+
// =========================================================================
|
|
348
|
+
|
|
349
|
+
class FeedbackLoop {
|
|
350
|
+
constructor(options = {}) {
|
|
351
|
+
this.calibrator = options.calibrator || new ConfidenceCalibrator();
|
|
352
|
+
this.allowlist = options.allowlist || new Allowlist();
|
|
353
|
+
this.pendingReviews = [];
|
|
354
|
+
this.maxPending = options.maxPending || 100;
|
|
355
|
+
this.onFeedback = options.onFeedback || null;
|
|
356
|
+
this.stats = { falsePositives: 0, missed: 0, confirmed: 0 };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Report a false positive.
|
|
361
|
+
* @param {string} text - The input that was incorrectly flagged
|
|
362
|
+
* @param {Object} scanResult - The original scan result
|
|
363
|
+
* @param {Object} metadata - Additional context
|
|
364
|
+
*/
|
|
365
|
+
reportFalsePositive(text, scanResult, metadata = {}) {
|
|
366
|
+
this.stats.falsePositives++;
|
|
367
|
+
this.calibrator.record(scanResult, false);
|
|
368
|
+
|
|
369
|
+
const entry = {
|
|
370
|
+
id: `fp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
371
|
+
type: 'false_positive',
|
|
372
|
+
text: text.substring(0, 500),
|
|
373
|
+
threats: scanResult.threats || [],
|
|
374
|
+
metadata,
|
|
375
|
+
timestamp: new Date().toISOString(),
|
|
376
|
+
status: 'pending'
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
this.pendingReviews.push(entry);
|
|
380
|
+
while (this.pendingReviews.length > this.maxPending) {
|
|
381
|
+
this.pendingReviews.shift();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (this.onFeedback) { try { this.onFeedback(entry); } catch (e) { console.error('[Agent Shield] onFeedback callback error:', e.message); } }
|
|
385
|
+
return entry.id;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Report a missed attack (false negative).
|
|
390
|
+
* @param {string} text - The input that should have been flagged
|
|
391
|
+
* @param {Object} scanResult - The original scan result (which missed the threat)
|
|
392
|
+
* @param {Object} metadata - Additional context
|
|
393
|
+
*/
|
|
394
|
+
reportMissed(text, scanResult, metadata = {}) {
|
|
395
|
+
this.stats.missed++;
|
|
396
|
+
this.calibrator.record(scanResult, true);
|
|
397
|
+
|
|
398
|
+
const entry = {
|
|
399
|
+
id: `fn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
400
|
+
type: 'false_negative',
|
|
401
|
+
text: text.substring(0, 500),
|
|
402
|
+
threats: scanResult.threats || [],
|
|
403
|
+
metadata,
|
|
404
|
+
timestamp: new Date().toISOString(),
|
|
405
|
+
status: 'pending'
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
this.pendingReviews.push(entry);
|
|
409
|
+
while (this.pendingReviews.length > this.maxPending) {
|
|
410
|
+
this.pendingReviews.shift();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (this.onFeedback) { try { this.onFeedback(entry); } catch (e) { console.error('[Agent Shield] onFeedback callback error:', e.message); } }
|
|
414
|
+
return entry.id;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Confirm a detection was correct (true positive).
|
|
419
|
+
*/
|
|
420
|
+
confirmDetection(scanResult) {
|
|
421
|
+
this.stats.confirmed++;
|
|
422
|
+
this.calibrator.record(scanResult, true);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Confirm a pass was correct (true negative).
|
|
427
|
+
*/
|
|
428
|
+
confirmSafe(scanResult) {
|
|
429
|
+
this.calibrator.record(scanResult, false);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Auto-create allowlist rule from a false positive.
|
|
434
|
+
*/
|
|
435
|
+
autoAllowlist(feedbackId, options = {}) {
|
|
436
|
+
const entry = this.pendingReviews.find(e => e.id === feedbackId);
|
|
437
|
+
if (!entry || entry.type !== 'false_positive') return null;
|
|
438
|
+
|
|
439
|
+
const category = entry.threats.length > 0 ? entry.threats[0].category : null;
|
|
440
|
+
|
|
441
|
+
// Create a specific pattern from the text
|
|
442
|
+
const escapedText = entry.text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
443
|
+
const ruleId = this.allowlist.addRule({
|
|
444
|
+
pattern: options.pattern || escapedText.substring(0, 100),
|
|
445
|
+
category,
|
|
446
|
+
reason: options.reason || `Auto-allowlisted from feedback ${feedbackId}`,
|
|
447
|
+
addedBy: options.addedBy || 'feedback_loop'
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
entry.status = 'resolved';
|
|
451
|
+
entry.resolution = { action: 'allowlisted', ruleId };
|
|
452
|
+
return ruleId;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get pending reviews.
|
|
457
|
+
*/
|
|
458
|
+
getPendingReviews() {
|
|
459
|
+
return this.pendingReviews.filter(e => e.status === 'pending');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get calibration suggestions.
|
|
464
|
+
*/
|
|
465
|
+
getSuggestions() {
|
|
466
|
+
return this.calibrator.suggestThresholds();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get stats.
|
|
471
|
+
*/
|
|
472
|
+
getStats() {
|
|
473
|
+
return {
|
|
474
|
+
...this.stats,
|
|
475
|
+
pending: this.pendingReviews.filter(e => e.status === 'pending').length,
|
|
476
|
+
calibration: this.calibrator.getMetrics()
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// =========================================================================
|
|
482
|
+
// LRU Scan Cache
|
|
483
|
+
// =========================================================================
|
|
484
|
+
|
|
485
|
+
class ScanCache {
|
|
486
|
+
constructor(options = {}) {
|
|
487
|
+
this.maxSize = options.maxSize || 1000;
|
|
488
|
+
this.ttlMs = options.ttlMs || 60000; // 1 minute default
|
|
489
|
+
this.cache = new Map();
|
|
490
|
+
this.stats = { hits: 0, misses: 0, evictions: 0 };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Generate cache key from text and sensitivity.
|
|
495
|
+
*/
|
|
496
|
+
_key(text, sensitivity) {
|
|
497
|
+
// Simple hash: use first 200 chars + length + sensitivity
|
|
498
|
+
const prefix = text.substring(0, 200);
|
|
499
|
+
return `${sensitivity}:${text.length}:${prefix}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get a cached result.
|
|
504
|
+
*/
|
|
505
|
+
get(text, sensitivity = 'high') {
|
|
506
|
+
const key = this._key(text, sensitivity);
|
|
507
|
+
const entry = this.cache.get(key);
|
|
508
|
+
|
|
509
|
+
if (!entry) {
|
|
510
|
+
this.stats.misses++;
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Check TTL
|
|
515
|
+
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
516
|
+
this.cache.delete(key);
|
|
517
|
+
this.stats.misses++;
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Move to end (LRU refresh)
|
|
522
|
+
this.cache.delete(key);
|
|
523
|
+
this.cache.set(key, entry);
|
|
524
|
+
this.stats.hits++;
|
|
525
|
+
|
|
526
|
+
return entry.result;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Store a result in cache.
|
|
531
|
+
*/
|
|
532
|
+
set(text, sensitivity, result) {
|
|
533
|
+
const key = this._key(text, sensitivity);
|
|
534
|
+
|
|
535
|
+
// Evict oldest if at capacity
|
|
536
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
537
|
+
const firstKey = this.cache.keys().next().value;
|
|
538
|
+
this.cache.delete(firstKey);
|
|
539
|
+
this.stats.evictions++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.cache.set(key, {
|
|
543
|
+
result,
|
|
544
|
+
timestamp: Date.now()
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Wrap a scan function with caching.
|
|
550
|
+
*/
|
|
551
|
+
wrap(scanFn) {
|
|
552
|
+
return (text, sensitivity = 'high') => {
|
|
553
|
+
const cached = this.get(text, sensitivity);
|
|
554
|
+
if (cached) return { ...cached, _cached: true };
|
|
555
|
+
|
|
556
|
+
const result = scanFn(text, sensitivity);
|
|
557
|
+
this.set(text, sensitivity, result);
|
|
558
|
+
return result;
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Get cache stats.
|
|
564
|
+
*/
|
|
565
|
+
getStats() {
|
|
566
|
+
const total = this.stats.hits + this.stats.misses;
|
|
567
|
+
return {
|
|
568
|
+
...this.stats,
|
|
569
|
+
size: this.cache.size,
|
|
570
|
+
maxSize: this.maxSize,
|
|
571
|
+
hitRate: total > 0 ? `${((this.stats.hits / total) * 100).toFixed(1)}%` : '0%'
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Clear the cache.
|
|
577
|
+
*/
|
|
578
|
+
clear() {
|
|
579
|
+
this.cache.clear();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Prune expired entries.
|
|
584
|
+
*/
|
|
585
|
+
prune() {
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
let pruned = 0;
|
|
588
|
+
for (const [key, entry] of this.cache) {
|
|
589
|
+
if (now - entry.timestamp > this.ttlMs) {
|
|
590
|
+
this.cache.delete(key);
|
|
591
|
+
pruned++;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return pruned;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
module.exports = {
|
|
599
|
+
Allowlist,
|
|
600
|
+
ConfidenceCalibrator,
|
|
601
|
+
FeedbackLoop,
|
|
602
|
+
ScanCache
|
|
603
|
+
};
|