@aientrophy/sdk 0.2.0 → 0.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/README.md +3 -4
- package/dist/{console-BRZJJX1_.js → console-DbZZ4Ctg.js} +7 -0
- package/dist/{console-D3Kd9M_6.cjs → console-aIpHQlgP.cjs} +1 -1
- package/dist/core.js +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/loader.js +1 -1
- package/dist/npm/index.cjs.js +1 -1
- package/dist/npm/index.es.js +1589 -200
- package/dist/sri-hashes.json +9 -0
- package/dist/types/challenge/ChallengeHandler.d.ts +1 -0
- package/dist/types/collection/BehaviorCollector.d.ts +0 -13
- package/dist/types/core/index.d.ts +14 -1
- package/dist/types/detectors/AutomationDetector.d.ts +43 -0
- package/dist/types/detectors/CanvasFingerprinter.d.ts +19 -2
- package/dist/types/detectors/CloudEnvironmentDetector.d.ts +51 -0
- package/dist/types/detectors/CrossContextDetector.d.ts +34 -0
- package/dist/types/detectors/HeadlessProbeDetector.d.ts +66 -0
- package/dist/types/detectors/InputTracker.d.ts +1 -0
- package/dist/types/detectors/MouseTracker.d.ts +2 -0
- package/dist/types/detectors/SessionAnalyzer.d.ts +39 -0
- package/dist/types/detectors/TabNavigationAnalyzer.d.ts +36 -0
- package/dist/types/detectors/console.d.ts +4 -0
- package/dist/types/protection/CrawlProtect.d.ts +38 -0
- package/dist/types/protection/CryptoUtils.d.ts +38 -3
- package/dist/types/transport/index.d.ts +5 -0
- package/package.json +2 -2
- package/dist/types/collector/index.d.ts +0 -11
package/dist/npm/index.es.js
CHANGED
|
@@ -7,33 +7,100 @@ const CONFIG = {
|
|
|
7
7
|
RSA_PUBLIC_KEY: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbBCvDqyUtfhgI/Lo5pDtn1phwA6qczAyp8N1ZgQ68OTxgbJUGiXV6N67p15bBdME1R4va51P2Czq2IbRby/N+GHTciTaXvmusV8IjnIOGtKlGxWCuKIPrCS+rGjGA2j1irxBbNpqItltFxjhxBGOzsyQAF6LNIz5IKA7sC6cSH8zUyLuFrV96udc801Zc4nCJG64ZljNbDvlVbJJZ6ex5OLLS6AnrVAXpjEDR/MI/I8JvAPe/psHj6EpXgvKrBYMBOH3jzQRzMFoR79jXyGNgPjdy0A+f6RVuEG8H5sUXKeyy+cFvaT+pm6h+t6RPED11tCOwQxYZ2pExhhPxDGeQIDAQAB"
|
|
8
8
|
};
|
|
9
9
|
class CryptoUtils {
|
|
10
|
-
//
|
|
10
|
+
// 2 = ECDH, 1 = RSA
|
|
11
11
|
/**
|
|
12
|
-
* Initialize session key
|
|
12
|
+
* Initialize session key using ECDH + HKDF (PFS) or RSA fallback.
|
|
13
13
|
*/
|
|
14
14
|
static async init() {
|
|
15
15
|
if (this.sessionKey) return;
|
|
16
|
-
|
|
16
|
+
try {
|
|
17
|
+
await this.initECDH();
|
|
18
|
+
} catch (e) {
|
|
19
|
+
await this.initRSA();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* ECDH + HKDF key exchange — provides Perfect Forward Secrecy.
|
|
24
|
+
*
|
|
25
|
+
* 1. Generate ephemeral ECDH key pair (P-256)
|
|
26
|
+
* 2. Fetch server's ephemeral ECDH public key from /key-exchange
|
|
27
|
+
* 3. Derive shared secret via ECDH
|
|
28
|
+
* 4. Derive AES-256-GCM key via HKDF-SHA256
|
|
29
|
+
*/
|
|
30
|
+
static async initECDH() {
|
|
31
|
+
const keyPair = await window.crypto.subtle.generateKey(
|
|
32
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
33
|
+
true,
|
|
34
|
+
["deriveBits"]
|
|
35
|
+
);
|
|
36
|
+
const clientPubRaw = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
37
|
+
this.ecdhPublicKeyBase64 = this.arrayBufferToBase64(clientPubRaw);
|
|
38
|
+
const endpoint = window.__securitySDKEndpoint || "";
|
|
39
|
+
const baseUrl = endpoint.replace(/\/events$/, "");
|
|
40
|
+
const res = await fetch(`${baseUrl}/key-exchange`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ clientPublicKey: this.ecdhPublicKeyBase64 })
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) throw new Error("Key exchange failed");
|
|
46
|
+
const { serverPublicKey, salt } = await res.json();
|
|
47
|
+
const serverPubBytes = this.base64ToArrayBuffer(serverPublicKey);
|
|
48
|
+
const serverPubKey = await window.crypto.subtle.importKey(
|
|
49
|
+
"raw",
|
|
50
|
+
serverPubBytes,
|
|
51
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
52
|
+
false,
|
|
53
|
+
[]
|
|
54
|
+
);
|
|
55
|
+
const sharedBits = await window.crypto.subtle.deriveBits(
|
|
56
|
+
{ name: "ECDH", public: serverPubKey },
|
|
57
|
+
keyPair.privateKey,
|
|
58
|
+
256
|
|
59
|
+
);
|
|
60
|
+
const hkdfKey = await window.crypto.subtle.importKey(
|
|
61
|
+
"raw",
|
|
62
|
+
sharedBits,
|
|
63
|
+
"HKDF",
|
|
64
|
+
false,
|
|
65
|
+
["deriveKey"]
|
|
66
|
+
);
|
|
67
|
+
const saltBytes = this.base64ToArrayBuffer(salt);
|
|
68
|
+
this.sessionKey = await window.crypto.subtle.deriveKey(
|
|
17
69
|
{
|
|
18
|
-
name: "
|
|
19
|
-
|
|
70
|
+
name: "HKDF",
|
|
71
|
+
hash: "SHA-256",
|
|
72
|
+
salt: saltBytes,
|
|
73
|
+
info: new TextEncoder().encode("aientrophy-session-enc")
|
|
20
74
|
},
|
|
75
|
+
hkdfKey,
|
|
76
|
+
{ name: "AES-GCM", length: 256 },
|
|
21
77
|
true,
|
|
22
78
|
["encrypt", "decrypt"]
|
|
23
79
|
);
|
|
24
|
-
|
|
80
|
+
this.protocolVersion = 2;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* RSA-OAEP key exchange (legacy fallback, no PFS).
|
|
84
|
+
*/
|
|
85
|
+
static async initRSA() {
|
|
86
|
+
this.sessionKey = await window.crypto.subtle.generateKey(
|
|
87
|
+
{ name: "AES-GCM", length: 256 },
|
|
88
|
+
true,
|
|
89
|
+
["encrypt", "decrypt"]
|
|
90
|
+
);
|
|
91
|
+
const publicKey = await this.importRSAPublicKey(CONFIG.RSA_PUBLIC_KEY);
|
|
25
92
|
const rawSessionKey = await window.crypto.subtle.exportKey("raw", this.sessionKey);
|
|
26
93
|
const encryptedKeyBuffer = await window.crypto.subtle.encrypt(
|
|
27
|
-
{
|
|
28
|
-
name: "RSA-OAEP"
|
|
29
|
-
},
|
|
94
|
+
{ name: "RSA-OAEP" },
|
|
30
95
|
publicKey,
|
|
31
96
|
rawSessionKey
|
|
32
97
|
);
|
|
33
98
|
this.encryptedSessionKey = this.arrayBufferToBase64(encryptedKeyBuffer);
|
|
99
|
+
this.protocolVersion = 1;
|
|
34
100
|
}
|
|
35
101
|
/**
|
|
36
|
-
* Encrypt Payload using AES-GCM
|
|
102
|
+
* Encrypt Payload using AES-GCM.
|
|
103
|
+
* Returns envelope with version indicator for server-side dispatch.
|
|
37
104
|
*/
|
|
38
105
|
static async encrypt(data) {
|
|
39
106
|
if (!this.sessionKey) await this.init();
|
|
@@ -41,10 +108,7 @@ class CryptoUtils {
|
|
|
41
108
|
const encodedData = new TextEncoder().encode(jsonString);
|
|
42
109
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
43
110
|
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
|
44
|
-
{
|
|
45
|
-
name: "AES-GCM",
|
|
46
|
-
iv
|
|
47
|
-
},
|
|
111
|
+
{ name: "AES-GCM", iv },
|
|
48
112
|
this.sessionKey,
|
|
49
113
|
encodedData
|
|
50
114
|
);
|
|
@@ -52,12 +116,16 @@ class CryptoUtils {
|
|
|
52
116
|
const tagLength = 16;
|
|
53
117
|
const ciphertext = encryptedBytes.slice(0, encryptedBytes.length - tagLength);
|
|
54
118
|
const tag = encryptedBytes.slice(encryptedBytes.length - tagLength);
|
|
55
|
-
|
|
56
|
-
ek: this.encryptedSessionKey,
|
|
119
|
+
const envelope = {
|
|
120
|
+
ek: this.protocolVersion === 2 ? this.ecdhPublicKeyBase64 : this.encryptedSessionKey,
|
|
57
121
|
d: this.arrayBufferToBase64(ciphertext.buffer),
|
|
58
122
|
iv: this.arrayBufferToBase64(iv.buffer),
|
|
59
123
|
tag: this.arrayBufferToBase64(tag.buffer)
|
|
60
124
|
};
|
|
125
|
+
if (this.protocolVersion === 2) {
|
|
126
|
+
envelope.v = 2;
|
|
127
|
+
}
|
|
128
|
+
return envelope;
|
|
61
129
|
}
|
|
62
130
|
/**
|
|
63
131
|
* Decrypt Response using AES-GCM
|
|
@@ -71,26 +139,27 @@ class CryptoUtils {
|
|
|
71
139
|
combinedBuffer.set(new Uint8Array(ciphertext), 0);
|
|
72
140
|
combinedBuffer.set(new Uint8Array(tag), ciphertext.byteLength);
|
|
73
141
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
74
|
-
{
|
|
75
|
-
name: "AES-GCM",
|
|
76
|
-
iv
|
|
77
|
-
},
|
|
142
|
+
{ name: "AES-GCM", iv },
|
|
78
143
|
this.sessionKey,
|
|
79
144
|
combinedBuffer
|
|
80
145
|
);
|
|
81
146
|
const decodedString = new TextDecoder().decode(decryptedBuffer);
|
|
82
147
|
return JSON.parse(decodedString);
|
|
83
148
|
}
|
|
84
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Store the endpoint URL for ECDH key exchange.
|
|
151
|
+
* Called from Transmitter.setConfig().
|
|
152
|
+
*/
|
|
153
|
+
static setEndpoint(endpoint) {
|
|
154
|
+
window.__securitySDKEndpoint = endpoint;
|
|
155
|
+
}
|
|
156
|
+
static async importRSAPublicKey(pemContent) {
|
|
85
157
|
const binaryDerString = window.atob(pemContent);
|
|
86
158
|
const binaryDer = this.str2ab(binaryDerString);
|
|
87
159
|
return await window.crypto.subtle.importKey(
|
|
88
160
|
"spki",
|
|
89
161
|
binaryDer,
|
|
90
|
-
{
|
|
91
|
-
name: "RSA-OAEP",
|
|
92
|
-
hash: "SHA-256"
|
|
93
|
-
},
|
|
162
|
+
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
94
163
|
true,
|
|
95
164
|
["encrypt"]
|
|
96
165
|
);
|
|
@@ -123,7 +192,10 @@ class CryptoUtils {
|
|
|
123
192
|
}
|
|
124
193
|
}
|
|
125
194
|
__publicField(CryptoUtils, "sessionKey", null);
|
|
195
|
+
__publicField(CryptoUtils, "ecdhPublicKeyBase64", null);
|
|
126
196
|
__publicField(CryptoUtils, "encryptedSessionKey", null);
|
|
197
|
+
// RSA fallback
|
|
198
|
+
__publicField(CryptoUtils, "protocolVersion", 2);
|
|
127
199
|
const THREAT_EVENTS = /* @__PURE__ */ new Set([
|
|
128
200
|
"rapid_click",
|
|
129
201
|
"honeypot_triggered",
|
|
@@ -136,7 +208,10 @@ const THREAT_EVENTS = /* @__PURE__ */ new Set([
|
|
|
136
208
|
"input_metrics",
|
|
137
209
|
"keystroke_dynamics",
|
|
138
210
|
"environmental_integrity",
|
|
139
|
-
"init_signals"
|
|
211
|
+
"init_signals",
|
|
212
|
+
"automation_detected",
|
|
213
|
+
"cross_context_mismatch",
|
|
214
|
+
"headless_probe"
|
|
140
215
|
]);
|
|
141
216
|
class CallbackManager {
|
|
142
217
|
constructor() {
|
|
@@ -256,11 +331,16 @@ class Transmitter {
|
|
|
256
331
|
__publicField(this, "encryptionRequired", false);
|
|
257
332
|
__publicField(this, "currentNonce", "");
|
|
258
333
|
__publicField(this, "buffer", []);
|
|
259
|
-
__publicField(this, "batchSize",
|
|
260
|
-
__publicField(this, "flushInterval",
|
|
334
|
+
__publicField(this, "batchSize", 20);
|
|
335
|
+
__publicField(this, "flushInterval", 5e3);
|
|
261
336
|
__publicField(this, "intervalId");
|
|
262
337
|
__publicField(this, "responseHandler", null);
|
|
263
338
|
__publicField(this, "callbackManager", null);
|
|
339
|
+
__publicField(this, "flushing", false);
|
|
340
|
+
__publicField(this, "pendingFlush", false);
|
|
341
|
+
__publicField(this, "backoffMs", 0);
|
|
342
|
+
__publicField(this, "maxBackoffMs", 6e4);
|
|
343
|
+
__publicField(this, "_lastBackoffStart", 0);
|
|
264
344
|
this.startFlushInterval();
|
|
265
345
|
}
|
|
266
346
|
setConfig(config) {
|
|
@@ -269,6 +349,7 @@ class Transmitter {
|
|
|
269
349
|
this.clientKey = config.clientKey;
|
|
270
350
|
this.encryptionRequired = ((_a = config.serverConfig) == null ? void 0 : _a.encryptionRequired) ?? false;
|
|
271
351
|
this.currentNonce = ((_b = config.serverConfig) == null ? void 0 : _b.initialNonce) ?? "";
|
|
352
|
+
CryptoUtils.setEndpoint(config.endpoint);
|
|
272
353
|
}
|
|
273
354
|
setResponseHandler(handler) {
|
|
274
355
|
this.responseHandler = handler;
|
|
@@ -293,6 +374,17 @@ class Transmitter {
|
|
|
293
374
|
}
|
|
294
375
|
async flush() {
|
|
295
376
|
if (this.buffer.length === 0 || !this.endpoint) return;
|
|
377
|
+
if (this.backoffMs > 0) {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
if (!this._lastBackoffStart) this._lastBackoffStart = now;
|
|
380
|
+
if (now - this._lastBackoffStart < this.backoffMs) return;
|
|
381
|
+
this._lastBackoffStart = 0;
|
|
382
|
+
}
|
|
383
|
+
if (this.flushing) {
|
|
384
|
+
this.pendingFlush = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.flushing = true;
|
|
296
388
|
const rawPayload = this.buffer;
|
|
297
389
|
this.buffer = [];
|
|
298
390
|
try {
|
|
@@ -311,12 +403,20 @@ class Transmitter {
|
|
|
311
403
|
requestHeaders["x-request-nonce"] = this.currentNonce;
|
|
312
404
|
this.currentNonce = "";
|
|
313
405
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
406
|
+
try {
|
|
407
|
+
const res = await fetch(this.endpoint, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: payload,
|
|
410
|
+
headers: requestHeaders,
|
|
411
|
+
keepalive: true
|
|
412
|
+
});
|
|
413
|
+
if (res.status === 429) {
|
|
414
|
+
this.buffer = rawPayload.concat(this.buffer).slice(0, 500);
|
|
415
|
+
this.backoffMs = this.backoffMs === 0 ? 5e3 : Math.min(this.backoffMs * 2, this.maxBackoffMs);
|
|
416
|
+
this._lastBackoffStart = Date.now();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.backoffMs = 0;
|
|
320
420
|
if (res.ok && this.callbackManager) {
|
|
321
421
|
this.callbackManager.emit("allow", { action: "allow" });
|
|
322
422
|
}
|
|
@@ -336,14 +436,14 @@ class Transmitter {
|
|
|
336
436
|
} catch (e) {
|
|
337
437
|
}
|
|
338
438
|
}
|
|
339
|
-
}
|
|
439
|
+
} catch (err) {
|
|
340
440
|
if (this.callbackManager) {
|
|
341
441
|
this.callbackManager.emit("error", {
|
|
342
442
|
code: "NETWORK_ERROR",
|
|
343
443
|
message: "Failed to send security events"
|
|
344
444
|
});
|
|
345
445
|
}
|
|
346
|
-
}
|
|
446
|
+
}
|
|
347
447
|
} catch (e) {
|
|
348
448
|
if (this.callbackManager) {
|
|
349
449
|
this.callbackManager.emit("error", {
|
|
@@ -351,6 +451,12 @@ class Transmitter {
|
|
|
351
451
|
message: "Failed to prepare security payload"
|
|
352
452
|
});
|
|
353
453
|
}
|
|
454
|
+
} finally {
|
|
455
|
+
this.flushing = false;
|
|
456
|
+
if (this.pendingFlush) {
|
|
457
|
+
this.pendingFlush = false;
|
|
458
|
+
this.flush();
|
|
459
|
+
}
|
|
354
460
|
}
|
|
355
461
|
}
|
|
356
462
|
stop() {
|
|
@@ -366,149 +472,19 @@ class Transmitter {
|
|
|
366
472
|
}, this.flushInterval);
|
|
367
473
|
}
|
|
368
474
|
}
|
|
369
|
-
class EventCollector {
|
|
370
|
-
constructor(transmitter) {
|
|
371
|
-
__publicField(this, "transmitter");
|
|
372
|
-
__publicField(this, "listeners", {});
|
|
373
|
-
this.transmitter = transmitter;
|
|
374
|
-
}
|
|
375
|
-
start() {
|
|
376
|
-
this.attachListeners();
|
|
377
|
-
}
|
|
378
|
-
stop() {
|
|
379
|
-
this.detachListeners();
|
|
380
|
-
}
|
|
381
|
-
attachListeners() {
|
|
382
|
-
this.listeners["mousemove"] = this.throttle((e) => {
|
|
383
|
-
const mouseEvent = e;
|
|
384
|
-
this.transmitter.send("mousemove", {
|
|
385
|
-
x: mouseEvent.clientX,
|
|
386
|
-
y: mouseEvent.clientY,
|
|
387
|
-
timestamp: Date.now()
|
|
388
|
-
});
|
|
389
|
-
}, 100);
|
|
390
|
-
this.listeners["click"] = (e) => {
|
|
391
|
-
const mouseEvent = e;
|
|
392
|
-
this.transmitter.send("click", {
|
|
393
|
-
x: mouseEvent.clientX,
|
|
394
|
-
y: mouseEvent.clientY,
|
|
395
|
-
target: mouseEvent.target.tagName,
|
|
396
|
-
timestamp: Date.now()
|
|
397
|
-
});
|
|
398
|
-
};
|
|
399
|
-
this.listeners["keydown"] = (e) => {
|
|
400
|
-
const keyEvent = e;
|
|
401
|
-
this.transmitter.send("keydown", {
|
|
402
|
-
key: keyEvent.key,
|
|
403
|
-
timestamp: Date.now()
|
|
404
|
-
});
|
|
405
|
-
};
|
|
406
|
-
window.addEventListener("mousemove", this.listeners["mousemove"]);
|
|
407
|
-
window.addEventListener("click", this.listeners["click"]);
|
|
408
|
-
window.addEventListener("keydown", this.listeners["keydown"]);
|
|
409
|
-
}
|
|
410
|
-
detachListeners() {
|
|
411
|
-
if (this.listeners["mousemove"]) {
|
|
412
|
-
window.removeEventListener("mousemove", this.listeners["mousemove"]);
|
|
413
|
-
}
|
|
414
|
-
if (this.listeners["click"]) {
|
|
415
|
-
window.removeEventListener("click", this.listeners["click"]);
|
|
416
|
-
}
|
|
417
|
-
if (this.listeners["keydown"]) {
|
|
418
|
-
window.removeEventListener("keydown", this.listeners["keydown"]);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
throttle(func, limit) {
|
|
422
|
-
let inThrottle;
|
|
423
|
-
return function(...args) {
|
|
424
|
-
if (!inThrottle) {
|
|
425
|
-
func.apply(this, args);
|
|
426
|
-
inThrottle = true;
|
|
427
|
-
setTimeout(() => inThrottle = false, limit);
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
475
|
class BehaviorCollector {
|
|
433
476
|
constructor(transmitter) {
|
|
477
|
+
// @ts-ignore kept for future extension (mouse/keyboard-외 행동 수집)
|
|
434
478
|
__publicField(this, "transmitter");
|
|
435
|
-
__publicField(this, "trajectoryBuffer", []);
|
|
436
|
-
__publicField(this, "keystrokeBuffer", []);
|
|
437
479
|
__publicField(this, "isCollecting", false);
|
|
438
|
-
__publicField(this, "flushInterval", 3e3);
|
|
439
|
-
// Send batch every 3 seconds
|
|
440
|
-
__publicField(this, "flushTimer");
|
|
441
|
-
__publicField(this, "lastMouseMoveTime", 0);
|
|
442
|
-
__publicField(this, "onMouseMove", (e) => {
|
|
443
|
-
const now = Date.now();
|
|
444
|
-
if (now - this.lastMouseMoveTime > 50) {
|
|
445
|
-
this.trajectoryBuffer.push({
|
|
446
|
-
x: e.clientX,
|
|
447
|
-
y: e.clientY,
|
|
448
|
-
t: now
|
|
449
|
-
});
|
|
450
|
-
this.lastMouseMoveTime = now;
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
__publicField(this, "onKeyDown", (e) => {
|
|
454
|
-
this.keystrokeBuffer.push({
|
|
455
|
-
code: e.code,
|
|
456
|
-
t: Date.now(),
|
|
457
|
-
type: "down"
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
__publicField(this, "onKeyUp", (e) => {
|
|
461
|
-
this.keystrokeBuffer.push({
|
|
462
|
-
code: e.code,
|
|
463
|
-
t: Date.now(),
|
|
464
|
-
type: "up"
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
480
|
this.transmitter = transmitter;
|
|
468
481
|
}
|
|
469
482
|
start() {
|
|
470
483
|
if (this.isCollecting) return;
|
|
471
484
|
this.isCollecting = true;
|
|
472
|
-
this.attachListeners();
|
|
473
|
-
this.startFlushTimer();
|
|
474
485
|
}
|
|
475
486
|
stop() {
|
|
476
487
|
this.isCollecting = false;
|
|
477
|
-
this.detachListeners();
|
|
478
|
-
this.stopFlushTimer();
|
|
479
|
-
this.flush();
|
|
480
|
-
}
|
|
481
|
-
attachListeners() {
|
|
482
|
-
window.addEventListener("mousemove", this.onMouseMove);
|
|
483
|
-
window.addEventListener("keydown", this.onKeyDown);
|
|
484
|
-
window.addEventListener("keyup", this.onKeyUp);
|
|
485
|
-
}
|
|
486
|
-
detachListeners() {
|
|
487
|
-
window.removeEventListener("mousemove", this.onMouseMove);
|
|
488
|
-
window.removeEventListener("keydown", this.onKeyDown);
|
|
489
|
-
window.removeEventListener("keyup", this.onKeyUp);
|
|
490
|
-
}
|
|
491
|
-
startFlushTimer() {
|
|
492
|
-
this.flushTimer = setInterval(() => {
|
|
493
|
-
this.flush();
|
|
494
|
-
}, this.flushInterval);
|
|
495
|
-
}
|
|
496
|
-
stopFlushTimer() {
|
|
497
|
-
if (this.flushTimer) {
|
|
498
|
-
clearInterval(this.flushTimer);
|
|
499
|
-
this.flushTimer = null;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
flush() {
|
|
503
|
-
if (this.trajectoryBuffer.length === 0 && this.keystrokeBuffer.length === 0) return;
|
|
504
|
-
const data = {
|
|
505
|
-
trajectory: [...this.trajectoryBuffer],
|
|
506
|
-
keystrokes: [...this.keystrokeBuffer],
|
|
507
|
-
url: window.location.href
|
|
508
|
-
};
|
|
509
|
-
this.trajectoryBuffer = [];
|
|
510
|
-
this.keystrokeBuffer = [];
|
|
511
|
-
this.transmitter.send("behavior_data", data);
|
|
512
488
|
}
|
|
513
489
|
}
|
|
514
490
|
class RapidClickDetector {
|
|
@@ -603,10 +579,14 @@ class MouseTracker {
|
|
|
603
579
|
__publicField(this, "transmitter");
|
|
604
580
|
__publicField(this, "wasmService");
|
|
605
581
|
__publicField(this, "buffer", []);
|
|
606
|
-
__publicField(this, "BUFFER_SIZE",
|
|
607
|
-
|
|
608
|
-
|
|
582
|
+
__publicField(this, "BUFFER_SIZE", 50);
|
|
583
|
+
// 50 points for accurate entropy (was 10)
|
|
584
|
+
__publicField(this, "SAMPLE_INTERVAL", 100);
|
|
585
|
+
// 100ms between samples (was 50)
|
|
586
|
+
__publicField(this, "SEND_COOLDOWN", 5e3);
|
|
587
|
+
// 5s between server transmissions
|
|
609
588
|
__publicField(this, "lastSampleTime", 0);
|
|
589
|
+
__publicField(this, "lastSendTime", 0);
|
|
610
590
|
__publicField(this, "handleMouseMove", (e) => {
|
|
611
591
|
const now = Date.now();
|
|
612
592
|
if (now - this.lastSampleTime < this.SAMPLE_INTERVAL) return;
|
|
@@ -614,7 +594,10 @@ class MouseTracker {
|
|
|
614
594
|
this.buffer.push({ x: e.clientX, y: e.clientY, time: now });
|
|
615
595
|
if (this.buffer.length > this.BUFFER_SIZE) {
|
|
616
596
|
this.buffer.shift();
|
|
597
|
+
}
|
|
598
|
+
if (this.buffer.length >= this.BUFFER_SIZE && now - this.lastSendTime >= this.SEND_COOLDOWN) {
|
|
617
599
|
this.analyze();
|
|
600
|
+
this.lastSendTime = now;
|
|
618
601
|
}
|
|
619
602
|
});
|
|
620
603
|
this.transmitter = transmitter;
|
|
@@ -631,15 +614,17 @@ class MouseTracker {
|
|
|
631
614
|
}
|
|
632
615
|
}
|
|
633
616
|
analyze() {
|
|
634
|
-
if (this.buffer.length <
|
|
617
|
+
if (this.buffer.length < 20) return;
|
|
635
618
|
const entropy = this.wasmService.calculateEntropy(this.buffer);
|
|
636
619
|
const speedVariance = this.calculateSpeedVariance();
|
|
620
|
+
const rawTrajectory = this.buffer.map((p) => ({ x: p.x, y: p.y, t: p.time }));
|
|
637
621
|
this.transmitter.send("behavior_metrics", {
|
|
638
622
|
entropy,
|
|
639
623
|
speedVariance,
|
|
640
|
-
|
|
624
|
+
sampleSize: this.buffer.length,
|
|
625
|
+
timestamp: Date.now(),
|
|
626
|
+
rawTrajectory
|
|
641
627
|
});
|
|
642
|
-
this.buffer = [];
|
|
643
628
|
}
|
|
644
629
|
// Speed Variance is simple enough to keep in JS or move to Wasm later.
|
|
645
630
|
calculateSpeedVariance() {
|
|
@@ -660,8 +645,6 @@ class MouseTracker {
|
|
|
660
645
|
}
|
|
661
646
|
}
|
|
662
647
|
class InputTracker {
|
|
663
|
-
// Buffer for raw input events - currently unused but kept for potential future raw data analysis
|
|
664
|
-
// private inputBuffer: any[] = [];
|
|
665
648
|
constructor(transmitter) {
|
|
666
649
|
__publicField(this, "lastInteractionTime", 0);
|
|
667
650
|
__publicField(this, "lastInteractionType", "none");
|
|
@@ -676,6 +659,8 @@ class InputTracker {
|
|
|
676
659
|
// Suspicious focus tracking
|
|
677
660
|
__publicField(this, "suspiciousFocusCount", 0);
|
|
678
661
|
__publicField(this, "suspiciousFocusResetTimer", null);
|
|
662
|
+
// Raw keystroke buffer for ML training
|
|
663
|
+
__publicField(this, "rawKeystrokes", []);
|
|
679
664
|
this.transmitter = transmitter;
|
|
680
665
|
this.isMobile = this.detectMobile();
|
|
681
666
|
}
|
|
@@ -700,6 +685,7 @@ class InputTracker {
|
|
|
700
685
|
this.checkFocusIntegrity();
|
|
701
686
|
this.flightTimes = [];
|
|
702
687
|
this.dwellTimes = [];
|
|
688
|
+
this.rawKeystrokes = [];
|
|
703
689
|
this.lastKeyUpTime = 0;
|
|
704
690
|
}
|
|
705
691
|
}, { capture: true });
|
|
@@ -708,6 +694,7 @@ class InputTracker {
|
|
|
708
694
|
if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
|
|
709
695
|
const now = Date.now();
|
|
710
696
|
this.keyPressTimes.set(e.code, now);
|
|
697
|
+
this.rawKeystrokes.push({ code: e.code, t: now, type: "down" });
|
|
711
698
|
if (this.lastKeyUpTime > 0) {
|
|
712
699
|
const flight = now - this.lastKeyUpTime;
|
|
713
700
|
if (flight < 2e3) {
|
|
@@ -720,6 +707,7 @@ class InputTracker {
|
|
|
720
707
|
if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
|
|
721
708
|
const now = Date.now();
|
|
722
709
|
this.lastKeyUpTime = now;
|
|
710
|
+
this.rawKeystrokes.push({ code: e.code, t: now, type: "up" });
|
|
723
711
|
const startTime = this.keyPressTimes.get(e.code);
|
|
724
712
|
if (startTime) {
|
|
725
713
|
const dwell = now - startTime;
|
|
@@ -740,7 +728,7 @@ class InputTracker {
|
|
|
740
728
|
checkFocusIntegrity() {
|
|
741
729
|
const now = Date.now();
|
|
742
730
|
const timeDiff = now - this.lastInteractionTime;
|
|
743
|
-
if (performance.now() <
|
|
731
|
+
if (performance.now() < 5e3) return;
|
|
744
732
|
if (timeDiff > 200) {
|
|
745
733
|
this.suspiciousFocusCount++;
|
|
746
734
|
clearTimeout(this.suspiciousFocusResetTimer);
|
|
@@ -754,15 +742,6 @@ class InputTracker {
|
|
|
754
742
|
consecutiveCount: this.suspiciousFocusCount,
|
|
755
743
|
timestamp: now
|
|
756
744
|
});
|
|
757
|
-
if (this.suspiciousFocusCount >= 3) {
|
|
758
|
-
this.transmitter.send("integrity_violation", {
|
|
759
|
-
type: "programmatic_focus",
|
|
760
|
-
details: `${this.suspiciousFocusCount} consecutive input focuses without click/tab`,
|
|
761
|
-
consecutiveCount: this.suspiciousFocusCount,
|
|
762
|
-
timestamp: now
|
|
763
|
-
});
|
|
764
|
-
this.suspiciousFocusCount = 0;
|
|
765
|
-
}
|
|
766
745
|
} else {
|
|
767
746
|
this.suspiciousFocusCount = 0;
|
|
768
747
|
}
|
|
@@ -784,10 +763,13 @@ class InputTracker {
|
|
|
784
763
|
variance: varDwell
|
|
785
764
|
},
|
|
786
765
|
isMobile: this.isMobile,
|
|
787
|
-
timestamp: Date.now()
|
|
766
|
+
timestamp: Date.now(),
|
|
767
|
+
rawKeystrokes: [...this.rawKeystrokes]
|
|
768
|
+
// ML training data (~10-20 entries, ~800B)
|
|
788
769
|
});
|
|
789
770
|
this.flightTimes = [];
|
|
790
771
|
this.dwellTimes = [];
|
|
772
|
+
this.rawKeystrokes = [];
|
|
791
773
|
}
|
|
792
774
|
average(data) {
|
|
793
775
|
if (data.length === 0) return 0;
|
|
@@ -862,22 +844,32 @@ class WasmService {
|
|
|
862
844
|
}
|
|
863
845
|
}
|
|
864
846
|
class CanvasFingerprinter {
|
|
865
|
-
|
|
847
|
+
// wasmService kept in constructor signature for backward compatibility
|
|
848
|
+
// but no longer used — fingerprint hashing upgraded to SHA-256 via Web Crypto
|
|
849
|
+
constructor(transmitter, _wasmService) {
|
|
866
850
|
__publicField(this, "transmitter");
|
|
867
|
-
__publicField(this, "wasmService");
|
|
868
851
|
__publicField(this, "hasRun", false);
|
|
869
852
|
this.transmitter = transmitter;
|
|
870
|
-
this.wasmService = wasmService;
|
|
871
853
|
}
|
|
872
854
|
start() {
|
|
873
855
|
if (this.hasRun || typeof document === "undefined") return;
|
|
874
|
-
setTimeout(() => {
|
|
875
|
-
const
|
|
876
|
-
this.
|
|
856
|
+
setTimeout(async () => {
|
|
857
|
+
const canvasHash = await this.generateFingerprint();
|
|
858
|
+
const webglFp = this.collectWebGLFingerprint();
|
|
859
|
+
const audioFp = this.collectAudioFingerprint();
|
|
860
|
+
this.transmitter.send("fingerprint_collected", {
|
|
861
|
+
hash: canvasHash,
|
|
862
|
+
webgl: webglFp,
|
|
863
|
+
audio: audioFp
|
|
864
|
+
});
|
|
877
865
|
this.hasRun = true;
|
|
878
866
|
}, 500);
|
|
879
867
|
}
|
|
880
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Generate canvas fingerprint using SHA-256 (upgraded from 32-bit FNV hash).
|
|
870
|
+
* SHA-256 eliminates collision risk at scale (2^128 vs ~77K for 32-bit).
|
|
871
|
+
*/
|
|
872
|
+
async generateFingerprint() {
|
|
881
873
|
try {
|
|
882
874
|
const canvas = document.createElement("canvas");
|
|
883
875
|
const ctx = canvas.getContext("2d");
|
|
@@ -908,18 +900,75 @@ class CanvasFingerprinter {
|
|
|
908
900
|
ctx.closePath();
|
|
909
901
|
ctx.fill();
|
|
910
902
|
const dataUrl = canvas.toDataURL();
|
|
911
|
-
const hash = this.
|
|
912
|
-
return
|
|
903
|
+
const hash = await this.sha256(dataUrl);
|
|
904
|
+
return hash;
|
|
913
905
|
} catch (e) {
|
|
914
906
|
return "error";
|
|
915
907
|
}
|
|
916
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Compute SHA-256 hex digest using Web Crypto API.
|
|
911
|
+
*/
|
|
912
|
+
async sha256(data) {
|
|
913
|
+
const encoded = new TextEncoder().encode(data);
|
|
914
|
+
const hashBuffer = await window.crypto.subtle.digest("SHA-256", encoded);
|
|
915
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
916
|
+
return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Collect WebGL fingerprint data for cross-signal consistency validation.
|
|
920
|
+
* Includes renderer, vendor, and hardware parameter profile.
|
|
921
|
+
*/
|
|
922
|
+
collectWebGLFingerprint() {
|
|
923
|
+
try {
|
|
924
|
+
const canvas = document.createElement("canvas");
|
|
925
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
926
|
+
if (!gl) return { available: false };
|
|
927
|
+
const g = gl;
|
|
928
|
+
const result = { available: true };
|
|
929
|
+
const debugInfo = g.getExtension("WEBGL_debug_renderer_info");
|
|
930
|
+
if (debugInfo) {
|
|
931
|
+
result.renderer = g.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
932
|
+
result.vendor = g.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
933
|
+
}
|
|
934
|
+
result.maxTextureSize = g.getParameter(g.MAX_TEXTURE_SIZE);
|
|
935
|
+
result.maxViewportDims = Array.from(g.getParameter(g.MAX_VIEWPORT_DIMS));
|
|
936
|
+
result.maxRenderbufferSize = g.getParameter(g.MAX_RENDERBUFFER_SIZE);
|
|
937
|
+
result.maxVertexAttribs = g.getParameter(g.MAX_VERTEX_ATTRIBS);
|
|
938
|
+
result.extensions = (g.getSupportedExtensions() || []).length;
|
|
939
|
+
return result;
|
|
940
|
+
} catch (e) {
|
|
941
|
+
return { available: false, error: true };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* AudioContext fingerprint — the audio stack produces unique output
|
|
946
|
+
* per hardware/OS/browser combination, even for silent oscillators.
|
|
947
|
+
*/
|
|
948
|
+
collectAudioFingerprint() {
|
|
949
|
+
try {
|
|
950
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
951
|
+
if (!AudioCtx) return { available: false };
|
|
952
|
+
const ctx = new AudioCtx();
|
|
953
|
+
const result = {
|
|
954
|
+
available: true,
|
|
955
|
+
sampleRate: ctx.sampleRate,
|
|
956
|
+
state: ctx.state,
|
|
957
|
+
maxChannels: ctx.destination.maxChannelCount
|
|
958
|
+
};
|
|
959
|
+
ctx.close();
|
|
960
|
+
return result;
|
|
961
|
+
} catch (e) {
|
|
962
|
+
return { available: false };
|
|
963
|
+
}
|
|
964
|
+
}
|
|
917
965
|
}
|
|
918
966
|
class ChallengeHandler {
|
|
919
967
|
constructor(endpoint) {
|
|
920
968
|
__publicField(this, "originalFetch");
|
|
921
969
|
__publicField(this, "endpoint");
|
|
922
970
|
__publicField(this, "callbackManager", null);
|
|
971
|
+
__publicField(this, "challengeInProgress", null);
|
|
923
972
|
this.endpoint = endpoint;
|
|
924
973
|
this.originalFetch = window.fetch.bind(window);
|
|
925
974
|
}
|
|
@@ -946,7 +995,13 @@ class ChallengeHandler {
|
|
|
946
995
|
return this.originalFetch(input, init);
|
|
947
996
|
}
|
|
948
997
|
}
|
|
949
|
-
|
|
998
|
+
if (!this.challengeInProgress) {
|
|
999
|
+
this.challengeInProgress = this.showCaptchaModal().finally(() => {
|
|
1000
|
+
this.challengeInProgress = null;
|
|
1001
|
+
});
|
|
1002
|
+
} else {
|
|
1003
|
+
}
|
|
1004
|
+
const passed = await this.challengeInProgress;
|
|
950
1005
|
if (passed) {
|
|
951
1006
|
return this.originalFetch(input, init);
|
|
952
1007
|
} else {
|
|
@@ -1160,10 +1215,1315 @@ class InvisibleInteraction {
|
|
|
1160
1215
|
return null;
|
|
1161
1216
|
}
|
|
1162
1217
|
}
|
|
1218
|
+
class AutomationDetector {
|
|
1219
|
+
constructor(transmitter) {
|
|
1220
|
+
__publicField(this, "transmitter");
|
|
1221
|
+
__publicField(this, "hasRun", false);
|
|
1222
|
+
this.transmitter = transmitter;
|
|
1223
|
+
}
|
|
1224
|
+
start() {
|
|
1225
|
+
if (this.hasRun || typeof window === "undefined") return;
|
|
1226
|
+
setTimeout(() => this.detect(), 1500);
|
|
1227
|
+
this.hasRun = true;
|
|
1228
|
+
}
|
|
1229
|
+
detect() {
|
|
1230
|
+
var _a, _b;
|
|
1231
|
+
const signals = {};
|
|
1232
|
+
let score = 0;
|
|
1233
|
+
if (navigator.webdriver === true) {
|
|
1234
|
+
signals.webdriver = true;
|
|
1235
|
+
score += 40;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
const cdpResult = this.detectCDP();
|
|
1239
|
+
if (cdpResult) {
|
|
1240
|
+
signals.cdp = true;
|
|
1241
|
+
score += 50;
|
|
1242
|
+
}
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
}
|
|
1245
|
+
const globals = this.detectAutomationGlobals();
|
|
1246
|
+
if (globals.length > 0) {
|
|
1247
|
+
signals.automationGlobals = globals;
|
|
1248
|
+
score += 30;
|
|
1249
|
+
}
|
|
1250
|
+
const webglInfo = this.checkWebGL();
|
|
1251
|
+
if (webglInfo.suspicious) {
|
|
1252
|
+
signals.webgl = webglInfo;
|
|
1253
|
+
score += 25;
|
|
1254
|
+
}
|
|
1255
|
+
const inconsistencies = this.checkInconsistencies();
|
|
1256
|
+
if (inconsistencies.length > 0) {
|
|
1257
|
+
signals.inconsistencies = inconsistencies;
|
|
1258
|
+
score += 15;
|
|
1259
|
+
}
|
|
1260
|
+
const headlessSignals = this.checkHeadless();
|
|
1261
|
+
if (headlessSignals.score > 0) {
|
|
1262
|
+
signals.headless = headlessSignals.flags;
|
|
1263
|
+
score += headlessSignals.score;
|
|
1264
|
+
}
|
|
1265
|
+
if (score > 0) {
|
|
1266
|
+
this.transmitter.send("automation_detected", {
|
|
1267
|
+
signals,
|
|
1268
|
+
score,
|
|
1269
|
+
timestamp: Date.now()
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
this.transmitter.send("init_signals", {
|
|
1273
|
+
webdriver: !!navigator.webdriver,
|
|
1274
|
+
plugins: ((_a = navigator.plugins) == null ? void 0 : _a.length) ?? 0,
|
|
1275
|
+
languages: ((_b = navigator.languages) == null ? void 0 : _b.length) ?? 0,
|
|
1276
|
+
platform: navigator.platform,
|
|
1277
|
+
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
1278
|
+
deviceMemory: navigator.deviceMemory,
|
|
1279
|
+
maxTouchPoints: navigator.maxTouchPoints,
|
|
1280
|
+
webglRenderer: webglInfo.renderer,
|
|
1281
|
+
webglVendor: webglInfo.vendor,
|
|
1282
|
+
timestamp: Date.now()
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* CDP serialization detection.
|
|
1287
|
+
* When CDP's Runtime domain is active (Playwright/Puppeteer),
|
|
1288
|
+
* console.log triggers serialization of objects, invoking getters.
|
|
1289
|
+
* In normal browsers without DevTools open, getters are NOT invoked.
|
|
1290
|
+
*/
|
|
1291
|
+
detectCDP() {
|
|
1292
|
+
let detected = false;
|
|
1293
|
+
const orig = console.debug;
|
|
1294
|
+
console.debug = orig;
|
|
1295
|
+
return detected;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Check for automation framework globals injected into window.
|
|
1299
|
+
*/
|
|
1300
|
+
detectAutomationGlobals() {
|
|
1301
|
+
const found = [];
|
|
1302
|
+
const checks = [
|
|
1303
|
+
// Selenium
|
|
1304
|
+
["selenium", () => !!window._selenium],
|
|
1305
|
+
["selenium_unwrapped", () => !!document.__selenium_unwrapped],
|
|
1306
|
+
["driver_evaluate", () => !!document.__driver_evaluate],
|
|
1307
|
+
["webdriver_evaluate", () => !!document.__webdriver_evaluate],
|
|
1308
|
+
["driver_unwrapped", () => !!document.__driver_unwrapped],
|
|
1309
|
+
["fxdriver", () => !!document.__fxdriver_evaluate],
|
|
1310
|
+
// Playwright
|
|
1311
|
+
["playwright", () => !!window.__playwright],
|
|
1312
|
+
["playwright_binding", () => !!window.__playwright__binding__],
|
|
1313
|
+
["pwInitScripts", () => !!window.__pwInitScripts],
|
|
1314
|
+
// Puppeteer
|
|
1315
|
+
["puppeteer", () => !!window.__puppeteer_evaluation_script__],
|
|
1316
|
+
// PhantomJS
|
|
1317
|
+
["phantom", () => !!window.callPhantom || !!window._phantom],
|
|
1318
|
+
// Nightmare
|
|
1319
|
+
["nightmare", () => !!window.__nightmare],
|
|
1320
|
+
// Generic CDP
|
|
1321
|
+
["cdc", () => {
|
|
1322
|
+
return Object.keys(document).some((k) => k.startsWith("cdc_") || k.startsWith("$cdc_"));
|
|
1323
|
+
}]
|
|
1324
|
+
];
|
|
1325
|
+
for (const [name, check] of checks) {
|
|
1326
|
+
try {
|
|
1327
|
+
if (check()) found.push(name);
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return found;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Check WebGL renderer for headless indicators.
|
|
1335
|
+
* SwiftShader, Mesa OffScreen, and ANGLE (software) are headless markers.
|
|
1336
|
+
*/
|
|
1337
|
+
checkWebGL() {
|
|
1338
|
+
try {
|
|
1339
|
+
const canvas = document.createElement("canvas");
|
|
1340
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1341
|
+
if (!gl) return { suspicious: false, renderer: "unavailable", vendor: "unavailable" };
|
|
1342
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1343
|
+
if (!debugInfo) return { suspicious: false, renderer: "no_debug_info", vendor: "no_debug_info" };
|
|
1344
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "";
|
|
1345
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "";
|
|
1346
|
+
const suspicious = /SwiftShader/i.test(renderer) || /Mesa OffScreen/i.test(renderer) || /llvmpipe/i.test(renderer);
|
|
1347
|
+
return { suspicious, renderer, vendor };
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
return { suspicious: false, renderer: "error", vendor: "error" };
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Cross-signal consistency checks.
|
|
1354
|
+
* Compares multiple browser properties that should be correlated.
|
|
1355
|
+
*/
|
|
1356
|
+
checkInconsistencies() {
|
|
1357
|
+
const issues = [];
|
|
1358
|
+
try {
|
|
1359
|
+
const ua = navigator.userAgent;
|
|
1360
|
+
const platform = navigator.platform;
|
|
1361
|
+
if (/Windows/.test(ua) && /Mac/.test(platform)) {
|
|
1362
|
+
issues.push("ua_platform_mismatch_win_mac");
|
|
1363
|
+
}
|
|
1364
|
+
if (/Macintosh/.test(ua) && /Win/.test(platform)) {
|
|
1365
|
+
issues.push("ua_platform_mismatch_mac_win");
|
|
1366
|
+
}
|
|
1367
|
+
if (/Mobile|Android|iPhone/.test(ua) && navigator.maxTouchPoints === 0) {
|
|
1368
|
+
issues.push("mobile_ua_no_touch");
|
|
1369
|
+
}
|
|
1370
|
+
if (/Chrome/.test(ua) && navigator.plugins && navigator.plugins.length === 0) {
|
|
1371
|
+
issues.push("chrome_no_plugins");
|
|
1372
|
+
}
|
|
1373
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
1374
|
+
issues.push("no_languages");
|
|
1375
|
+
}
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
}
|
|
1378
|
+
return issues;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Additional headless Chrome detection signals.
|
|
1382
|
+
*/
|
|
1383
|
+
checkHeadless() {
|
|
1384
|
+
const flags = [];
|
|
1385
|
+
let score = 0;
|
|
1386
|
+
try {
|
|
1387
|
+
if (/HeadlessChrome/.test(navigator.userAgent)) {
|
|
1388
|
+
flags.push("headless_ua");
|
|
1389
|
+
score += 40;
|
|
1390
|
+
}
|
|
1391
|
+
if (/Chrome/.test(navigator.userAgent) && !window.chrome) {
|
|
1392
|
+
flags.push("chrome_ua_no_chrome_obj");
|
|
1393
|
+
score += 15;
|
|
1394
|
+
}
|
|
1395
|
+
if (window.screen && window.screen.width === 800 && window.screen.height === 600) {
|
|
1396
|
+
flags.push("default_screen_size");
|
|
1397
|
+
score += 10;
|
|
1398
|
+
}
|
|
1399
|
+
if ("Notification" in window) {
|
|
1400
|
+
const perm = window.Notification.permission;
|
|
1401
|
+
if (perm === "denied" && navigator.userAgent.includes("Chrome")) {
|
|
1402
|
+
flags.push("notification_denied_default");
|
|
1403
|
+
score += 5;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const conn = navigator.connection;
|
|
1407
|
+
if (conn && conn.rtt === 0) {
|
|
1408
|
+
flags.push("zero_rtt");
|
|
1409
|
+
score += 10;
|
|
1410
|
+
}
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
}
|
|
1413
|
+
return { score, flags };
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
class SessionAnalyzer {
|
|
1417
|
+
constructor(transmitter) {
|
|
1418
|
+
__publicField(this, "transmitter");
|
|
1419
|
+
__publicField(this, "sessionStart", 0);
|
|
1420
|
+
__publicField(this, "pageTransitions", []);
|
|
1421
|
+
__publicField(this, "interactionTimestamps", []);
|
|
1422
|
+
__publicField(this, "lastPageUrl", "");
|
|
1423
|
+
__publicField(this, "formStartTimes", /* @__PURE__ */ new Map());
|
|
1424
|
+
// @ts-ignore
|
|
1425
|
+
__publicField(this, "reportInterval", null);
|
|
1426
|
+
this.transmitter = transmitter;
|
|
1427
|
+
}
|
|
1428
|
+
start() {
|
|
1429
|
+
if (typeof window === "undefined") return;
|
|
1430
|
+
this.sessionStart = Date.now();
|
|
1431
|
+
this.lastPageUrl = window.location.href;
|
|
1432
|
+
const events = ["mousedown", "keydown", "touchstart", "scroll"];
|
|
1433
|
+
events.forEach((evt) => {
|
|
1434
|
+
document.addEventListener(evt, () => {
|
|
1435
|
+
this.interactionTimestamps.push(Date.now());
|
|
1436
|
+
if (this.interactionTimestamps.length > 500) {
|
|
1437
|
+
this.interactionTimestamps.shift();
|
|
1438
|
+
}
|
|
1439
|
+
}, { passive: true, capture: true });
|
|
1440
|
+
});
|
|
1441
|
+
this.observeNavigation();
|
|
1442
|
+
this.observeForms();
|
|
1443
|
+
this.reportInterval = setInterval(() => this.report(), 15e3);
|
|
1444
|
+
}
|
|
1445
|
+
observeNavigation() {
|
|
1446
|
+
const origPushState = history.pushState;
|
|
1447
|
+
const origReplaceState = history.replaceState;
|
|
1448
|
+
const self = this;
|
|
1449
|
+
history.pushState = function(data, unused, url) {
|
|
1450
|
+
origPushState.call(this, data, unused, url);
|
|
1451
|
+
self.onPageTransition();
|
|
1452
|
+
};
|
|
1453
|
+
history.replaceState = function(data, unused, url) {
|
|
1454
|
+
origReplaceState.call(this, data, unused, url);
|
|
1455
|
+
self.onPageTransition();
|
|
1456
|
+
};
|
|
1457
|
+
window.addEventListener("popstate", () => this.onPageTransition());
|
|
1458
|
+
}
|
|
1459
|
+
onPageTransition() {
|
|
1460
|
+
const now = Date.now();
|
|
1461
|
+
const newUrl = window.location.href;
|
|
1462
|
+
if (newUrl !== this.lastPageUrl) {
|
|
1463
|
+
this.pageTransitions.push(now);
|
|
1464
|
+
this.lastPageUrl = newUrl;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
observeForms() {
|
|
1468
|
+
document.addEventListener("focus", (e) => {
|
|
1469
|
+
const target = e.target;
|
|
1470
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") {
|
|
1471
|
+
const form = target.closest("form");
|
|
1472
|
+
const formId = (form == null ? void 0 : form.id) || (form == null ? void 0 : form.action) || "anonymous_form";
|
|
1473
|
+
if (!this.formStartTimes.has(formId)) {
|
|
1474
|
+
this.formStartTimes.set(formId, Date.now());
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}, { capture: true });
|
|
1478
|
+
document.addEventListener("submit", (e) => {
|
|
1479
|
+
const form = e.target;
|
|
1480
|
+
const formId = form.id || form.action || "anonymous_form";
|
|
1481
|
+
const startTime = this.formStartTimes.get(formId);
|
|
1482
|
+
if (startTime) {
|
|
1483
|
+
const duration = Date.now() - startTime;
|
|
1484
|
+
this.transmitter.send("session_metrics", {
|
|
1485
|
+
type: "form_completion",
|
|
1486
|
+
formId,
|
|
1487
|
+
duration,
|
|
1488
|
+
timestamp: Date.now()
|
|
1489
|
+
});
|
|
1490
|
+
this.formStartTimes.delete(formId);
|
|
1491
|
+
}
|
|
1492
|
+
}, { capture: true });
|
|
1493
|
+
}
|
|
1494
|
+
report() {
|
|
1495
|
+
const now = Date.now();
|
|
1496
|
+
const sessionDuration = now - this.sessionStart;
|
|
1497
|
+
if (this.interactionTimestamps.length < 10) return;
|
|
1498
|
+
const intervals = this.calculateIntervals(this.interactionTimestamps);
|
|
1499
|
+
const timingMetrics = this.analyzeTimingDistribution(intervals);
|
|
1500
|
+
const pageMetrics = this.analyzePageTransitions();
|
|
1501
|
+
this.transmitter.send("session_metrics", {
|
|
1502
|
+
type: "session_analysis",
|
|
1503
|
+
sessionDuration,
|
|
1504
|
+
totalInteractions: this.interactionTimestamps.length,
|
|
1505
|
+
pageTransitions: this.pageTransitions.length,
|
|
1506
|
+
timing: timingMetrics,
|
|
1507
|
+
pages: pageMetrics,
|
|
1508
|
+
timestamp: now
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
calculateIntervals(timestamps) {
|
|
1512
|
+
const intervals = [];
|
|
1513
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
1514
|
+
intervals.push(timestamps[i] - timestamps[i - 1]);
|
|
1515
|
+
}
|
|
1516
|
+
return intervals;
|
|
1517
|
+
}
|
|
1518
|
+
analyzeTimingDistribution(intervals) {
|
|
1519
|
+
if (intervals.length < 5) return { samples: intervals.length };
|
|
1520
|
+
const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1521
|
+
const variance = intervals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / intervals.length;
|
|
1522
|
+
const stdDev = Math.sqrt(variance);
|
|
1523
|
+
const cv = mean > 0 ? stdDev / mean : 0;
|
|
1524
|
+
const entropy = this.shannonEntropy(intervals, 50);
|
|
1525
|
+
const autocorr = this.autocorrelation(intervals);
|
|
1526
|
+
return {
|
|
1527
|
+
mean: Math.round(mean),
|
|
1528
|
+
stdDev: Math.round(stdDev),
|
|
1529
|
+
cv: parseFloat(cv.toFixed(4)),
|
|
1530
|
+
entropy: parseFloat(entropy.toFixed(4)),
|
|
1531
|
+
autocorrelation: parseFloat(autocorr.toFixed(4)),
|
|
1532
|
+
samples: intervals.length
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
analyzePageTransitions() {
|
|
1536
|
+
if (this.pageTransitions.length < 2) {
|
|
1537
|
+
return { count: this.pageTransitions.length };
|
|
1538
|
+
}
|
|
1539
|
+
const intervals = this.calculateIntervals(this.pageTransitions);
|
|
1540
|
+
const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1541
|
+
const variance = intervals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / intervals.length;
|
|
1542
|
+
const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
|
|
1543
|
+
return {
|
|
1544
|
+
count: this.pageTransitions.length,
|
|
1545
|
+
avgInterval: Math.round(mean),
|
|
1546
|
+
cv: parseFloat(cv.toFixed(4))
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Shannon entropy of intervals binned into buckets.
|
|
1551
|
+
* Low entropy = predictable/regular timing = bot-like.
|
|
1552
|
+
*/
|
|
1553
|
+
shannonEntropy(values, binSize) {
|
|
1554
|
+
const bins = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const v of values) {
|
|
1556
|
+
const bin = Math.floor(v / binSize);
|
|
1557
|
+
bins.set(bin, (bins.get(bin) || 0) + 1);
|
|
1558
|
+
}
|
|
1559
|
+
const total = values.length;
|
|
1560
|
+
let entropy = 0;
|
|
1561
|
+
for (const count of bins.values()) {
|
|
1562
|
+
const p = count / total;
|
|
1563
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
1564
|
+
}
|
|
1565
|
+
return entropy;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Autocorrelation at lag-1.
|
|
1569
|
+
* Human timing: moderate positive autocorrelation (motor control inertia).
|
|
1570
|
+
* Random delays: near-zero autocorrelation.
|
|
1571
|
+
*/
|
|
1572
|
+
autocorrelation(values) {
|
|
1573
|
+
if (values.length < 3) return 0;
|
|
1574
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
1575
|
+
let numerator = 0;
|
|
1576
|
+
let denominator = 0;
|
|
1577
|
+
for (let i = 0; i < values.length - 1; i++) {
|
|
1578
|
+
numerator += (values[i] - mean) * (values[i + 1] - mean);
|
|
1579
|
+
}
|
|
1580
|
+
for (let i = 0; i < values.length; i++) {
|
|
1581
|
+
denominator += Math.pow(values[i] - mean, 2);
|
|
1582
|
+
}
|
|
1583
|
+
return denominator === 0 ? 0 : numerator / denominator;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
class TabNavigationAnalyzer {
|
|
1587
|
+
// Also report when 20+ Tabs accumulated
|
|
1588
|
+
constructor(transmitter) {
|
|
1589
|
+
__publicField(this, "transmitter");
|
|
1590
|
+
// Tab event tracking
|
|
1591
|
+
__publicField(this, "tabTimestamps", []);
|
|
1592
|
+
__publicField(this, "shiftTabCount", 0);
|
|
1593
|
+
__publicField(this, "tabCount", 0);
|
|
1594
|
+
__publicField(this, "isShiftDown", false);
|
|
1595
|
+
// Burst detection
|
|
1596
|
+
__publicField(this, "BURST_THRESHOLD_MS", 80);
|
|
1597
|
+
// Consecutive Tabs under 80ms = burst
|
|
1598
|
+
// Reporting
|
|
1599
|
+
__publicField(this, "reportInterval", null);
|
|
1600
|
+
__publicField(this, "REPORT_INTERVAL_MS", 1e4);
|
|
1601
|
+
// Report every 10 seconds
|
|
1602
|
+
__publicField(this, "MIN_TABS_TO_REPORT", 5);
|
|
1603
|
+
// Need at least 5 Tabs to analyze
|
|
1604
|
+
__publicField(this, "TAB_TRIGGER_COUNT", 20);
|
|
1605
|
+
__publicField(this, "onKeyDown", (e) => {
|
|
1606
|
+
if (e.key === "Shift") {
|
|
1607
|
+
this.isShiftDown = true;
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (e.key === "Tab") {
|
|
1611
|
+
const now = Date.now();
|
|
1612
|
+
if (this.isShiftDown) {
|
|
1613
|
+
this.shiftTabCount++;
|
|
1614
|
+
} else {
|
|
1615
|
+
this.tabCount++;
|
|
1616
|
+
this.tabTimestamps.push(now);
|
|
1617
|
+
if (this.tabTimestamps.length > 100) {
|
|
1618
|
+
this.tabTimestamps.shift();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (this.tabCount + this.shiftTabCount >= this.TAB_TRIGGER_COUNT) {
|
|
1622
|
+
this.report();
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
__publicField(this, "onKeyUp", (e) => {
|
|
1627
|
+
if (e.key === "Shift") {
|
|
1628
|
+
this.isShiftDown = false;
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
this.transmitter = transmitter;
|
|
1632
|
+
}
|
|
1633
|
+
start() {
|
|
1634
|
+
if (typeof document === "undefined") return;
|
|
1635
|
+
document.addEventListener("keydown", this.onKeyDown, { passive: true, capture: true });
|
|
1636
|
+
document.addEventListener("keyup", this.onKeyUp, { passive: true, capture: true });
|
|
1637
|
+
this.reportInterval = setInterval(() => this.report(), this.REPORT_INTERVAL_MS);
|
|
1638
|
+
}
|
|
1639
|
+
stop() {
|
|
1640
|
+
if (typeof document === "undefined") return;
|
|
1641
|
+
document.removeEventListener("keydown", this.onKeyDown, { capture: true });
|
|
1642
|
+
document.removeEventListener("keyup", this.onKeyUp, { capture: true });
|
|
1643
|
+
if (this.reportInterval) {
|
|
1644
|
+
clearInterval(this.reportInterval);
|
|
1645
|
+
this.reportInterval = null;
|
|
1646
|
+
}
|
|
1647
|
+
this.report();
|
|
1648
|
+
}
|
|
1649
|
+
report() {
|
|
1650
|
+
const totalTabs = this.tabCount + this.shiftTabCount;
|
|
1651
|
+
if (totalTabs < this.MIN_TABS_TO_REPORT) return;
|
|
1652
|
+
const intervals = this.calculateIntervals(this.tabTimestamps);
|
|
1653
|
+
if (intervals.length < 3) {
|
|
1654
|
+
this.resetBuffers();
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const intervalStats = this.analyzeIntervals(intervals);
|
|
1658
|
+
const burstStats = this.analyzeBursts(intervals);
|
|
1659
|
+
const shiftTabRatio = totalTabs > 0 ? this.shiftTabCount / totalTabs : 0;
|
|
1660
|
+
this.transmitter.send("tab_navigation_metrics", {
|
|
1661
|
+
tabCount: this.tabCount,
|
|
1662
|
+
shiftTabCount: this.shiftTabCount,
|
|
1663
|
+
shiftTabRatio: parseFloat(shiftTabRatio.toFixed(4)),
|
|
1664
|
+
intervals: intervalStats,
|
|
1665
|
+
bursts: burstStats,
|
|
1666
|
+
timestamp: Date.now()
|
|
1667
|
+
});
|
|
1668
|
+
this.resetBuffers();
|
|
1669
|
+
}
|
|
1670
|
+
resetBuffers() {
|
|
1671
|
+
this.tabTimestamps = [];
|
|
1672
|
+
this.tabCount = 0;
|
|
1673
|
+
this.shiftTabCount = 0;
|
|
1674
|
+
}
|
|
1675
|
+
calculateIntervals(timestamps) {
|
|
1676
|
+
const intervals = [];
|
|
1677
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
1678
|
+
const interval = timestamps[i] - timestamps[i - 1];
|
|
1679
|
+
if (interval < 5e3) {
|
|
1680
|
+
intervals.push(interval);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return intervals;
|
|
1684
|
+
}
|
|
1685
|
+
analyzeIntervals(intervals) {
|
|
1686
|
+
if (intervals.length === 0) {
|
|
1687
|
+
return { avg: 0, variance: 0, cv: 0, min: 0, samples: 0 };
|
|
1688
|
+
}
|
|
1689
|
+
const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1690
|
+
const variance = intervals.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / intervals.length;
|
|
1691
|
+
const stdDev = Math.sqrt(variance);
|
|
1692
|
+
const cv = avg > 0 ? stdDev / avg : 0;
|
|
1693
|
+
const min = Math.min(...intervals);
|
|
1694
|
+
return {
|
|
1695
|
+
avg: parseFloat(avg.toFixed(2)),
|
|
1696
|
+
variance: parseFloat(variance.toFixed(2)),
|
|
1697
|
+
cv: parseFloat(cv.toFixed(4)),
|
|
1698
|
+
min,
|
|
1699
|
+
samples: intervals.length
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
analyzeBursts(intervals) {
|
|
1703
|
+
let burstCount = 0;
|
|
1704
|
+
let currentBurstLength = 0;
|
|
1705
|
+
let totalBurstLength = 0;
|
|
1706
|
+
let totalBurstInterval = 0;
|
|
1707
|
+
let burstIntervalCount = 0;
|
|
1708
|
+
for (const interval of intervals) {
|
|
1709
|
+
if (interval < this.BURST_THRESHOLD_MS) {
|
|
1710
|
+
currentBurstLength++;
|
|
1711
|
+
totalBurstInterval += interval;
|
|
1712
|
+
burstIntervalCount++;
|
|
1713
|
+
} else {
|
|
1714
|
+
if (currentBurstLength >= 3) {
|
|
1715
|
+
burstCount++;
|
|
1716
|
+
totalBurstLength += currentBurstLength;
|
|
1717
|
+
}
|
|
1718
|
+
currentBurstLength = 0;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (currentBurstLength >= 3) {
|
|
1722
|
+
burstCount++;
|
|
1723
|
+
totalBurstLength += currentBurstLength;
|
|
1724
|
+
}
|
|
1725
|
+
return {
|
|
1726
|
+
count: burstCount,
|
|
1727
|
+
avgLength: burstCount > 0 ? parseFloat((totalBurstLength / burstCount).toFixed(1)) : 0,
|
|
1728
|
+
avgInterval: burstIntervalCount > 0 ? parseFloat((totalBurstInterval / burstIntervalCount).toFixed(2)) : 0
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
class CloudEnvironmentDetector {
|
|
1733
|
+
constructor(transmitter) {
|
|
1734
|
+
__publicField(this, "transmitter");
|
|
1735
|
+
__publicField(this, "hasRun", false);
|
|
1736
|
+
this.transmitter = transmitter;
|
|
1737
|
+
}
|
|
1738
|
+
start() {
|
|
1739
|
+
if (this.hasRun || typeof window === "undefined") return;
|
|
1740
|
+
this.hasRun = true;
|
|
1741
|
+
setTimeout(() => this.detect(), 2e3);
|
|
1742
|
+
}
|
|
1743
|
+
async detect() {
|
|
1744
|
+
try {
|
|
1745
|
+
const gpu = this.analyzeGPU();
|
|
1746
|
+
const hardware = this.analyzeHardware();
|
|
1747
|
+
const screen2 = this.analyzeScreen();
|
|
1748
|
+
const media = await this.analyzeMedia();
|
|
1749
|
+
const performance2 = this.benchmarkCanvas();
|
|
1750
|
+
let vmIndicatorCount = 0;
|
|
1751
|
+
if (gpu.category === "virtual_gpu" || gpu.category === "software_render" || gpu.category === "cloud_gpu") {
|
|
1752
|
+
vmIndicatorCount += 2;
|
|
1753
|
+
}
|
|
1754
|
+
if (hardware.cores <= 2) vmIndicatorCount++;
|
|
1755
|
+
if (hardware.memory !== null && hardware.memory <= 2) vmIndicatorCount++;
|
|
1756
|
+
if (hardware.colorDepth === 24) vmIndicatorCount++;
|
|
1757
|
+
if (hardware.pixelRatio === 1) vmIndicatorCount++;
|
|
1758
|
+
if (screen2.noTaskbar) vmIndicatorCount++;
|
|
1759
|
+
if (screen2.isVMResolution) vmIndicatorCount++;
|
|
1760
|
+
if (media.cameras === 0 && media.microphones === 0) vmIndicatorCount++;
|
|
1761
|
+
if (!media.hasBluetooth && !media.hasUSB) vmIndicatorCount++;
|
|
1762
|
+
if (performance2.canvasRenderTime > 50) vmIndicatorCount++;
|
|
1763
|
+
const { classification, confidence } = this.classify(gpu, hardware, vmIndicatorCount);
|
|
1764
|
+
this.transmitter.send("cloud_environment", {
|
|
1765
|
+
classification,
|
|
1766
|
+
confidence,
|
|
1767
|
+
signals: {
|
|
1768
|
+
gpu: { renderer: gpu.renderer, vendor: gpu.vendor, category: gpu.category },
|
|
1769
|
+
hardware,
|
|
1770
|
+
screen: screen2,
|
|
1771
|
+
media,
|
|
1772
|
+
performance: performance2
|
|
1773
|
+
},
|
|
1774
|
+
vmIndicatorCount,
|
|
1775
|
+
timestamp: Date.now()
|
|
1776
|
+
});
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Analyze WebGL renderer to identify virtual GPUs.
|
|
1782
|
+
*/
|
|
1783
|
+
analyzeGPU() {
|
|
1784
|
+
try {
|
|
1785
|
+
const canvas = document.createElement("canvas");
|
|
1786
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1787
|
+
if (!gl) return { renderer: "unavailable", vendor: "unavailable", category: "unknown" };
|
|
1788
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1789
|
+
if (!debugInfo) return { renderer: "no_debug_info", vendor: "no_debug_info", category: "unknown" };
|
|
1790
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "";
|
|
1791
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "";
|
|
1792
|
+
const category = this.categorizeGPU(renderer, vendor);
|
|
1793
|
+
return { renderer, vendor, category };
|
|
1794
|
+
} catch (e) {
|
|
1795
|
+
return { renderer: "error", vendor: "error", category: "unknown" };
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Categorize GPU renderer string into environment type.
|
|
1800
|
+
*/
|
|
1801
|
+
categorizeGPU(renderer, vendor) {
|
|
1802
|
+
const r = renderer.toLowerCase();
|
|
1803
|
+
const v = vendor.toLowerCase();
|
|
1804
|
+
if (/swiftshader|llvmpipe|mesa offscreen|softpipe/.test(r)) {
|
|
1805
|
+
return "software_render";
|
|
1806
|
+
}
|
|
1807
|
+
if (/svga3d|svga|vmware/.test(r)) return "virtual_gpu";
|
|
1808
|
+
if (/virtualbox/.test(r)) return "virtual_gpu";
|
|
1809
|
+
if (/parallels/.test(r)) return "virtual_gpu";
|
|
1810
|
+
if (/qxl|virtio|red hat|spice/.test(r)) return "virtual_gpu";
|
|
1811
|
+
if (/hyper-v|microsoft basic render/i.test(r)) return "virtual_gpu";
|
|
1812
|
+
if (/citrix/.test(r)) return "virtual_gpu";
|
|
1813
|
+
if (/amazon|elastic|aws/.test(r) || /amazon|elastic|aws/.test(v)) return "cloud_gpu";
|
|
1814
|
+
if (/google cloud|gce/.test(r) || /google cloud|gce/.test(v)) return "cloud_gpu";
|
|
1815
|
+
if (/azure/.test(r) || /azure/.test(v)) return "cloud_gpu";
|
|
1816
|
+
return "physical";
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Collect hardware characteristics.
|
|
1820
|
+
*/
|
|
1821
|
+
analyzeHardware() {
|
|
1822
|
+
return {
|
|
1823
|
+
cores: navigator.hardwareConcurrency || 0,
|
|
1824
|
+
memory: navigator.deviceMemory ?? null,
|
|
1825
|
+
colorDepth: screen.colorDepth,
|
|
1826
|
+
pixelRatio: window.devicePixelRatio || 1
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Analyze screen properties for VM indicators.
|
|
1831
|
+
*/
|
|
1832
|
+
analyzeScreen() {
|
|
1833
|
+
const width = screen.width;
|
|
1834
|
+
const height = screen.height;
|
|
1835
|
+
const availHeight = screen.availHeight;
|
|
1836
|
+
const noTaskbar = height === availHeight && height === window.innerHeight;
|
|
1837
|
+
const vmResolutions = [
|
|
1838
|
+
[800, 600],
|
|
1839
|
+
[1024, 768],
|
|
1840
|
+
[1280, 1024],
|
|
1841
|
+
[1280, 800],
|
|
1842
|
+
[1920, 1080]
|
|
1843
|
+
// Also common physical, but combined with other signals
|
|
1844
|
+
];
|
|
1845
|
+
const isVMResolution = vmResolutions.some(
|
|
1846
|
+
([w, h]) => width === w && height === h && !(w === 1920 && h === 1080)
|
|
1847
|
+
);
|
|
1848
|
+
return { width, height, availHeight, noTaskbar, isVMResolution };
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Check media device availability (cameras, microphones, peripherals).
|
|
1852
|
+
*/
|
|
1853
|
+
async analyzeMedia() {
|
|
1854
|
+
let cameras = -1;
|
|
1855
|
+
let microphones = -1;
|
|
1856
|
+
try {
|
|
1857
|
+
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
|
1858
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
1859
|
+
cameras = devices.filter((d) => d.kind === "videoinput").length;
|
|
1860
|
+
microphones = devices.filter((d) => d.kind === "audioinput").length;
|
|
1861
|
+
}
|
|
1862
|
+
} catch (e) {
|
|
1863
|
+
}
|
|
1864
|
+
const hasBluetooth = !!navigator.bluetooth;
|
|
1865
|
+
const hasUSB = !!navigator.usb;
|
|
1866
|
+
let hasBattery = null;
|
|
1867
|
+
try {
|
|
1868
|
+
if (navigator.getBattery) {
|
|
1869
|
+
const battery = await navigator.getBattery();
|
|
1870
|
+
hasBattery = !(battery.charging && battery.level === 1 && battery.chargingTime === 0);
|
|
1871
|
+
}
|
|
1872
|
+
} catch (e) {
|
|
1873
|
+
}
|
|
1874
|
+
return { cameras, microphones, hasBluetooth, hasUSB, hasBattery };
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Quick canvas rendering benchmark.
|
|
1878
|
+
* Software rendering (VM without GPU) is significantly slower.
|
|
1879
|
+
*/
|
|
1880
|
+
benchmarkCanvas() {
|
|
1881
|
+
try {
|
|
1882
|
+
const canvas = document.createElement("canvas");
|
|
1883
|
+
canvas.width = 200;
|
|
1884
|
+
canvas.height = 200;
|
|
1885
|
+
const ctx = canvas.getContext("2d");
|
|
1886
|
+
if (!ctx) return { canvasRenderTime: -1 };
|
|
1887
|
+
const start = performance.now();
|
|
1888
|
+
for (let i = 0; i < 500; i++) {
|
|
1889
|
+
ctx.fillStyle = `rgb(${i % 256},${i * 3 % 256},${i * 7 % 256})`;
|
|
1890
|
+
ctx.fillRect(i * 13 % 200, i * 7 % 200, 10, 10);
|
|
1891
|
+
}
|
|
1892
|
+
ctx.getImageData(0, 0, 1, 1);
|
|
1893
|
+
const elapsed = performance.now() - start;
|
|
1894
|
+
return { canvasRenderTime: Math.round(elapsed * 100) / 100 };
|
|
1895
|
+
} catch (e) {
|
|
1896
|
+
return { canvasRenderTime: -1 };
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Classify the environment based on collected signals.
|
|
1901
|
+
*/
|
|
1902
|
+
classify(gpu, hardware, vmIndicatorCount) {
|
|
1903
|
+
if (gpu.category === "virtual_gpu") {
|
|
1904
|
+
if (hardware.colorDepth === 24) {
|
|
1905
|
+
return { classification: "rdp_likely", confidence: 0.85 };
|
|
1906
|
+
}
|
|
1907
|
+
return { classification: "vm_detected", confidence: 0.95 };
|
|
1908
|
+
}
|
|
1909
|
+
if (gpu.category === "cloud_gpu") {
|
|
1910
|
+
return { classification: "cloud_likely", confidence: 0.9 };
|
|
1911
|
+
}
|
|
1912
|
+
if (gpu.category === "software_render") {
|
|
1913
|
+
return { classification: "vm_detected", confidence: 0.9 };
|
|
1914
|
+
}
|
|
1915
|
+
if (vmIndicatorCount >= 5) {
|
|
1916
|
+
return { classification: "vm_likely", confidence: 0.8 };
|
|
1917
|
+
}
|
|
1918
|
+
if (vmIndicatorCount >= 3) {
|
|
1919
|
+
return { classification: "vm_likely", confidence: 0.6 };
|
|
1920
|
+
}
|
|
1921
|
+
return { classification: "physical", confidence: 1 - vmIndicatorCount * 0.1 };
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
class CrossContextDetector {
|
|
1925
|
+
constructor(transmitter) {
|
|
1926
|
+
__publicField(this, "transmitter");
|
|
1927
|
+
__publicField(this, "hasRun", false);
|
|
1928
|
+
this.transmitter = transmitter;
|
|
1929
|
+
}
|
|
1930
|
+
start() {
|
|
1931
|
+
if (this.hasRun || typeof window === "undefined") return;
|
|
1932
|
+
this.hasRun = true;
|
|
1933
|
+
setTimeout(() => this.detect(), 2500);
|
|
1934
|
+
}
|
|
1935
|
+
async detect() {
|
|
1936
|
+
var _a, _b;
|
|
1937
|
+
const signals = {};
|
|
1938
|
+
let score = 0;
|
|
1939
|
+
const main = {
|
|
1940
|
+
webdriver: !!navigator.webdriver,
|
|
1941
|
+
languages: ((_a = navigator.languages) == null ? void 0 : _a.join(",")) || "",
|
|
1942
|
+
platform: navigator.platform || "",
|
|
1943
|
+
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
1944
|
+
userAgent: navigator.userAgent || "",
|
|
1945
|
+
pluginsLength: ((_b = navigator.plugins) == null ? void 0 : _b.length) ?? 0
|
|
1946
|
+
};
|
|
1947
|
+
const iframeResult = this.checkIframe(main);
|
|
1948
|
+
if (iframeResult.score > 0) {
|
|
1949
|
+
signals.iframe = iframeResult.mismatches;
|
|
1950
|
+
score += iframeResult.score;
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
const workerResult = await this.checkWorker(main);
|
|
1954
|
+
if (workerResult.score > 0) {
|
|
1955
|
+
signals.worker = workerResult.mismatches;
|
|
1956
|
+
score += workerResult.score;
|
|
1957
|
+
}
|
|
1958
|
+
} catch (_e) {
|
|
1959
|
+
}
|
|
1960
|
+
if (score > 0) {
|
|
1961
|
+
this.transmitter.send("cross_context_mismatch", {
|
|
1962
|
+
signals,
|
|
1963
|
+
score,
|
|
1964
|
+
timestamp: Date.now()
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Create a sandboxed iframe and compare navigator values.
|
|
1970
|
+
*/
|
|
1971
|
+
checkIframe(main) {
|
|
1972
|
+
var _a, _b;
|
|
1973
|
+
const mismatches = [];
|
|
1974
|
+
let score = 0;
|
|
1975
|
+
try {
|
|
1976
|
+
const iframe = document.createElement("iframe");
|
|
1977
|
+
iframe.style.display = "none";
|
|
1978
|
+
iframe.sandbox = "allow-same-origin";
|
|
1979
|
+
document.body.appendChild(iframe);
|
|
1980
|
+
const iframeWindow = iframe.contentWindow;
|
|
1981
|
+
if (!iframeWindow) {
|
|
1982
|
+
document.body.removeChild(iframe);
|
|
1983
|
+
return { score: 0, mismatches: [] };
|
|
1984
|
+
}
|
|
1985
|
+
const iframeNav = iframeWindow.navigator;
|
|
1986
|
+
if (iframeNav.webdriver !== navigator.webdriver) {
|
|
1987
|
+
mismatches.push("webdriver_mismatch");
|
|
1988
|
+
score += 40;
|
|
1989
|
+
}
|
|
1990
|
+
if (!navigator.webdriver && iframeNav.webdriver === true) {
|
|
1991
|
+
mismatches.push("webdriver_spoofed_in_main");
|
|
1992
|
+
score += 30;
|
|
1993
|
+
}
|
|
1994
|
+
const iframeLangs = ((_a = iframeNav.languages) == null ? void 0 : _a.join(",")) || "";
|
|
1995
|
+
if (iframeLangs !== main.languages) {
|
|
1996
|
+
mismatches.push("languages_mismatch");
|
|
1997
|
+
score += 15;
|
|
1998
|
+
}
|
|
1999
|
+
if (iframeNav.platform !== main.platform) {
|
|
2000
|
+
mismatches.push("platform_mismatch");
|
|
2001
|
+
score += 15;
|
|
2002
|
+
}
|
|
2003
|
+
const iframePlugins = ((_b = iframeNav.plugins) == null ? void 0 : _b.length) ?? 0;
|
|
2004
|
+
if (iframePlugins !== main.pluginsLength) {
|
|
2005
|
+
mismatches.push("plugins_mismatch");
|
|
2006
|
+
score += 10;
|
|
2007
|
+
}
|
|
2008
|
+
document.body.removeChild(iframe);
|
|
2009
|
+
} catch (_e) {
|
|
2010
|
+
}
|
|
2011
|
+
return { score, mismatches };
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Spin up a WebWorker and compare navigator values.
|
|
2015
|
+
* Workers have their own navigator which is harder to spoof.
|
|
2016
|
+
*/
|
|
2017
|
+
checkWorker(main) {
|
|
2018
|
+
return new Promise((resolve) => {
|
|
2019
|
+
try {
|
|
2020
|
+
const workerCode = `
|
|
2021
|
+
self.onmessage = function() {
|
|
2022
|
+
var nav = self.navigator;
|
|
2023
|
+
self.postMessage({
|
|
2024
|
+
webdriver: !!nav.webdriver,
|
|
2025
|
+
languages: (nav.languages || []).join(','),
|
|
2026
|
+
platform: nav.platform || '',
|
|
2027
|
+
hardwareConcurrency: nav.hardwareConcurrency || 0,
|
|
2028
|
+
userAgent: nav.userAgent || ''
|
|
2029
|
+
});
|
|
2030
|
+
};
|
|
2031
|
+
`;
|
|
2032
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
2033
|
+
const url = URL.createObjectURL(blob);
|
|
2034
|
+
const worker = new Worker(url);
|
|
2035
|
+
const timeout = setTimeout(() => {
|
|
2036
|
+
worker.terminate();
|
|
2037
|
+
URL.revokeObjectURL(url);
|
|
2038
|
+
resolve({ score: 0, mismatches: [] });
|
|
2039
|
+
}, 3e3);
|
|
2040
|
+
worker.onmessage = (e) => {
|
|
2041
|
+
clearTimeout(timeout);
|
|
2042
|
+
worker.terminate();
|
|
2043
|
+
URL.revokeObjectURL(url);
|
|
2044
|
+
const w = e.data;
|
|
2045
|
+
const mismatches = [];
|
|
2046
|
+
let score = 0;
|
|
2047
|
+
if (w.webdriver !== main.webdriver) {
|
|
2048
|
+
mismatches.push("worker_webdriver_mismatch");
|
|
2049
|
+
score += 35;
|
|
2050
|
+
}
|
|
2051
|
+
if (w.languages !== main.languages) {
|
|
2052
|
+
mismatches.push("worker_languages_mismatch");
|
|
2053
|
+
score += 15;
|
|
2054
|
+
}
|
|
2055
|
+
if (w.platform !== main.platform) {
|
|
2056
|
+
mismatches.push("worker_platform_mismatch");
|
|
2057
|
+
score += 15;
|
|
2058
|
+
}
|
|
2059
|
+
if (w.hardwareConcurrency !== main.hardwareConcurrency) {
|
|
2060
|
+
mismatches.push("worker_concurrency_mismatch");
|
|
2061
|
+
score += 10;
|
|
2062
|
+
}
|
|
2063
|
+
if (w.userAgent !== main.userAgent) {
|
|
2064
|
+
mismatches.push("worker_ua_mismatch");
|
|
2065
|
+
score += 20;
|
|
2066
|
+
}
|
|
2067
|
+
resolve({ score, mismatches });
|
|
2068
|
+
};
|
|
2069
|
+
worker.onerror = () => {
|
|
2070
|
+
clearTimeout(timeout);
|
|
2071
|
+
worker.terminate();
|
|
2072
|
+
URL.revokeObjectURL(url);
|
|
2073
|
+
resolve({ score: 0, mismatches: [] });
|
|
2074
|
+
};
|
|
2075
|
+
worker.postMessage("check");
|
|
2076
|
+
} catch (_e) {
|
|
2077
|
+
resolve({ score: 0, mismatches: [] });
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
class HeadlessProbeDetector {
|
|
2083
|
+
constructor(transmitter) {
|
|
2084
|
+
__publicField(this, "transmitter");
|
|
2085
|
+
__publicField(this, "hasRun", false);
|
|
2086
|
+
this.transmitter = transmitter;
|
|
2087
|
+
}
|
|
2088
|
+
start() {
|
|
2089
|
+
if (this.hasRun || typeof window === "undefined") return;
|
|
2090
|
+
this.hasRun = true;
|
|
2091
|
+
setTimeout(() => this.detect(), 2e3);
|
|
2092
|
+
}
|
|
2093
|
+
async detect() {
|
|
2094
|
+
const signals = {};
|
|
2095
|
+
let score = 0;
|
|
2096
|
+
const brokenImg = await this.checkBrokenImage();
|
|
2097
|
+
if (brokenImg.suspicious) {
|
|
2098
|
+
signals.brokenImage = brokenImg;
|
|
2099
|
+
score += 25;
|
|
2100
|
+
}
|
|
2101
|
+
const clientHints = await this.checkClientHints();
|
|
2102
|
+
if (clientHints.suspicious) {
|
|
2103
|
+
signals.clientHints = clientHints;
|
|
2104
|
+
score += 20;
|
|
2105
|
+
}
|
|
2106
|
+
const cdpMouse = this.checkCDPMouseSignature();
|
|
2107
|
+
if (cdpMouse.suspicious) {
|
|
2108
|
+
signals.cdpMouse = cdpMouse;
|
|
2109
|
+
score += 30;
|
|
2110
|
+
}
|
|
2111
|
+
const hairline = this.checkHairlineSupport();
|
|
2112
|
+
if (hairline.suspicious) {
|
|
2113
|
+
signals.hairline = hairline;
|
|
2114
|
+
score += 10;
|
|
2115
|
+
}
|
|
2116
|
+
const extraFrameworks = this.checkExtraFrameworks();
|
|
2117
|
+
if (extraFrameworks.length > 0) {
|
|
2118
|
+
signals.extraFrameworks = extraFrameworks;
|
|
2119
|
+
score += 20;
|
|
2120
|
+
}
|
|
2121
|
+
const workerCDP = await this.checkWorkerCDP();
|
|
2122
|
+
if (workerCDP.suspicious) {
|
|
2123
|
+
signals.workerCDP = true;
|
|
2124
|
+
score += 35;
|
|
2125
|
+
}
|
|
2126
|
+
if (score > 0) {
|
|
2127
|
+
this.transmitter.send("headless_probe", {
|
|
2128
|
+
signals,
|
|
2129
|
+
score,
|
|
2130
|
+
timestamp: Date.now()
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Broken image dimensions test.
|
|
2136
|
+
* Normal browsers render a placeholder for broken images (typically 16x16 or 24x24).
|
|
2137
|
+
* Headless Chrome returns 0x0.
|
|
2138
|
+
*/
|
|
2139
|
+
checkBrokenImage() {
|
|
2140
|
+
return new Promise((resolve) => {
|
|
2141
|
+
try {
|
|
2142
|
+
const img = document.createElement("img");
|
|
2143
|
+
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==___invalid";
|
|
2144
|
+
const timeout = setTimeout(() => {
|
|
2145
|
+
resolve({ suspicious: false, width: -1, height: -1 });
|
|
2146
|
+
}, 2e3);
|
|
2147
|
+
img.onerror = () => {
|
|
2148
|
+
clearTimeout(timeout);
|
|
2149
|
+
const w = img.width;
|
|
2150
|
+
const h = img.height;
|
|
2151
|
+
resolve({
|
|
2152
|
+
suspicious: w === 0 && h === 0,
|
|
2153
|
+
width: w,
|
|
2154
|
+
height: h
|
|
2155
|
+
});
|
|
2156
|
+
};
|
|
2157
|
+
img.onload = () => {
|
|
2158
|
+
clearTimeout(timeout);
|
|
2159
|
+
resolve({ suspicious: false, width: img.width, height: img.height });
|
|
2160
|
+
};
|
|
2161
|
+
img.style.position = "absolute";
|
|
2162
|
+
img.style.left = "-9999px";
|
|
2163
|
+
document.body.appendChild(img);
|
|
2164
|
+
setTimeout(() => {
|
|
2165
|
+
try {
|
|
2166
|
+
document.body.removeChild(img);
|
|
2167
|
+
} catch (_e) {
|
|
2168
|
+
}
|
|
2169
|
+
}, 100);
|
|
2170
|
+
} catch (_e) {
|
|
2171
|
+
resolve({ suspicious: false, width: -1, height: -1 });
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Client Hints consistency check.
|
|
2177
|
+
* navigator.userAgentData (if available) should match UA string.
|
|
2178
|
+
* Bots that spoof UA often forget to update Client Hints.
|
|
2179
|
+
*/
|
|
2180
|
+
async checkClientHints() {
|
|
2181
|
+
const mismatches = [];
|
|
2182
|
+
try {
|
|
2183
|
+
const uad = navigator.userAgentData;
|
|
2184
|
+
if (!uad) return { suspicious: false, mismatches: [] };
|
|
2185
|
+
const ua = navigator.userAgent;
|
|
2186
|
+
const brands = uad.brands || [];
|
|
2187
|
+
const brandNames = brands.map((b) => b.brand);
|
|
2188
|
+
if (/Chrome\/\d/.test(ua) && !brandNames.some((b) => /Chromium|Google Chrome|Chrome/.test(b))) {
|
|
2189
|
+
mismatches.push("ua_chrome_no_brand");
|
|
2190
|
+
}
|
|
2191
|
+
if (uad.platform) {
|
|
2192
|
+
if (/Windows/.test(ua) && uad.platform !== "Windows") {
|
|
2193
|
+
mismatches.push("ua_windows_brand_mismatch");
|
|
2194
|
+
}
|
|
2195
|
+
if (/Macintosh/.test(ua) && uad.platform !== "macOS") {
|
|
2196
|
+
mismatches.push("ua_mac_brand_mismatch");
|
|
2197
|
+
}
|
|
2198
|
+
if (/Linux/.test(ua) && !/Linux|Android|ChromeOS/.test(uad.platform)) {
|
|
2199
|
+
mismatches.push("ua_linux_brand_mismatch");
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (/Mobile/.test(ua) && uad.mobile === false) {
|
|
2203
|
+
mismatches.push("ua_mobile_brand_mismatch");
|
|
2204
|
+
}
|
|
2205
|
+
} catch (_e) {
|
|
2206
|
+
}
|
|
2207
|
+
return { suspicious: mismatches.length > 0, mismatches };
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* CDP mouse event signature detection.
|
|
2211
|
+
* CDP's Input.dispatchMouseEvent creates events where:
|
|
2212
|
+
* - movementX/Y are always 0 (no real mouse delta)
|
|
2213
|
+
* - screenX/Y may not match clientX/Y + window offset
|
|
2214
|
+
* - Multiple rapid mousemove without mousedown have consistent offsets
|
|
2215
|
+
*
|
|
2216
|
+
* We collect a few mouse events and check for CDP patterns.
|
|
2217
|
+
*/
|
|
2218
|
+
checkCDPMouseSignature() {
|
|
2219
|
+
let zeroMovementCount = 0;
|
|
2220
|
+
let totalEvents = 0;
|
|
2221
|
+
let suspicious = false;
|
|
2222
|
+
try {
|
|
2223
|
+
const events = [];
|
|
2224
|
+
const handler = (e) => {
|
|
2225
|
+
events.push(e);
|
|
2226
|
+
totalEvents++;
|
|
2227
|
+
if (e.movementX === 0 && e.movementY === 0) {
|
|
2228
|
+
zeroMovementCount++;
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
document.addEventListener("mousemove", handler, { passive: true });
|
|
2232
|
+
setTimeout(() => {
|
|
2233
|
+
document.removeEventListener("mousemove", handler);
|
|
2234
|
+
}, 5e3);
|
|
2235
|
+
if (totalEvents > 10 && zeroMovementCount / totalEvents > 0.9) {
|
|
2236
|
+
suspicious = true;
|
|
2237
|
+
}
|
|
2238
|
+
} catch (_e) {
|
|
2239
|
+
}
|
|
2240
|
+
return { suspicious, zeroMovementCount, totalEvents };
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Hairline (0.5px border) support test.
|
|
2244
|
+
* HiDPI devices support 0.5px borders. Headless Chrome may not.
|
|
2245
|
+
*/
|
|
2246
|
+
checkHairlineSupport() {
|
|
2247
|
+
try {
|
|
2248
|
+
const div = document.createElement("div");
|
|
2249
|
+
div.style.border = ".5px solid transparent";
|
|
2250
|
+
div.style.position = "absolute";
|
|
2251
|
+
div.style.left = "-9999px";
|
|
2252
|
+
document.body.appendChild(div);
|
|
2253
|
+
const height = div.offsetHeight;
|
|
2254
|
+
document.body.removeChild(div);
|
|
2255
|
+
return { suspicious: height === 0, offsetHeight: height };
|
|
2256
|
+
} catch (_e) {
|
|
2257
|
+
return { suspicious: false, offsetHeight: -1 };
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Additional automation framework globals.
|
|
2262
|
+
*/
|
|
2263
|
+
checkExtraFrameworks() {
|
|
2264
|
+
const found = [];
|
|
2265
|
+
try {
|
|
2266
|
+
const ext = window.external;
|
|
2267
|
+
if (ext && typeof ext.toString === "function") {
|
|
2268
|
+
try {
|
|
2269
|
+
const extStr = ext.toString();
|
|
2270
|
+
if (/Sequentum/i.test(extStr)) found.push("sequentum");
|
|
2271
|
+
} catch (_e) {
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const webdriverKeys = [
|
|
2275
|
+
"__webdriver_script_fn",
|
|
2276
|
+
"__webdriver_script_func",
|
|
2277
|
+
"__webdriver_evaluate",
|
|
2278
|
+
"__selenium_evaluate",
|
|
2279
|
+
"__fxdriver_evaluate",
|
|
2280
|
+
"__driver_evaluate",
|
|
2281
|
+
"__webdriver_unwrapped",
|
|
2282
|
+
"__selenium_unwrapped",
|
|
2283
|
+
"__fxdriver_unwrapped",
|
|
2284
|
+
"__driver_unwrapped",
|
|
2285
|
+
"_Selenium_IDE_Recorder",
|
|
2286
|
+
"_selenium",
|
|
2287
|
+
"calledSelenium",
|
|
2288
|
+
"_WEBDRIVER_ELEM_CACHE",
|
|
2289
|
+
"ChromeDriverw",
|
|
2290
|
+
"driver-hierarchical",
|
|
2291
|
+
"__$webdriverAsyncExecutor",
|
|
2292
|
+
"__lastWatirAlert",
|
|
2293
|
+
"__lastWatirConfirm",
|
|
2294
|
+
"__lastWatirPrompt",
|
|
2295
|
+
"_WEBDRIVER_ELEM_CACHE",
|
|
2296
|
+
"webdriver",
|
|
2297
|
+
"_phantom",
|
|
2298
|
+
"__nightmare",
|
|
2299
|
+
"_selenium"
|
|
2300
|
+
];
|
|
2301
|
+
for (const key of webdriverKeys) {
|
|
2302
|
+
if (key in document || key in window) {
|
|
2303
|
+
found.push(`global_${key}`);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const navKeys = Object.getOwnPropertyNames(navigator);
|
|
2307
|
+
for (const key of navKeys) {
|
|
2308
|
+
if (/webdriver|selenium|puppeteer|playwright|phantom/i.test(key)) {
|
|
2309
|
+
found.push(`nav_${key}`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
} catch (_e) {
|
|
2313
|
+
}
|
|
2314
|
+
return found;
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* CDP detection within WebWorker context.
|
|
2318
|
+
* Same getter-trap technique but inside a Worker where it's harder to intercept.
|
|
2319
|
+
*/
|
|
2320
|
+
checkWorkerCDP() {
|
|
2321
|
+
return new Promise((resolve) => {
|
|
2322
|
+
try {
|
|
2323
|
+
const workerCode = `
|
|
2324
|
+
self.onmessage = function() {
|
|
2325
|
+
var detected = false;
|
|
2326
|
+
try {
|
|
2327
|
+
var marker = {};
|
|
2328
|
+
Object.defineProperty(marker, 'stack', {
|
|
2329
|
+
get: function() { detected = true; return ''; }
|
|
2330
|
+
});
|
|
2331
|
+
console.debug(marker);
|
|
2332
|
+
} catch(e) {}
|
|
2333
|
+
self.postMessage({ cdp: detected });
|
|
2334
|
+
};
|
|
2335
|
+
`;
|
|
2336
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
2337
|
+
const url = URL.createObjectURL(blob);
|
|
2338
|
+
const worker = new Worker(url);
|
|
2339
|
+
const timeout = setTimeout(() => {
|
|
2340
|
+
worker.terminate();
|
|
2341
|
+
URL.revokeObjectURL(url);
|
|
2342
|
+
resolve({ suspicious: false });
|
|
2343
|
+
}, 3e3);
|
|
2344
|
+
worker.onmessage = (e) => {
|
|
2345
|
+
clearTimeout(timeout);
|
|
2346
|
+
worker.terminate();
|
|
2347
|
+
URL.revokeObjectURL(url);
|
|
2348
|
+
resolve({ suspicious: e.data.cdp === true });
|
|
2349
|
+
};
|
|
2350
|
+
worker.onerror = () => {
|
|
2351
|
+
clearTimeout(timeout);
|
|
2352
|
+
worker.terminate();
|
|
2353
|
+
URL.revokeObjectURL(url);
|
|
2354
|
+
resolve({ suspicious: false });
|
|
2355
|
+
};
|
|
2356
|
+
worker.postMessage("check");
|
|
2357
|
+
} catch (_e) {
|
|
2358
|
+
resolve({ suspicious: false });
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
const COOKIE_NAME = "__aie_token";
|
|
2364
|
+
const RENEWAL_MARGIN_MS = 5 * 60 * 1e3;
|
|
2365
|
+
class CrawlProtect {
|
|
2366
|
+
constructor(config) {
|
|
2367
|
+
__publicField(this, "config");
|
|
2368
|
+
__publicField(this, "renewTimer", null);
|
|
2369
|
+
__publicField(this, "verified", false);
|
|
2370
|
+
this.config = config;
|
|
2371
|
+
}
|
|
2372
|
+
async start() {
|
|
2373
|
+
const existing = this.getToken();
|
|
2374
|
+
if (existing && !this.isExpired(existing)) {
|
|
2375
|
+
this.verified = true;
|
|
2376
|
+
this.scheduleRenewal(existing);
|
|
2377
|
+
this.revealContent();
|
|
2378
|
+
if (this.config.debug) ;
|
|
2379
|
+
return true;
|
|
2380
|
+
}
|
|
2381
|
+
this.hideContent();
|
|
2382
|
+
const success = await this.solveChallenge();
|
|
2383
|
+
if (success) {
|
|
2384
|
+
this.verified = true;
|
|
2385
|
+
this.revealContent();
|
|
2386
|
+
} else {
|
|
2387
|
+
if (this.config.insertHoneypot) {
|
|
2388
|
+
this.insertHoneypotData();
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
return success;
|
|
2392
|
+
}
|
|
2393
|
+
isVerified() {
|
|
2394
|
+
return this.verified;
|
|
2395
|
+
}
|
|
2396
|
+
stop() {
|
|
2397
|
+
if (this.renewTimer) {
|
|
2398
|
+
clearTimeout(this.renewTimer);
|
|
2399
|
+
this.renewTimer = null;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
// ── Challenge Flow ──
|
|
2403
|
+
async solveChallenge() {
|
|
2404
|
+
try {
|
|
2405
|
+
const challengeRes = await fetch(`${this.config.apiBase}/crawl-protect/challenge`, {
|
|
2406
|
+
method: "POST",
|
|
2407
|
+
headers: { "Content-Type": "application/json", "x-client-id": this.config.clientId }
|
|
2408
|
+
});
|
|
2409
|
+
if (!challengeRes.ok) return false;
|
|
2410
|
+
const { id, challenges } = await challengeRes.json();
|
|
2411
|
+
const solutions = [];
|
|
2412
|
+
for (const ch of challenges) {
|
|
2413
|
+
if (ch.type === "math") {
|
|
2414
|
+
const result = ch.op === "add" ? ch.a + ch.b : ch.a * ch.b;
|
|
2415
|
+
solutions.push(String(result));
|
|
2416
|
+
} else if (ch.type === "crypto") {
|
|
2417
|
+
const hash = await this.sha256(ch.nonce);
|
|
2418
|
+
solutions.push(hash.slice(0, 8));
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
const solution = solutions.join(":");
|
|
2422
|
+
const fp = await this.getFingerprint();
|
|
2423
|
+
const verifyRes = await fetch(`${this.config.apiBase}/crawl-protect/verify`, {
|
|
2424
|
+
method: "POST",
|
|
2425
|
+
headers: { "Content-Type": "application/json", "x-client-id": this.config.clientId },
|
|
2426
|
+
body: JSON.stringify({ id, solution, fingerprint: fp }),
|
|
2427
|
+
credentials: "include"
|
|
2428
|
+
// Accept cookie
|
|
2429
|
+
});
|
|
2430
|
+
if (!verifyRes.ok) return false;
|
|
2431
|
+
const data = await verifyRes.json();
|
|
2432
|
+
if (data.success && data.token) {
|
|
2433
|
+
this.setToken(data.token);
|
|
2434
|
+
this.scheduleRenewal(data.token);
|
|
2435
|
+
if (this.config.debug) ;
|
|
2436
|
+
return true;
|
|
2437
|
+
}
|
|
2438
|
+
return false;
|
|
2439
|
+
} catch (e) {
|
|
2440
|
+
if (this.config.debug) ;
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
// ── Content Protection (FE-only mode) ──
|
|
2445
|
+
hideContent() {
|
|
2446
|
+
var _a;
|
|
2447
|
+
if (!((_a = this.config.protectSelectors) == null ? void 0 : _a.length)) return;
|
|
2448
|
+
for (const selector of this.config.protectSelectors) {
|
|
2449
|
+
const elements = document.querySelectorAll(selector);
|
|
2450
|
+
elements.forEach((el) => {
|
|
2451
|
+
el.dataset.aieOrigDisplay = el.style.display;
|
|
2452
|
+
el.style.display = "none";
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
revealContent() {
|
|
2457
|
+
var _a;
|
|
2458
|
+
if (!((_a = this.config.protectSelectors) == null ? void 0 : _a.length)) return;
|
|
2459
|
+
for (const selector of this.config.protectSelectors) {
|
|
2460
|
+
const elements = document.querySelectorAll(selector);
|
|
2461
|
+
elements.forEach((el) => {
|
|
2462
|
+
el.style.display = el.dataset.aieOrigDisplay || "";
|
|
2463
|
+
delete el.dataset.aieOrigDisplay;
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
insertHoneypotData() {
|
|
2468
|
+
const honeypot = document.createElement("div");
|
|
2469
|
+
honeypot.style.cssText = "position:absolute;left:-9999px;top:-9999px;opacity:0;pointer-events:none;";
|
|
2470
|
+
honeypot.setAttribute("aria-hidden", "true");
|
|
2471
|
+
honeypot.innerHTML = `
|
|
2472
|
+
<span class="price" data-aie-honeypot="1">₩999,999,999</span>
|
|
2473
|
+
<span class="stock" data-aie-honeypot="1">재고: 0개</span>
|
|
2474
|
+
<a href="mailto:honeypot@aientrophy.com" data-aie-honeypot="1">contact</a>
|
|
2475
|
+
`;
|
|
2476
|
+
document.body.appendChild(honeypot);
|
|
2477
|
+
}
|
|
2478
|
+
// ── Token Management ──
|
|
2479
|
+
getToken() {
|
|
2480
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));
|
|
2481
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
2482
|
+
}
|
|
2483
|
+
setToken(token) {
|
|
2484
|
+
const expires = new Date(Date.now() + 30 * 60 * 1e3).toUTCString();
|
|
2485
|
+
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(token)};path=/;expires=${expires};SameSite=None;Secure`;
|
|
2486
|
+
}
|
|
2487
|
+
isExpired(token) {
|
|
2488
|
+
try {
|
|
2489
|
+
const [payloadStr] = token.split(".");
|
|
2490
|
+
const payload = JSON.parse(atob(payloadStr.replace(/-/g, "+").replace(/_/g, "/")));
|
|
2491
|
+
return Date.now() > payload.exp;
|
|
2492
|
+
} catch {
|
|
2493
|
+
return true;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
scheduleRenewal(token) {
|
|
2497
|
+
try {
|
|
2498
|
+
const [payloadStr] = token.split(".");
|
|
2499
|
+
const payload = JSON.parse(atob(payloadStr.replace(/-/g, "+").replace(/_/g, "/")));
|
|
2500
|
+
const renewAt = payload.exp - RENEWAL_MARGIN_MS;
|
|
2501
|
+
const delay = renewAt - Date.now();
|
|
2502
|
+
if (delay > 0) {
|
|
2503
|
+
this.renewTimer = setTimeout(() => this.solveChallenge(), delay);
|
|
2504
|
+
}
|
|
2505
|
+
} catch {
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
// ── Crypto Helpers ──
|
|
2509
|
+
async sha256(data) {
|
|
2510
|
+
const encoder = new TextEncoder();
|
|
2511
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
2512
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2513
|
+
}
|
|
2514
|
+
async getFingerprint() {
|
|
2515
|
+
const parts = [
|
|
2516
|
+
navigator.userAgent,
|
|
2517
|
+
navigator.language,
|
|
2518
|
+
screen.width + "x" + screen.height,
|
|
2519
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
2520
|
+
];
|
|
2521
|
+
return this.sha256(parts.join("|"));
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
1163
2524
|
class SecuritySDK {
|
|
1164
2525
|
constructor() {
|
|
1165
2526
|
__publicField(this, "transmitter");
|
|
1166
|
-
__publicField(this, "collector");
|
|
1167
2527
|
__publicField(this, "behaviorCollector");
|
|
1168
2528
|
// @ts-ignore
|
|
1169
2529
|
__publicField(this, "rapidClickDetector");
|
|
@@ -1175,6 +2535,13 @@ class SecuritySDK {
|
|
|
1175
2535
|
// @ts-ignore
|
|
1176
2536
|
__publicField(this, "challengeHandler");
|
|
1177
2537
|
__publicField(this, "invisibleInteraction");
|
|
2538
|
+
__publicField(this, "automationDetector");
|
|
2539
|
+
__publicField(this, "sessionAnalyzer");
|
|
2540
|
+
__publicField(this, "tabNavigationAnalyzer");
|
|
2541
|
+
__publicField(this, "cloudEnvironmentDetector");
|
|
2542
|
+
__publicField(this, "crossContextDetector");
|
|
2543
|
+
__publicField(this, "headlessProbeDetector");
|
|
2544
|
+
__publicField(this, "crawlProtect", null);
|
|
1178
2545
|
__publicField(this, "initialized", false);
|
|
1179
2546
|
__publicField(this, "wasmService");
|
|
1180
2547
|
__publicField(this, "callbackManager");
|
|
@@ -1182,7 +2549,6 @@ class SecuritySDK {
|
|
|
1182
2549
|
this.wasmService = new WasmService();
|
|
1183
2550
|
this.callbackManager = new CallbackManager();
|
|
1184
2551
|
this.transmitter.setCallbackManager(this.callbackManager);
|
|
1185
|
-
this.collector = new EventCollector(this.transmitter);
|
|
1186
2552
|
this.behaviorCollector = new BehaviorCollector(this.transmitter);
|
|
1187
2553
|
this.rapidClickDetector = new RapidClickDetector(this.transmitter);
|
|
1188
2554
|
this.honeypot = new Honeypot(this.transmitter);
|
|
@@ -1190,6 +2556,12 @@ class SecuritySDK {
|
|
|
1190
2556
|
this.canvasFingerprinter = new CanvasFingerprinter(this.transmitter, this.wasmService);
|
|
1191
2557
|
this.inputTracker = new InputTracker(this.transmitter);
|
|
1192
2558
|
this.invisibleInteraction = new InvisibleInteraction(this.transmitter);
|
|
2559
|
+
this.automationDetector = new AutomationDetector(this.transmitter);
|
|
2560
|
+
this.sessionAnalyzer = new SessionAnalyzer(this.transmitter);
|
|
2561
|
+
this.tabNavigationAnalyzer = new TabNavigationAnalyzer(this.transmitter);
|
|
2562
|
+
this.cloudEnvironmentDetector = new CloudEnvironmentDetector(this.transmitter);
|
|
2563
|
+
this.crossContextDetector = new CrossContextDetector(this.transmitter);
|
|
2564
|
+
this.headlessProbeDetector = new HeadlessProbeDetector(this.transmitter);
|
|
1193
2565
|
}
|
|
1194
2566
|
async init(config) {
|
|
1195
2567
|
var _a, _b;
|
|
@@ -1213,13 +2585,25 @@ class SecuritySDK {
|
|
|
1213
2585
|
if (devtoolsEnabled) {
|
|
1214
2586
|
import("../anti-debug-CRuvY4WC.js").then(({ AntiDebug }) => {
|
|
1215
2587
|
AntiDebug.start();
|
|
1216
|
-
import("../console-
|
|
2588
|
+
import("../console-DbZZ4Ctg.js").then(({ ConsoleDetector }) => {
|
|
1217
2589
|
new ConsoleDetector(() => {
|
|
1218
2590
|
this.transmitter.send("devtools_open", { detected: true });
|
|
1219
2591
|
}).start();
|
|
1220
2592
|
});
|
|
1221
2593
|
});
|
|
1222
2594
|
}
|
|
2595
|
+
if (config.crawlProtect) {
|
|
2596
|
+
this.crawlProtect = new CrawlProtect({
|
|
2597
|
+
apiBase: baseApiUrl,
|
|
2598
|
+
clientId: config.clientKey,
|
|
2599
|
+
protectSelectors: config.crawlProtect.protectSelectors,
|
|
2600
|
+
insertHoneypot: config.crawlProtect.insertHoneypot,
|
|
2601
|
+
debug: config.debug
|
|
2602
|
+
});
|
|
2603
|
+
this.crawlProtect.start().catch((err) => {
|
|
2604
|
+
if (config.debug) ;
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
1223
2607
|
this.initialized = true;
|
|
1224
2608
|
if (config.debug) {
|
|
1225
2609
|
}
|
|
@@ -1243,13 +2627,18 @@ class SecuritySDK {
|
|
|
1243
2627
|
this.callbackManager.once(event, handler);
|
|
1244
2628
|
}
|
|
1245
2629
|
startDetectors() {
|
|
1246
|
-
this.collector.start();
|
|
1247
2630
|
this.behaviorCollector.start();
|
|
1248
2631
|
this.honeypot.start();
|
|
1249
2632
|
this.mouseTracker.start();
|
|
1250
2633
|
this.canvasFingerprinter.start();
|
|
1251
2634
|
this.inputTracker.start();
|
|
1252
2635
|
this.invisibleInteraction.start();
|
|
2636
|
+
this.automationDetector.start();
|
|
2637
|
+
this.sessionAnalyzer.start();
|
|
2638
|
+
this.tabNavigationAnalyzer.start();
|
|
2639
|
+
this.cloudEnvironmentDetector.start();
|
|
2640
|
+
this.crossContextDetector.start();
|
|
2641
|
+
this.headlessProbeDetector.start();
|
|
1253
2642
|
}
|
|
1254
2643
|
}
|
|
1255
2644
|
const securitySDK = new SecuritySDK();
|