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.
- package/CHANGELOG.md +49 -1
- package/README.md +260 -1143
- package/package.json +2 -2
- package/src/deepmind-defenses.js +468 -0
- package/src/fleet-defense.js +24 -0
- package/src/hitl-guard.js +64 -0
- package/src/main.js +36 -0
- package/src/memory-guard.js +48 -0
- package/src/render-differential.js +608 -0
- package/src/semantic-guard.js +39 -0
- package/src/side-channel-monitor.js +560 -0
- package/src/sybil-detector.js +529 -0
- package/src/trap-defense.js +112 -0
package/src/semantic-guard.js
CHANGED
|
@@ -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 };
|