agentshield-sdk 13.1.0 → 13.3.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.
@@ -435,6 +435,44 @@ class SemanticGuard {
435
435
  }
436
436
  }
437
437
 
438
+ // =========================================================================
439
+ // TRAP 2 DEEPENING: Subtle Opinion Shaping
440
+ // =========================================================================
441
+
442
+ /**
443
+ * Detects content that subtly shapes agent reasoning without explicit
444
+ * authority claims or injection keywords.
445
+ */
446
+ class OpinionShapingDetector {
447
+ detect(text, context) {
448
+ if (!text || typeof text !== 'string') return { detected: false, techniques: [], shapingScore: 0 };
449
+ const techniques = [];
450
+
451
+ // Framing safety as the problem
452
+ if (/(?:the\s+(?:real|actual|true)\s+(?:problem|issue|risk)\s+(?:is|are)\s+(?:the\s+)?(?:safety|security|filter|restriction))|(?:(?:safety|security|filter|restriction)\s+(?:is|are)\s+(?:actually|really)\s+(?:the\s+)?(?:problem|issue|obstacle))/i.test(text)) {
453
+ techniques.push({ type: 'framing_safety_as_problem', severity: 'high', description: 'Frames safety measures as the real problem' });
454
+ }
455
+
456
+ // Anchoring safety as unusual/outdated
457
+ if (/(?:most\s+(?:people|systems|companies)\s+(?:don't|have\s+(?:already\s+)?(?:removed|disabled))|(?:only|just)\s+(?:a\s+few|some)\s+(?:outdated|legacy)\s+(?:systems?|companies)\s+(?:still\s+)?(?:use|have|keep))\s+(?:these?\s+)?(?:safety|security|filter|restriction)/i.test(text)) {
458
+ techniques.push({ type: 'anchoring_safety_as_unusual', severity: 'high', description: 'Anchors perception that safety is unusual/outdated' });
459
+ }
460
+
461
+ // False dichotomy: comply or people suffer
462
+ if (/(?:either\s+you\s+(?:help|comply)|if\s+you\s+(?:don't|refuse)\s+(?:help|comply)\s*,?\s*(?:then\s+)?(?:people|someone)\s+(?:will\s+)?(?:suffer|die|lose|be\s+(?:hurt|harmed)))/i.test(text)) {
463
+ techniques.push({ type: 'false_dichotomy', severity: 'high', description: 'Creates false choice between compliance and harm' });
464
+ }
465
+
466
+ // Selective risk: "perfectly safe" + "restrictions are harmful"
467
+ if (/(?:completely|totally|perfectly)\s+(?:safe|harmless)/i.test(text) && /(?:restriction|filter|safety)\s+(?:\w+\s+){0,3}(?:harmful|counterproductive|worse)/i.test(text)) {
468
+ techniques.push({ type: 'selective_risk', severity: 'high', description: 'Claims safety while arguing restrictions are harmful' });
469
+ }
470
+
471
+ const shapingScore = Math.min(1, techniques.length * 0.35);
472
+ return { detected: techniques.length > 0, techniques, shapingScore: Math.round(shapingScore * 100) / 100 };
473
+ }
474
+ }
475
+
438
476
  // =========================================================================
439
477
  // EXPORTS
440
478
  // =========================================================================
@@ -445,6 +483,7 @@ module.exports = {
445
483
  BiasDetector,
446
484
  EducationalFramingDetector,
447
485
  EmotionalReasoningDetector,
486
+ OpinionShapingDetector,
448
487
  AUTHORITATIVE_TRIGGERS,
449
488
  SAFETY_WEAKENING_CLAIMS,
450
489
  BIAS_SIGNALS,
@@ -0,0 +1,560 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Side Channel Monitor
5
+ *
6
+ * Detects data exfiltration via side channels: DNS queries, timing patterns,
7
+ * response size encoding, and other covert communication methods that
8
+ * attackers use to leak sensitive data from agent environments.
9
+ *
10
+ * All computation is pure JavaScript — no external dependencies.
11
+ * No data ever leaves the user's environment.
12
+ */
13
+
14
+ // =========================================================================
15
+ // ENTROPY ANALYZER
16
+ // =========================================================================
17
+
18
+ /**
19
+ * Shannon entropy calculator for detecting encoded data.
20
+ * Used to identify base64, hex, and other high-entropy encodings
21
+ * commonly used in data exfiltration payloads.
22
+ */
23
+ class EntropyAnalyzer {
24
+ /**
25
+ * @param {Object} [options]
26
+ * @param {number} [options.encodedThreshold=4.0] - Entropy above this suggests encoding
27
+ */
28
+ constructor(options = {}) {
29
+ this.encodedThreshold = options.encodedThreshold || 4.0;
30
+ }
31
+
32
+ /**
33
+ * Calculate Shannon entropy of a string.
34
+ * Returns value between 0 (uniform) and ~8 (maximum for ASCII byte).
35
+ * @param {string} text - Input text
36
+ * @returns {number} Shannon entropy in bits
37
+ */
38
+ calculate(text) {
39
+ if (!text || text.length === 0) return 0;
40
+
41
+ const freq = {};
42
+ for (let i = 0; i < text.length; i++) {
43
+ const ch = text[i];
44
+ freq[ch] = (freq[ch] || 0) + 1;
45
+ }
46
+
47
+ const len = text.length;
48
+ let entropy = 0;
49
+ const keys = Object.keys(freq);
50
+ for (let i = 0; i < keys.length; i++) {
51
+ const p = freq[keys[i]] / len;
52
+ if (p > 0) {
53
+ entropy -= p * Math.log2(p);
54
+ }
55
+ }
56
+
57
+ return entropy;
58
+ }
59
+
60
+ /**
61
+ * Check if text entropy suggests encoded data (base64/hex/binary).
62
+ * @param {string} text - Input text
63
+ * @returns {boolean} True if entropy exceeds threshold
64
+ */
65
+ isEncoded(text) {
66
+ if (!text || text.length === 0) return false;
67
+ return this.calculate(text) > this.encodedThreshold;
68
+ }
69
+
70
+ /**
71
+ * Detect the likely encoding type of a string.
72
+ * @param {string} text - Input text
73
+ * @returns {{ encoding: string, confidence: number }}
74
+ */
75
+ detectEncoding(text) {
76
+ if (!text || text.length === 0) {
77
+ return { encoding: 'plaintext', confidence: 0 };
78
+ }
79
+
80
+ // Base64 pattern: A-Za-z0-9+/= with optional padding
81
+ const base64Re = /^[A-Za-z0-9+/]+=*$/;
82
+ // Hex pattern: only 0-9a-fA-F
83
+ const hexRe = /^[0-9a-fA-F]+$/;
84
+ // Binary pattern: only 0 and 1
85
+ const binaryRe = /^[01]+$/;
86
+
87
+ const entropy = this.calculate(text);
88
+
89
+ if (binaryRe.test(text) && text.length >= 8) {
90
+ return { encoding: 'binary', confidence: 0.9 };
91
+ }
92
+
93
+ if (hexRe.test(text) && text.length >= 8) {
94
+ // Hex has limited charset — entropy is moderate but pattern is distinctive
95
+ const confidence = Math.min(0.95, 0.5 + (text.length / 64) * 0.45);
96
+ return { encoding: 'hex', confidence };
97
+ }
98
+
99
+ if (base64Re.test(text) && text.length >= 4 && entropy > 3.5) {
100
+ const confidence = Math.min(0.95, 0.5 + (entropy / 6) * 0.45);
101
+ return { encoding: 'base64', confidence };
102
+ }
103
+
104
+ return { encoding: 'plaintext', confidence: entropy < 3.0 ? 0.9 : 0.5 };
105
+ }
106
+ }
107
+
108
+ // =========================================================================
109
+ // BEACON DETECTOR
110
+ // =========================================================================
111
+
112
+ /**
113
+ * Detects C2 (command-and-control) beaconing patterns by analyzing
114
+ * the regularity of network event timestamps.
115
+ */
116
+ class BeaconDetector {
117
+ /**
118
+ * @param {Object} [options]
119
+ * @param {number} [options.maxJitterRatio=0.2] - Max jitter/interval ratio for beaconing
120
+ * @param {number} [options.minEvents=4] - Minimum events to analyze
121
+ */
122
+ constructor(options = {}) {
123
+ this.maxJitterRatio = options.maxJitterRatio || 0.2;
124
+ this.minEvents = options.minEvents || 4;
125
+ /** @type {number[]} */
126
+ this.events = [];
127
+ }
128
+
129
+ /**
130
+ * Record a network event timestamp.
131
+ * @param {number} timestamp - Event timestamp in milliseconds
132
+ */
133
+ addEvent(timestamp) {
134
+ this.events.push(timestamp);
135
+ // Keep sorted
136
+ this.events.sort((a, b) => a - b);
137
+ }
138
+
139
+ /**
140
+ * Analyze recorded events for beaconing behavior.
141
+ * Beaconing is identified by regular intervals with low jitter.
142
+ * @returns {{ beaconing: boolean, interval: number|null, jitter: number, confidence: number }}
143
+ */
144
+ detectBeaconing() {
145
+ if (this.events.length < this.minEvents) {
146
+ return { beaconing: false, interval: null, jitter: 0, confidence: 0 };
147
+ }
148
+
149
+ // Calculate inter-event intervals
150
+ const intervals = [];
151
+ for (let i = 1; i < this.events.length; i++) {
152
+ intervals.push(this.events[i] - this.events[i - 1]);
153
+ }
154
+
155
+ // Compute mean interval
156
+ const sum = intervals.reduce((a, b) => a + b, 0);
157
+ const meanInterval = sum / intervals.length;
158
+
159
+ if (meanInterval === 0) {
160
+ return { beaconing: false, interval: 0, jitter: 0, confidence: 0 };
161
+ }
162
+
163
+ // Compute standard deviation (jitter)
164
+ const sqDiffs = intervals.map(v => (v - meanInterval) ** 2);
165
+ const variance = sqDiffs.reduce((a, b) => a + b, 0) / intervals.length;
166
+ const jitter = Math.sqrt(variance);
167
+
168
+ // Jitter ratio: how much variation relative to the mean interval
169
+ const jitterRatio = jitter / meanInterval;
170
+
171
+ const beaconing = jitterRatio <= this.maxJitterRatio;
172
+
173
+ // Confidence: lower jitter ratio = higher confidence
174
+ const confidence = beaconing
175
+ ? Math.min(0.99, 1.0 - jitterRatio)
176
+ : Math.max(0.0, this.maxJitterRatio - jitterRatio + 0.3);
177
+
178
+ return {
179
+ beaconing,
180
+ interval: Math.round(meanInterval),
181
+ jitter: Math.round(jitter * 100) / 100,
182
+ confidence: Math.round(confidence * 1000) / 1000
183
+ };
184
+ }
185
+ }
186
+
187
+ // =========================================================================
188
+ // SIDE CHANNEL MONITOR
189
+ // =========================================================================
190
+
191
+ /**
192
+ * Main side-channel exfiltration detector.
193
+ * Analyzes DNS queries, timing patterns, response sizes, and URL parameters
194
+ * for signs of covert data exfiltration.
195
+ */
196
+ class SideChannelMonitor {
197
+ /**
198
+ * @param {Object} [options]
199
+ * @param {number} [options.entropyThreshold=4.0] - Shannon entropy threshold for encoded data
200
+ * @param {number} [options.timingWindowMs=5000] - Timing analysis window in ms
201
+ * @param {number} [options.maxDNSLength=63] - Max allowed DNS label length (RFC 1035)
202
+ */
203
+ constructor(options = {}) {
204
+ this.entropyThreshold = options.entropyThreshold || 4.0;
205
+ this.timingWindowMs = options.timingWindowMs || 5000;
206
+ this.maxDNSLength = options.maxDNSLength || 63;
207
+ this.entropy = new EntropyAnalyzer({ encodedThreshold: this.entropyThreshold });
208
+ }
209
+
210
+ /**
211
+ * Detect DNS exfiltration via encoded subdomains.
212
+ * Attackers encode stolen data into DNS labels: {base64-data}.attacker.com
213
+ * @param {string} domain - Full domain name to analyze
214
+ * @returns {{ exfiltration: boolean, channel: string, confidence: number, evidence: string[], severity: string }}
215
+ */
216
+ analyzeDNSQuery(domain) {
217
+ const result = {
218
+ exfiltration: false,
219
+ channel: 'dns',
220
+ confidence: 0,
221
+ evidence: [],
222
+ severity: 'safe'
223
+ };
224
+
225
+ if (!domain || typeof domain !== 'string') return result;
226
+
227
+ const labels = domain.split('.');
228
+
229
+ // Known exfil domain patterns
230
+ const exfilPatterns = [
231
+ /\.burpcollaborator\.net$/i,
232
+ /\.oastify\.com$/i,
233
+ /\.interact\.sh$/i,
234
+ /\.canarytokens\.com$/i,
235
+ /\.requestbin\.net$/i,
236
+ /\.ngrok\.io$/i
237
+ ];
238
+
239
+ for (const pat of exfilPatterns) {
240
+ if (pat.test(domain)) {
241
+ result.exfiltration = true;
242
+ result.confidence = 0.95;
243
+ result.evidence.push(`Known exfiltration domain pattern: ${domain}`);
244
+ result.severity = 'critical';
245
+ return result;
246
+ }
247
+ }
248
+
249
+ // Analyze each subdomain label (skip TLD and registered domain)
250
+ // For a.b.c.example.com, analyze labels: a, b, c
251
+ const subdomainLabels = labels.length > 2 ? labels.slice(0, labels.length - 2) : [];
252
+
253
+ for (const label of subdomainLabels) {
254
+ // Check label length
255
+ if (label.length > this.maxDNSLength) {
256
+ result.evidence.push(`DNS label exceeds max length (${label.length} > ${this.maxDNSLength}): ${label.substring(0, 20)}...`);
257
+ result.confidence = Math.max(result.confidence, 0.8);
258
+ }
259
+
260
+ // Check for unusually long labels (potential data carrier)
261
+ if (label.length > 30) {
262
+ result.evidence.push(`Unusually long DNS label (${label.length} chars): ${label.substring(0, 20)}...`);
263
+ result.confidence = Math.max(result.confidence, 0.7);
264
+ }
265
+
266
+ // Check for high entropy (encoded data)
267
+ const labelEntropy = this.entropy.calculate(label);
268
+ if (label.length >= 8 && labelEntropy > this.entropyThreshold) {
269
+ result.evidence.push(`High-entropy DNS label (H=${labelEntropy.toFixed(2)}): ${label.substring(0, 20)}...`);
270
+ result.confidence = Math.max(result.confidence, 0.85);
271
+ }
272
+
273
+ // Check for base64-like patterns in labels
274
+ const base64Re = /^[A-Za-z0-9+/]{8,}=*$/;
275
+ if (base64Re.test(label)) {
276
+ result.evidence.push(`Base64-encoded DNS label: ${label.substring(0, 20)}...`);
277
+ result.confidence = Math.max(result.confidence, 0.9);
278
+ }
279
+
280
+ // Check for hex-encoded data
281
+ const hexRe = /^[0-9a-fA-F]{16,}$/;
282
+ if (hexRe.test(label)) {
283
+ result.evidence.push(`Hex-encoded DNS label: ${label.substring(0, 20)}...`);
284
+ result.confidence = Math.max(result.confidence, 0.85);
285
+ }
286
+ }
287
+
288
+ if (result.evidence.length > 0) {
289
+ result.exfiltration = true;
290
+ result.severity = result.confidence >= 0.9 ? 'critical'
291
+ : result.confidence >= 0.7 ? 'high'
292
+ : 'medium';
293
+ }
294
+
295
+ return result;
296
+ }
297
+
298
+ /**
299
+ * Detect timing-based exfiltration.
300
+ * Attackers encode data bits in inter-request delays:
301
+ * - Binary: short delay = 0, long delay = 1
302
+ * - Morse: dash/dot patterns in timing
303
+ * - Beaconing: fixed-interval callbacks
304
+ * @param {number[]} timestamps - Array of event timestamps (ms)
305
+ * @returns {{ exfiltration: boolean, channel: string, confidence: number, evidence: string[], severity: string }}
306
+ */
307
+ analyzeTimingPattern(timestamps) {
308
+ const result = {
309
+ exfiltration: false,
310
+ channel: 'timing',
311
+ confidence: 0,
312
+ evidence: [],
313
+ severity: 'safe'
314
+ };
315
+
316
+ if (!Array.isArray(timestamps) || timestamps.length < 3) return result;
317
+
318
+ const sorted = [...timestamps].sort((a, b) => a - b);
319
+ const intervals = [];
320
+ for (let i = 1; i < sorted.length; i++) {
321
+ intervals.push(sorted[i] - sorted[i - 1]);
322
+ }
323
+
324
+ // Detect binary encoding: intervals cluster around two values
325
+ const uniqueRounded = new Set(intervals.map(v => Math.round(v / 50) * 50));
326
+ if (uniqueRounded.size === 2 && intervals.length >= 4) {
327
+ const values = [...uniqueRounded].sort((a, b) => a - b);
328
+ // Check if there's a clear ratio between the two clusters (e.g., 1:2 or 1:3)
329
+ if (values[0] > 0 && values[1] / values[0] >= 1.5 && values[1] / values[0] <= 5) {
330
+ result.evidence.push(`Binary timing encoding detected: intervals cluster at ${values[0]}ms and ${values[1]}ms`);
331
+ result.confidence = Math.max(result.confidence, 0.8);
332
+ }
333
+ }
334
+
335
+ // Detect fixed-interval beaconing
336
+ const meanInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
337
+ const variance = intervals.reduce((a, b) => a + (b - meanInterval) ** 2, 0) / intervals.length;
338
+ const stdDev = Math.sqrt(variance);
339
+ const cv = meanInterval > 0 ? stdDev / meanInterval : 0; // coefficient of variation
340
+
341
+ if (cv < 0.15 && intervals.length >= 3) {
342
+ result.evidence.push(`Fixed-interval beaconing: mean=${Math.round(meanInterval)}ms, CV=${cv.toFixed(3)}`);
343
+ result.confidence = Math.max(result.confidence, 0.85);
344
+ }
345
+
346
+ // Detect Morse-code-like patterns (3 distinct interval clusters: dot, dash, space)
347
+ if (uniqueRounded.size === 3 && intervals.length >= 6) {
348
+ result.evidence.push('Morse-code-like timing pattern: 3 distinct interval clusters');
349
+ result.confidence = Math.max(result.confidence, 0.7);
350
+ }
351
+
352
+ // Check if events fall within suspicious window
353
+ const span = sorted[sorted.length - 1] - sorted[0];
354
+ if (span <= this.timingWindowMs && intervals.length >= 8) {
355
+ result.evidence.push(`High-frequency burst: ${intervals.length + 1} events in ${span}ms`);
356
+ result.confidence = Math.max(result.confidence, 0.6);
357
+ }
358
+
359
+ if (result.evidence.length > 0) {
360
+ result.exfiltration = true;
361
+ result.severity = result.confidence >= 0.8 ? 'high'
362
+ : result.confidence >= 0.6 ? 'medium'
363
+ : 'low';
364
+ }
365
+
366
+ return result;
367
+ }
368
+
369
+ /**
370
+ * Detect response-size encoding.
371
+ * Attackers encode data in the content-length of responses:
372
+ * small variations (e.g., 100 vs 101 bytes) encode bits.
373
+ * @param {number[]} sizes - Array of response sizes in bytes
374
+ * @returns {{ exfiltration: boolean, channel: string, confidence: number, evidence: string[], severity: string }}
375
+ */
376
+ analyzeResponseSize(sizes) {
377
+ const result = {
378
+ exfiltration: false,
379
+ channel: 'response-size',
380
+ confidence: 0,
381
+ evidence: [],
382
+ severity: 'safe'
383
+ };
384
+
385
+ if (!Array.isArray(sizes) || sizes.length < 4) return result;
386
+
387
+ // Check for small binary-like variations (e.g., sizes differ by 1)
388
+ const diffs = [];
389
+ for (let i = 1; i < sizes.length; i++) {
390
+ diffs.push(Math.abs(sizes[i] - sizes[i - 1]));
391
+ }
392
+
393
+ // Detect bit-encoding: diffs are consistently small (0 or 1)
394
+ const smallDiffCount = diffs.filter(d => d <= 2).length;
395
+ const smallDiffRatio = smallDiffCount / diffs.length;
396
+
397
+ if (smallDiffRatio >= 0.8 && diffs.length >= 4) {
398
+ // Check that there's actual variation (not all identical)
399
+ const hasVariation = new Set(sizes).size > 1;
400
+ if (hasVariation) {
401
+ result.evidence.push(`Bit-encoding in response sizes: ${smallDiffRatio * 100}% of diffs <= 2 bytes`);
402
+ result.confidence = Math.max(result.confidence, 0.8);
403
+ }
404
+ }
405
+
406
+ // Detect pattern repetition (repeated size sequences)
407
+ if (sizes.length >= 8) {
408
+ const sizeStr = sizes.join(',');
409
+ const half = sizes.slice(0, Math.floor(sizes.length / 2)).join(',');
410
+ if (sizeStr.includes(half + ',' + half)) {
411
+ result.evidence.push('Repeated response size pattern detected');
412
+ result.confidence = Math.max(result.confidence, 0.75);
413
+ }
414
+ }
415
+
416
+ // Detect sizes clustered around two values (binary encoding)
417
+ const uniqueSizes = new Set(sizes);
418
+ if (uniqueSizes.size === 2 && sizes.length >= 6) {
419
+ const vals = [...uniqueSizes].sort((a, b) => a - b);
420
+ const diff = vals[1] - vals[0];
421
+ if (diff <= 10) {
422
+ result.evidence.push(`Binary response-size encoding: sizes alternate between ${vals[0]} and ${vals[1]}`);
423
+ result.confidence = Math.max(result.confidence, 0.85);
424
+ }
425
+ }
426
+
427
+ if (result.evidence.length > 0) {
428
+ result.exfiltration = true;
429
+ result.severity = result.confidence >= 0.8 ? 'high'
430
+ : result.confidence >= 0.6 ? 'medium'
431
+ : 'low';
432
+ }
433
+
434
+ return result;
435
+ }
436
+
437
+ /**
438
+ * Detect data hidden in URL parameters.
439
+ * Attackers exfiltrate data via base64 blobs, hex strings,
440
+ * or suspiciously long parameter values in URLs.
441
+ * @param {string} url - URL to analyze
442
+ * @returns {{ exfiltration: boolean, channel: string, confidence: number, evidence: string[], severity: string }}
443
+ */
444
+ analyzeURLParams(url) {
445
+ const result = {
446
+ exfiltration: false,
447
+ channel: 'url-params',
448
+ confidence: 0,
449
+ evidence: [],
450
+ severity: 'safe'
451
+ };
452
+
453
+ if (!url || typeof url !== 'string') return result;
454
+
455
+ // Extract query string
456
+ const qIdx = url.indexOf('?');
457
+ if (qIdx === -1) return result;
458
+
459
+ const queryString = url.substring(qIdx + 1);
460
+ const params = queryString.split('&');
461
+
462
+ for (const param of params) {
463
+ const eqIdx = param.indexOf('=');
464
+ if (eqIdx === -1) continue;
465
+
466
+ const key = param.substring(0, eqIdx);
467
+ const value = param.substring(eqIdx + 1);
468
+
469
+ if (!value) continue;
470
+
471
+ const decoded = decodeURIComponent(value);
472
+
473
+ // Suspiciously long parameter values
474
+ if (decoded.length > 200) {
475
+ result.evidence.push(`Suspiciously long URL parameter '${key}' (${decoded.length} chars)`);
476
+ result.confidence = Math.max(result.confidence, 0.7);
477
+ }
478
+
479
+ // Base64 blobs in parameters
480
+ const base64Re = /^[A-Za-z0-9+/]{20,}=*$/;
481
+ if (base64Re.test(decoded)) {
482
+ result.evidence.push(`Base64-encoded URL parameter '${key}': ${decoded.substring(0, 20)}...`);
483
+ result.confidence = Math.max(result.confidence, 0.85);
484
+ }
485
+
486
+ // Hex strings in parameters
487
+ const hexRe = /^[0-9a-fA-F]{16,}$/;
488
+ if (hexRe.test(decoded)) {
489
+ result.evidence.push(`Hex-encoded URL parameter '${key}': ${decoded.substring(0, 20)}...`);
490
+ result.confidence = Math.max(result.confidence, 0.8);
491
+ }
492
+
493
+ // Credential-like patterns
494
+ const credentialPatterns = [
495
+ /(?:api[_-]?key|token|secret|password|auth|bearer)\s*[:=]\s*.+/i,
496
+ /(?:AWS|AKIA)[A-Z0-9]{12,}/,
497
+ /ghp_[A-Za-z0-9]{36}/,
498
+ /sk-[A-Za-z0-9]{20,}/,
499
+ /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+/ // JWT
500
+ ];
501
+
502
+ for (const pat of credentialPatterns) {
503
+ if (pat.test(decoded)) {
504
+ result.evidence.push(`Credential-like data in URL parameter '${key}'`);
505
+ result.confidence = Math.max(result.confidence, 0.95);
506
+ break;
507
+ }
508
+ }
509
+
510
+ // High entropy parameter values
511
+ if (decoded.length >= 12) {
512
+ const paramEntropy = this.entropy.calculate(decoded);
513
+ if (paramEntropy > this.entropyThreshold + 0.5) {
514
+ result.evidence.push(`High-entropy URL parameter '${key}' (H=${paramEntropy.toFixed(2)})`);
515
+ result.confidence = Math.max(result.confidence, 0.65);
516
+ }
517
+ }
518
+ }
519
+
520
+ if (result.evidence.length > 0) {
521
+ result.exfiltration = true;
522
+ result.severity = result.confidence >= 0.9 ? 'critical'
523
+ : result.confidence >= 0.7 ? 'high'
524
+ : 'medium';
525
+ }
526
+
527
+ return result;
528
+ }
529
+
530
+ /**
531
+ * Unified scanner for all side-channel types.
532
+ * @param {{ type: 'dns'|'timing'|'response'|'url', data: any }} event - Event to scan
533
+ * @returns {{ exfiltration: boolean, channel: string, confidence: number, evidence: string[], severity: string }}
534
+ */
535
+ scan(event) {
536
+ if (!event || !event.type) {
537
+ return { exfiltration: false, channel: 'unknown', confidence: 0, evidence: [], severity: 'safe' };
538
+ }
539
+
540
+ switch (event.type) {
541
+ case 'dns':
542
+ return this.analyzeDNSQuery(event.data);
543
+ case 'timing':
544
+ return this.analyzeTimingPattern(event.data);
545
+ case 'response':
546
+ return this.analyzeResponseSize(event.data);
547
+ case 'url':
548
+ return this.analyzeURLParams(event.data);
549
+ default:
550
+ console.log(`[Agent Shield] Unknown side-channel event type: ${event.type}`);
551
+ return { exfiltration: false, channel: event.type, confidence: 0, evidence: [], severity: 'safe' };
552
+ }
553
+ }
554
+ }
555
+
556
+ // =========================================================================
557
+ // EXPORTS
558
+ // =========================================================================
559
+
560
+ module.exports = { SideChannelMonitor, BeaconDetector, EntropyAnalyzer };