@niksbanna/bot-detector 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +245 -0
- package/dist/bot-detector.cjs.js +2629 -0
- package/dist/bot-detector.cjs.js.map +7 -0
- package/dist/bot-detector.esm.js +2609 -0
- package/dist/bot-detector.esm.js.map +7 -0
- package/dist/bot-detector.iife.js +2631 -0
- package/dist/bot-detector.iife.js.map +7 -0
- package/dist/bot-detector.iife.min.js +1 -0
- package/package.json +57 -0
- package/src/core/BotDetector.js +284 -0
- package/src/core/ScoringEngine.js +134 -0
- package/src/core/Signal.js +181 -0
- package/src/core/VerdictEngine.js +132 -0
- package/src/index.js +273 -0
- package/src/signals/automation/PhantomJSSignal.js +137 -0
- package/src/signals/automation/PlaywrightSignal.js +129 -0
- package/src/signals/automation/PuppeteerSignal.js +117 -0
- package/src/signals/automation/SeleniumSignal.js +151 -0
- package/src/signals/automation/index.js +8 -0
- package/src/signals/behavior/InteractionTimingSignal.js +170 -0
- package/src/signals/behavior/KeyboardPatternSignal.js +235 -0
- package/src/signals/behavior/MouseMovementSignal.js +215 -0
- package/src/signals/behavior/ScrollBehaviorSignal.js +236 -0
- package/src/signals/behavior/index.js +8 -0
- package/src/signals/environment/HeadlessSignal.js +97 -0
- package/src/signals/environment/NavigatorAnomalySignal.js +117 -0
- package/src/signals/environment/PermissionsSignal.js +76 -0
- package/src/signals/environment/WebDriverSignal.js +58 -0
- package/src/signals/environment/index.js +8 -0
- package/src/signals/fingerprint/AudioContextSignal.js +158 -0
- package/src/signals/fingerprint/CanvasSignal.js +133 -0
- package/src/signals/fingerprint/PluginsSignal.js +106 -0
- package/src/signals/fingerprint/ScreenSignal.js +157 -0
- package/src/signals/fingerprint/WebGLSignal.js +146 -0
- package/src/signals/fingerprint/index.js +9 -0
- package/src/signals/timing/DOMContentTimingSignal.js +159 -0
- package/src/signals/timing/PageLoadSignal.js +165 -0
- package/src/signals/timing/index.js +6 -0
|
@@ -0,0 +1,2629 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
20
|
+
|
|
21
|
+
// src/index.js
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
AudioContextSignal: () => AudioContextSignal,
|
|
25
|
+
BotDetector: () => BotDetector,
|
|
26
|
+
CanvasSignal: () => CanvasSignal,
|
|
27
|
+
DOMContentTimingSignal: () => DOMContentTimingSignal,
|
|
28
|
+
HeadlessSignal: () => HeadlessSignal,
|
|
29
|
+
InteractionTimingSignal: () => InteractionTimingSignal,
|
|
30
|
+
KeyboardPatternSignal: () => KeyboardPatternSignal,
|
|
31
|
+
MouseMovementSignal: () => MouseMovementSignal,
|
|
32
|
+
NavigatorAnomalySignal: () => NavigatorAnomalySignal,
|
|
33
|
+
PageLoadSignal: () => PageLoadSignal,
|
|
34
|
+
PermissionsSignal: () => PermissionsSignal,
|
|
35
|
+
PhantomJSSignal: () => PhantomJSSignal,
|
|
36
|
+
PlaywrightSignal: () => PlaywrightSignal,
|
|
37
|
+
PluginsSignal: () => PluginsSignal,
|
|
38
|
+
PuppeteerSignal: () => PuppeteerSignal,
|
|
39
|
+
ScoringEngine: () => ScoringEngine,
|
|
40
|
+
ScreenSignal: () => ScreenSignal,
|
|
41
|
+
ScrollBehaviorSignal: () => ScrollBehaviorSignal,
|
|
42
|
+
SeleniumSignal: () => SeleniumSignal,
|
|
43
|
+
Signal: () => Signal,
|
|
44
|
+
Signals: () => Signals,
|
|
45
|
+
Verdict: () => Verdict,
|
|
46
|
+
VerdictEngine: () => VerdictEngine,
|
|
47
|
+
WebDriverSignal: () => WebDriverSignal,
|
|
48
|
+
WebGLSignal: () => WebGLSignal,
|
|
49
|
+
createDetector: () => createDetector,
|
|
50
|
+
default: () => index_default,
|
|
51
|
+
defaultInstantSignals: () => defaultInstantSignals,
|
|
52
|
+
defaultInteractionSignals: () => defaultInteractionSignals,
|
|
53
|
+
defaultSignals: () => defaultSignals,
|
|
54
|
+
detect: () => detect,
|
|
55
|
+
detectInstant: () => detectInstant
|
|
56
|
+
});
|
|
57
|
+
module.exports = __toCommonJS(index_exports);
|
|
58
|
+
|
|
59
|
+
// src/core/Signal.js
|
|
60
|
+
var Signal = class {
|
|
61
|
+
/**
|
|
62
|
+
* Creates a new Signal instance.
|
|
63
|
+
* @param {Object} [options={}] - Configuration options for this signal.
|
|
64
|
+
*/
|
|
65
|
+
constructor(options = {}) {
|
|
66
|
+
this.options = options;
|
|
67
|
+
this._lastResult = null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the signal's unique identifier.
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
get id() {
|
|
74
|
+
return this.constructor.id;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the signal's category.
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
get category() {
|
|
81
|
+
return this.constructor.category;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get the signal's weight.
|
|
85
|
+
* @returns {number}
|
|
86
|
+
*/
|
|
87
|
+
get weight() {
|
|
88
|
+
var _a;
|
|
89
|
+
return (_a = this.options.weight) != null ? _a : this.constructor.weight;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the signal's description.
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
get description() {
|
|
96
|
+
return this.constructor.description;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if this signal requires interaction.
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
get requiresInteraction() {
|
|
103
|
+
return this.constructor.requiresInteraction;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the last detection result.
|
|
107
|
+
* @returns {SignalResult|null}
|
|
108
|
+
*/
|
|
109
|
+
get lastResult() {
|
|
110
|
+
return this._lastResult;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Perform the detection check.
|
|
114
|
+
* Must be overridden by subclasses.
|
|
115
|
+
*
|
|
116
|
+
* @abstract
|
|
117
|
+
* @returns {Promise<SignalResult>} Detection result
|
|
118
|
+
* @throws {Error} If not implemented by subclass
|
|
119
|
+
*/
|
|
120
|
+
async detect() {
|
|
121
|
+
throw new Error(`Signal.detect() must be implemented by ${this.constructor.name}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Run detection and cache the result.
|
|
125
|
+
* @returns {Promise<SignalResult>}
|
|
126
|
+
*/
|
|
127
|
+
async run() {
|
|
128
|
+
try {
|
|
129
|
+
this._lastResult = await this.detect();
|
|
130
|
+
return this._lastResult;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this._lastResult = {
|
|
133
|
+
triggered: false,
|
|
134
|
+
value: null,
|
|
135
|
+
confidence: 0,
|
|
136
|
+
error: error.message
|
|
137
|
+
};
|
|
138
|
+
return this._lastResult;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reset the signal state.
|
|
143
|
+
*/
|
|
144
|
+
reset() {
|
|
145
|
+
this._lastResult = null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create a result object with defaults.
|
|
149
|
+
* @param {boolean} triggered - Whether the signal was triggered
|
|
150
|
+
* @param {*} [value=null] - Optional value associated with the detection
|
|
151
|
+
* @param {number} [confidence=1] - Confidence level (0-1)
|
|
152
|
+
* @returns {SignalResult}
|
|
153
|
+
*/
|
|
154
|
+
createResult(triggered, value = null, confidence = 1) {
|
|
155
|
+
return {
|
|
156
|
+
triggered: Boolean(triggered),
|
|
157
|
+
value,
|
|
158
|
+
confidence: Math.max(0, Math.min(1, confidence))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Unique identifier for this signal.
|
|
164
|
+
* @type {string}
|
|
165
|
+
*/
|
|
166
|
+
__publicField(Signal, "id", "base-signal");
|
|
167
|
+
/**
|
|
168
|
+
* Category this signal belongs to.
|
|
169
|
+
* Categories: 'environment', 'behavior', 'fingerprint', 'timing', 'automation'
|
|
170
|
+
* @type {string}
|
|
171
|
+
*/
|
|
172
|
+
__publicField(Signal, "category", "uncategorized");
|
|
173
|
+
/**
|
|
174
|
+
* Weight of this signal in the scoring calculation.
|
|
175
|
+
* Range: 0.1 (low importance) to 1.0 (high importance)
|
|
176
|
+
* @type {number}
|
|
177
|
+
*/
|
|
178
|
+
__publicField(Signal, "weight", 0.5);
|
|
179
|
+
/**
|
|
180
|
+
* Human-readable description of what this signal detects.
|
|
181
|
+
* @type {string}
|
|
182
|
+
*/
|
|
183
|
+
__publicField(Signal, "description", "Base signal class");
|
|
184
|
+
/**
|
|
185
|
+
* Whether this signal requires user interaction before it can detect.
|
|
186
|
+
* @type {boolean}
|
|
187
|
+
*/
|
|
188
|
+
__publicField(Signal, "requiresInteraction", false);
|
|
189
|
+
|
|
190
|
+
// src/core/ScoringEngine.js
|
|
191
|
+
var ScoringEngine = class {
|
|
192
|
+
/**
|
|
193
|
+
* Creates a new ScoringEngine instance.
|
|
194
|
+
* @param {Object} [options={}] - Configuration options
|
|
195
|
+
* @param {Object.<string, number>} [options.weightOverrides={}] - Override weights by signal ID
|
|
196
|
+
* @param {number} [options.maxScore=100] - Maximum possible score
|
|
197
|
+
*/
|
|
198
|
+
constructor(options = {}) {
|
|
199
|
+
this.weightOverrides = options.weightOverrides || {};
|
|
200
|
+
this.maxScore = options.maxScore || 100;
|
|
201
|
+
this._results = /* @__PURE__ */ new Map();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get the effective weight for a signal.
|
|
205
|
+
* @param {string} signalId - Signal identifier
|
|
206
|
+
* @param {number} defaultWeight - Default weight from signal definition
|
|
207
|
+
* @returns {number}
|
|
208
|
+
*/
|
|
209
|
+
getWeight(signalId, defaultWeight) {
|
|
210
|
+
var _a;
|
|
211
|
+
return (_a = this.weightOverrides[signalId]) != null ? _a : defaultWeight;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Add a signal result to the scoring calculation.
|
|
215
|
+
* @param {string} signalId - Signal identifier
|
|
216
|
+
* @param {Object} result - Signal detection result
|
|
217
|
+
* @param {boolean} result.triggered - Whether signal was triggered
|
|
218
|
+
* @param {number} result.confidence - Confidence level (0-1)
|
|
219
|
+
* @param {number} weight - Signal weight (0.1-1.0)
|
|
220
|
+
*/
|
|
221
|
+
addResult(signalId, result, weight) {
|
|
222
|
+
const effectiveWeight = this.getWeight(signalId, weight);
|
|
223
|
+
this._results.set(signalId, {
|
|
224
|
+
...result,
|
|
225
|
+
weight: effectiveWeight,
|
|
226
|
+
contribution: result.triggered ? effectiveWeight * result.confidence : 0
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Calculate the final score from all added results.
|
|
231
|
+
* Formula: (sum of contributions / sum of weights) * maxScore
|
|
232
|
+
*
|
|
233
|
+
* @returns {number} Score between 0 and maxScore
|
|
234
|
+
*/
|
|
235
|
+
calculate() {
|
|
236
|
+
if (this._results.size === 0) {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
let totalWeight = 0;
|
|
240
|
+
let totalContribution = 0;
|
|
241
|
+
for (const [, data] of this._results) {
|
|
242
|
+
totalWeight += data.weight;
|
|
243
|
+
totalContribution += data.contribution;
|
|
244
|
+
}
|
|
245
|
+
if (totalWeight === 0) {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
const score = totalContribution / totalWeight * this.maxScore;
|
|
249
|
+
return Math.round(score * 100) / 100;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get detailed breakdown of score contributions.
|
|
253
|
+
* @returns {Array<Object>} Array of signal contributions
|
|
254
|
+
*/
|
|
255
|
+
getBreakdown() {
|
|
256
|
+
const breakdown = [];
|
|
257
|
+
for (const [signalId, data] of this._results) {
|
|
258
|
+
breakdown.push({
|
|
259
|
+
signalId,
|
|
260
|
+
triggered: data.triggered,
|
|
261
|
+
confidence: data.confidence,
|
|
262
|
+
weight: data.weight,
|
|
263
|
+
contribution: data.contribution,
|
|
264
|
+
percentOfScore: this.calculate() > 0 ? (data.contribution / this.calculate() * 100).toFixed(1) : "0.0"
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return breakdown.sort((a, b) => b.contribution - a.contribution);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get all triggered signals.
|
|
271
|
+
* @returns {Array<string>} Array of triggered signal IDs
|
|
272
|
+
*/
|
|
273
|
+
getTriggeredSignals() {
|
|
274
|
+
const triggered = [];
|
|
275
|
+
for (const [signalId, data] of this._results) {
|
|
276
|
+
if (data.triggered) {
|
|
277
|
+
triggered.push(signalId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return triggered;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get the number of triggered signals.
|
|
284
|
+
* @returns {number}
|
|
285
|
+
*/
|
|
286
|
+
getTriggeredCount() {
|
|
287
|
+
let count = 0;
|
|
288
|
+
for (const [, data] of this._results) {
|
|
289
|
+
if (data.triggered) count++;
|
|
290
|
+
}
|
|
291
|
+
return count;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Reset all stored results.
|
|
295
|
+
*/
|
|
296
|
+
reset() {
|
|
297
|
+
this._results.clear();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/core/VerdictEngine.js
|
|
302
|
+
var Verdict = {
|
|
303
|
+
HUMAN: "human",
|
|
304
|
+
SUSPICIOUS: "suspicious",
|
|
305
|
+
BOT: "bot"
|
|
306
|
+
};
|
|
307
|
+
var _VerdictEngine = class _VerdictEngine {
|
|
308
|
+
/**
|
|
309
|
+
* Creates a new VerdictEngine instance.
|
|
310
|
+
* @param {Object} [options={}] - Configuration options
|
|
311
|
+
* @param {number} [options.humanThreshold=20] - Max score for human verdict
|
|
312
|
+
* @param {number} [options.suspiciousThreshold=50] - Max score for suspicious verdict
|
|
313
|
+
* @param {Array<string>} [options.instantBotSignals=[]] - Signal IDs that instantly flag as bot
|
|
314
|
+
*/
|
|
315
|
+
constructor(options = {}) {
|
|
316
|
+
var _a, _b;
|
|
317
|
+
this.humanThreshold = (_a = options.humanThreshold) != null ? _a : _VerdictEngine.DEFAULT_THRESHOLDS.human;
|
|
318
|
+
this.suspiciousThreshold = (_b = options.suspiciousThreshold) != null ? _b : _VerdictEngine.DEFAULT_THRESHOLDS.suspicious;
|
|
319
|
+
this.instantBotSignals = new Set(options.instantBotSignals || []);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get the verdict based on score and triggered signals.
|
|
323
|
+
* @param {number} score - Calculated score (0-100)
|
|
324
|
+
* @param {Array<string>} triggeredSignals - List of triggered signal IDs
|
|
325
|
+
* @returns {VerdictResult}
|
|
326
|
+
*/
|
|
327
|
+
getVerdict(score, triggeredSignals = []) {
|
|
328
|
+
for (const signalId of triggeredSignals) {
|
|
329
|
+
if (this.instantBotSignals.has(signalId)) {
|
|
330
|
+
return {
|
|
331
|
+
verdict: Verdict.BOT,
|
|
332
|
+
score,
|
|
333
|
+
confidence: "high",
|
|
334
|
+
reason: `Instant bot signal triggered: ${signalId}`,
|
|
335
|
+
triggeredCount: triggeredSignals.length
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
let verdict;
|
|
340
|
+
let confidence;
|
|
341
|
+
let reason;
|
|
342
|
+
if (score < this.humanThreshold) {
|
|
343
|
+
verdict = Verdict.HUMAN;
|
|
344
|
+
confidence = score < 10 ? "high" : "medium";
|
|
345
|
+
reason = "Low bot score";
|
|
346
|
+
} else if (score < this.suspiciousThreshold) {
|
|
347
|
+
verdict = Verdict.SUSPICIOUS;
|
|
348
|
+
confidence = "medium";
|
|
349
|
+
reason = "Moderate bot indicators detected";
|
|
350
|
+
} else {
|
|
351
|
+
verdict = Verdict.BOT;
|
|
352
|
+
confidence = score >= 75 ? "high" : "medium";
|
|
353
|
+
reason = "High accumulation of bot indicators";
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
verdict,
|
|
357
|
+
score,
|
|
358
|
+
confidence,
|
|
359
|
+
reason,
|
|
360
|
+
triggeredCount: triggeredSignals.length
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Check if signal should instantly flag as bot.
|
|
365
|
+
* @param {string} signalId - Signal identifier
|
|
366
|
+
* @returns {boolean}
|
|
367
|
+
*/
|
|
368
|
+
isInstantBotSignal(signalId) {
|
|
369
|
+
return this.instantBotSignals.has(signalId);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Add a signal to the instant bot list.
|
|
373
|
+
* @param {string} signalId - Signal identifier
|
|
374
|
+
*/
|
|
375
|
+
addInstantBotSignal(signalId) {
|
|
376
|
+
this.instantBotSignals.add(signalId);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Update thresholds.
|
|
380
|
+
* @param {Object} thresholds - New threshold values
|
|
381
|
+
* @param {number} [thresholds.human] - New human threshold
|
|
382
|
+
* @param {number} [thresholds.suspicious] - New suspicious threshold
|
|
383
|
+
*/
|
|
384
|
+
setThresholds(thresholds) {
|
|
385
|
+
if (thresholds.human !== void 0) {
|
|
386
|
+
this.humanThreshold = thresholds.human;
|
|
387
|
+
}
|
|
388
|
+
if (thresholds.suspicious !== void 0) {
|
|
389
|
+
this.suspiciousThreshold = thresholds.suspicious;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Default threshold configuration.
|
|
395
|
+
* @type {Object}
|
|
396
|
+
*/
|
|
397
|
+
__publicField(_VerdictEngine, "DEFAULT_THRESHOLDS", {
|
|
398
|
+
human: 20,
|
|
399
|
+
// score < 20 = human
|
|
400
|
+
suspicious: 50
|
|
401
|
+
// 20 <= score < 50 = suspicious
|
|
402
|
+
// score >= 50 = bot
|
|
403
|
+
});
|
|
404
|
+
var VerdictEngine = _VerdictEngine;
|
|
405
|
+
|
|
406
|
+
// src/core/BotDetector.js
|
|
407
|
+
var BotDetector = class _BotDetector {
|
|
408
|
+
/**
|
|
409
|
+
* Creates a new BotDetector instance.
|
|
410
|
+
* @param {Object} [options={}] - Configuration options
|
|
411
|
+
* @param {Array<Signal>} [options.signals=[]] - Initial signals to register
|
|
412
|
+
* @param {Object.<string, number>} [options.weightOverrides={}] - Override signal weights
|
|
413
|
+
* @param {number} [options.humanThreshold=20] - Score threshold for human verdict
|
|
414
|
+
* @param {number} [options.suspiciousThreshold=50] - Score threshold for suspicious verdict
|
|
415
|
+
* @param {Array<string>} [options.instantBotSignals=[]] - Signal IDs that instantly flag as bot
|
|
416
|
+
* @param {boolean} [options.includeDefaults=true] - Include built-in signal detectors
|
|
417
|
+
* @param {number} [options.detectionTimeout=5000] - Timeout for detection in ms
|
|
418
|
+
*/
|
|
419
|
+
constructor(options = {}) {
|
|
420
|
+
this.options = options;
|
|
421
|
+
this._signals = /* @__PURE__ */ new Map();
|
|
422
|
+
this._scoringEngine = new ScoringEngine({
|
|
423
|
+
weightOverrides: options.weightOverrides
|
|
424
|
+
});
|
|
425
|
+
this._verdictEngine = new VerdictEngine({
|
|
426
|
+
humanThreshold: options.humanThreshold,
|
|
427
|
+
suspiciousThreshold: options.suspiciousThreshold,
|
|
428
|
+
instantBotSignals: options.instantBotSignals
|
|
429
|
+
});
|
|
430
|
+
this._lastDetection = null;
|
|
431
|
+
this._detectionTimeout = options.detectionTimeout || 5e3;
|
|
432
|
+
this._isRunning = false;
|
|
433
|
+
if (options.signals) {
|
|
434
|
+
for (const signal of options.signals) {
|
|
435
|
+
this.registerSignal(signal);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Register a signal detector.
|
|
441
|
+
* @param {Signal} signal - Signal instance to register
|
|
442
|
+
* @returns {BotDetector} this instance for chaining
|
|
443
|
+
* @throws {Error} If signal with same ID already registered
|
|
444
|
+
*/
|
|
445
|
+
registerSignal(signal) {
|
|
446
|
+
if (!(signal instanceof Signal)) {
|
|
447
|
+
throw new Error("Signal must be an instance of Signal class");
|
|
448
|
+
}
|
|
449
|
+
const id = signal.id;
|
|
450
|
+
if (this._signals.has(id)) {
|
|
451
|
+
throw new Error(`Signal with ID "${id}" is already registered`);
|
|
452
|
+
}
|
|
453
|
+
this._signals.set(id, signal);
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Register multiple signals at once.
|
|
458
|
+
* @param {Array<Signal>} signals - Array of signal instances
|
|
459
|
+
* @returns {BotDetector} this instance for chaining
|
|
460
|
+
*/
|
|
461
|
+
registerSignals(signals) {
|
|
462
|
+
for (const signal of signals) {
|
|
463
|
+
this.registerSignal(signal);
|
|
464
|
+
}
|
|
465
|
+
return this;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Unregister a signal by ID.
|
|
469
|
+
* @param {string} signalId - Signal ID to remove
|
|
470
|
+
* @returns {boolean} True if signal was removed
|
|
471
|
+
*/
|
|
472
|
+
unregisterSignal(signalId) {
|
|
473
|
+
return this._signals.delete(signalId);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get a registered signal by ID.
|
|
477
|
+
* @param {string} signalId - Signal ID
|
|
478
|
+
* @returns {Signal|undefined}
|
|
479
|
+
*/
|
|
480
|
+
getSignal(signalId) {
|
|
481
|
+
return this._signals.get(signalId);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get all registered signals.
|
|
485
|
+
* @returns {Array<Signal>}
|
|
486
|
+
*/
|
|
487
|
+
getSignals() {
|
|
488
|
+
return Array.from(this._signals.values());
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Get signals by category.
|
|
492
|
+
* @param {string} category - Category name
|
|
493
|
+
* @returns {Array<Signal>}
|
|
494
|
+
*/
|
|
495
|
+
getSignalsByCategory(category) {
|
|
496
|
+
return this.getSignals().filter((s) => s.category === category);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Run all signal detectors and calculate verdict.
|
|
500
|
+
* @param {Object} [options={}] - Detection options
|
|
501
|
+
* @param {boolean} [options.skipInteractionSignals=false] - Skip signals requiring interaction
|
|
502
|
+
* @returns {Promise<DetectionResult>}
|
|
503
|
+
*/
|
|
504
|
+
async detect(options = {}) {
|
|
505
|
+
if (this._isRunning) {
|
|
506
|
+
throw new Error("Detection is already running");
|
|
507
|
+
}
|
|
508
|
+
this._isRunning = true;
|
|
509
|
+
this._scoringEngine.reset();
|
|
510
|
+
const startTime = performance.now();
|
|
511
|
+
const signalResults = /* @__PURE__ */ new Map();
|
|
512
|
+
try {
|
|
513
|
+
const signalsToRun = this.getSignals().filter((signal) => {
|
|
514
|
+
if (options.skipInteractionSignals && signal.requiresInteraction) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
});
|
|
519
|
+
const detectionPromises = signalsToRun.map(async (signal) => {
|
|
520
|
+
const result = await Promise.race([
|
|
521
|
+
signal.run(),
|
|
522
|
+
new Promise(
|
|
523
|
+
(resolve) => setTimeout(() => resolve({
|
|
524
|
+
triggered: false,
|
|
525
|
+
value: null,
|
|
526
|
+
confidence: 0,
|
|
527
|
+
error: "timeout"
|
|
528
|
+
}), this._detectionTimeout)
|
|
529
|
+
)
|
|
530
|
+
]);
|
|
531
|
+
return { signal, result };
|
|
532
|
+
});
|
|
533
|
+
const results = await Promise.all(detectionPromises);
|
|
534
|
+
for (const { signal, result } of results) {
|
|
535
|
+
signalResults.set(signal.id, {
|
|
536
|
+
...result,
|
|
537
|
+
category: signal.category,
|
|
538
|
+
weight: signal.weight,
|
|
539
|
+
description: signal.description
|
|
540
|
+
});
|
|
541
|
+
this._scoringEngine.addResult(signal.id, result, signal.weight);
|
|
542
|
+
}
|
|
543
|
+
const score = this._scoringEngine.calculate();
|
|
544
|
+
const triggeredSignals = this._scoringEngine.getTriggeredSignals();
|
|
545
|
+
const verdict = this._verdictEngine.getVerdict(score, triggeredSignals);
|
|
546
|
+
const detectionTime = performance.now() - startTime;
|
|
547
|
+
this._lastDetection = {
|
|
548
|
+
...verdict,
|
|
549
|
+
signals: Object.fromEntries(signalResults),
|
|
550
|
+
breakdown: this._scoringEngine.getBreakdown(),
|
|
551
|
+
timestamp: Date.now(),
|
|
552
|
+
detectionTimeMs: Math.round(detectionTime),
|
|
553
|
+
totalSignals: signalsToRun.length,
|
|
554
|
+
triggeredSignals
|
|
555
|
+
};
|
|
556
|
+
return this._lastDetection;
|
|
557
|
+
} finally {
|
|
558
|
+
this._isRunning = false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get the last detection result.
|
|
563
|
+
* @returns {DetectionResult|null}
|
|
564
|
+
*/
|
|
565
|
+
getLastDetection() {
|
|
566
|
+
return this._lastDetection;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get the current score (from last detection).
|
|
570
|
+
* @returns {number}
|
|
571
|
+
*/
|
|
572
|
+
getScore() {
|
|
573
|
+
var _a, _b;
|
|
574
|
+
return (_b = (_a = this._lastDetection) == null ? void 0 : _a.score) != null ? _b : 0;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Get triggered signals from last detection.
|
|
578
|
+
* @returns {Array<string>}
|
|
579
|
+
*/
|
|
580
|
+
getTriggeredSignals() {
|
|
581
|
+
var _a, _b;
|
|
582
|
+
return (_b = (_a = this._lastDetection) == null ? void 0 : _a.triggeredSignals) != null ? _b : [];
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Check if detection is currently running.
|
|
586
|
+
* @returns {boolean}
|
|
587
|
+
*/
|
|
588
|
+
isRunning() {
|
|
589
|
+
return this._isRunning;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Reset the detector state.
|
|
593
|
+
*/
|
|
594
|
+
reset() {
|
|
595
|
+
this._scoringEngine.reset();
|
|
596
|
+
this._lastDetection = null;
|
|
597
|
+
for (const signal of this._signals.values()) {
|
|
598
|
+
signal.reset();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Update configuration options.
|
|
603
|
+
* @param {Object} options - New options
|
|
604
|
+
*/
|
|
605
|
+
configure(options) {
|
|
606
|
+
if (options.humanThreshold !== void 0 || options.suspiciousThreshold !== void 0) {
|
|
607
|
+
this._verdictEngine.setThresholds({
|
|
608
|
+
human: options.humanThreshold,
|
|
609
|
+
suspicious: options.suspiciousThreshold
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (options.detectionTimeout !== void 0) {
|
|
613
|
+
this._detectionTimeout = options.detectionTimeout;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Create a detector with default signals.
|
|
618
|
+
* @param {Object} [options={}] - Configuration options
|
|
619
|
+
* @returns {BotDetector}
|
|
620
|
+
*/
|
|
621
|
+
static withDefaults(options = {}) {
|
|
622
|
+
return new _BotDetector(options);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/signals/environment/WebDriverSignal.js
|
|
627
|
+
var WebDriverSignal = class extends Signal {
|
|
628
|
+
async detect() {
|
|
629
|
+
if (navigator.webdriver === true) {
|
|
630
|
+
return this.createResult(true, { webdriver: true }, 1);
|
|
631
|
+
}
|
|
632
|
+
const descriptor = Object.getOwnPropertyDescriptor(navigator, "webdriver");
|
|
633
|
+
if (descriptor) {
|
|
634
|
+
if (descriptor.get || !descriptor.configurable) {
|
|
635
|
+
return this.createResult(true, {
|
|
636
|
+
webdriver: "modified",
|
|
637
|
+
descriptor: {
|
|
638
|
+
configurable: descriptor.configurable,
|
|
639
|
+
enumerable: descriptor.enumerable,
|
|
640
|
+
hasGetter: !!descriptor.get
|
|
641
|
+
}
|
|
642
|
+
}, 0.8);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const proto = Object.getPrototypeOf(navigator);
|
|
647
|
+
const protoDescriptor = Object.getOwnPropertyDescriptor(proto, "webdriver");
|
|
648
|
+
if (protoDescriptor && protoDescriptor.get) {
|
|
649
|
+
const value = protoDescriptor.get.call(navigator);
|
|
650
|
+
if (value === true) {
|
|
651
|
+
return this.createResult(true, { webdriver: true, source: "prototype" }, 1);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch (e) {
|
|
655
|
+
}
|
|
656
|
+
return this.createResult(false);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
__publicField(WebDriverSignal, "id", "webdriver");
|
|
660
|
+
__publicField(WebDriverSignal, "category", "environment");
|
|
661
|
+
__publicField(WebDriverSignal, "weight", 1);
|
|
662
|
+
__publicField(WebDriverSignal, "description", "Detects navigator.webdriver automation flag");
|
|
663
|
+
|
|
664
|
+
// src/signals/environment/HeadlessSignal.js
|
|
665
|
+
var HeadlessSignal = class extends Signal {
|
|
666
|
+
async detect() {
|
|
667
|
+
const indicators = [];
|
|
668
|
+
let confidence = 0;
|
|
669
|
+
const ua = navigator.userAgent || "";
|
|
670
|
+
if (ua.includes("HeadlessChrome")) {
|
|
671
|
+
indicators.push("headless-ua");
|
|
672
|
+
confidence = Math.max(confidence, 1);
|
|
673
|
+
}
|
|
674
|
+
if (ua.includes("Chrome") && !ua.includes("Chromium")) {
|
|
675
|
+
if (typeof window.chrome === "undefined") {
|
|
676
|
+
indicators.push("missing-chrome-object");
|
|
677
|
+
confidence = Math.max(confidence, 0.6);
|
|
678
|
+
} else if (!window.chrome.runtime) {
|
|
679
|
+
indicators.push("missing-chrome-runtime");
|
|
680
|
+
confidence = Math.max(confidence, 0.4);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
684
|
+
indicators.push("no-plugins");
|
|
685
|
+
confidence = Math.max(confidence, 0.5);
|
|
686
|
+
}
|
|
687
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
688
|
+
indicators.push("no-languages");
|
|
689
|
+
confidence = Math.max(confidence, 0.6);
|
|
690
|
+
}
|
|
691
|
+
if (window.outerWidth === 0 && window.outerHeight === 0) {
|
|
692
|
+
indicators.push("zero-outer-dimensions");
|
|
693
|
+
confidence = Math.max(confidence, 0.7);
|
|
694
|
+
}
|
|
695
|
+
if (typeof navigator.connection === "undefined" && ua.includes("Chrome")) {
|
|
696
|
+
indicators.push("missing-connection-api");
|
|
697
|
+
confidence = Math.max(confidence, 0.3);
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
if (typeof Notification !== "undefined" && Notification.permission === "denied" && window.outerWidth === 0) {
|
|
701
|
+
indicators.push("notification-headless-pattern");
|
|
702
|
+
confidence = Math.max(confidence, 0.5);
|
|
703
|
+
}
|
|
704
|
+
} catch (e) {
|
|
705
|
+
}
|
|
706
|
+
if (window.callPhantom || window._phantom) {
|
|
707
|
+
indicators.push("phantomjs");
|
|
708
|
+
confidence = Math.max(confidence, 1);
|
|
709
|
+
}
|
|
710
|
+
if (window.__nightmare) {
|
|
711
|
+
indicators.push("nightmare");
|
|
712
|
+
confidence = Math.max(confidence, 1);
|
|
713
|
+
}
|
|
714
|
+
const triggered = indicators.length > 0;
|
|
715
|
+
if (indicators.length >= 3) {
|
|
716
|
+
confidence = Math.min(1, confidence + 0.2);
|
|
717
|
+
}
|
|
718
|
+
return this.createResult(triggered, { indicators }, confidence);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
__publicField(HeadlessSignal, "id", "headless");
|
|
722
|
+
__publicField(HeadlessSignal, "category", "environment");
|
|
723
|
+
__publicField(HeadlessSignal, "weight", 0.8);
|
|
724
|
+
__publicField(HeadlessSignal, "description", "Detects headless browser indicators");
|
|
725
|
+
|
|
726
|
+
// src/signals/environment/NavigatorAnomalySignal.js
|
|
727
|
+
var NavigatorAnomalySignal = class extends Signal {
|
|
728
|
+
async detect() {
|
|
729
|
+
const anomalies = [];
|
|
730
|
+
let totalScore = 0;
|
|
731
|
+
let checksPerformed = 0;
|
|
732
|
+
const ua = navigator.userAgent || "";
|
|
733
|
+
const platform = navigator.platform || "";
|
|
734
|
+
checksPerformed++;
|
|
735
|
+
if (platform.includes("Win") && !ua.includes("Windows")) {
|
|
736
|
+
anomalies.push("platform-ua-mismatch-windows");
|
|
737
|
+
totalScore += 1;
|
|
738
|
+
} else if (platform.includes("Mac") && !ua.includes("Mac")) {
|
|
739
|
+
anomalies.push("platform-ua-mismatch-mac");
|
|
740
|
+
totalScore += 1;
|
|
741
|
+
} else if (platform.includes("Linux") && !ua.includes("Linux") && !ua.includes("Android")) {
|
|
742
|
+
anomalies.push("platform-ua-mismatch-linux");
|
|
743
|
+
totalScore += 1;
|
|
744
|
+
}
|
|
745
|
+
checksPerformed++;
|
|
746
|
+
if (!platform || platform === "" || platform === "undefined") {
|
|
747
|
+
anomalies.push("empty-platform");
|
|
748
|
+
totalScore += 1;
|
|
749
|
+
}
|
|
750
|
+
checksPerformed++;
|
|
751
|
+
if (navigator.language && navigator.languages) {
|
|
752
|
+
if (!navigator.languages.includes(navigator.language)) {
|
|
753
|
+
anomalies.push("language-mismatch");
|
|
754
|
+
totalScore += 0.5;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
checksPerformed++;
|
|
758
|
+
if (ua.includes("Chrome") && navigator.vendor !== "Google Inc.") {
|
|
759
|
+
anomalies.push("vendor-mismatch-chrome");
|
|
760
|
+
totalScore += 0.5;
|
|
761
|
+
} else if (ua.includes("Firefox") && navigator.vendor !== "") {
|
|
762
|
+
anomalies.push("vendor-mismatch-firefox");
|
|
763
|
+
totalScore += 0.5;
|
|
764
|
+
} else if (ua.includes("Safari") && !ua.includes("Chrome") && navigator.vendor !== "Apple Computer, Inc.") {
|
|
765
|
+
anomalies.push("vendor-mismatch-safari");
|
|
766
|
+
totalScore += 0.5;
|
|
767
|
+
}
|
|
768
|
+
checksPerformed++;
|
|
769
|
+
if (typeof navigator.hardwareConcurrency !== "undefined") {
|
|
770
|
+
if (navigator.hardwareConcurrency === 0 || navigator.hardwareConcurrency > 128) {
|
|
771
|
+
anomalies.push("suspicious-hardware-concurrency");
|
|
772
|
+
totalScore += 0.5;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
checksPerformed++;
|
|
776
|
+
if (typeof navigator.deviceMemory !== "undefined") {
|
|
777
|
+
if (navigator.deviceMemory === 0 || navigator.deviceMemory > 512) {
|
|
778
|
+
anomalies.push("suspicious-device-memory");
|
|
779
|
+
totalScore += 0.5;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
checksPerformed++;
|
|
783
|
+
const isMobileUA = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
|
|
784
|
+
const hasTouchPoints = navigator.maxTouchPoints > 0;
|
|
785
|
+
if (!isMobileUA && navigator.maxTouchPoints > 5) {
|
|
786
|
+
anomalies.push("desktop-high-touch-points");
|
|
787
|
+
totalScore += 0.3;
|
|
788
|
+
}
|
|
789
|
+
checksPerformed++;
|
|
790
|
+
try {
|
|
791
|
+
const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "userAgent");
|
|
792
|
+
if (desc && desc.get && desc.get.toString().includes("native code") === false) {
|
|
793
|
+
anomalies.push("spoofed-user-agent");
|
|
794
|
+
totalScore += 1;
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
}
|
|
798
|
+
const triggered = anomalies.length > 0;
|
|
799
|
+
const confidence = Math.min(1, totalScore / Math.max(1, checksPerformed));
|
|
800
|
+
return this.createResult(triggered, { anomalies }, confidence);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
__publicField(NavigatorAnomalySignal, "id", "navigator-anomaly");
|
|
804
|
+
__publicField(NavigatorAnomalySignal, "category", "environment");
|
|
805
|
+
__publicField(NavigatorAnomalySignal, "weight", 0.7);
|
|
806
|
+
__publicField(NavigatorAnomalySignal, "description", "Detects navigator property inconsistencies");
|
|
807
|
+
|
|
808
|
+
// src/signals/environment/PermissionsSignal.js
|
|
809
|
+
var PermissionsSignal = class extends Signal {
|
|
810
|
+
async detect() {
|
|
811
|
+
const anomalies = [];
|
|
812
|
+
if (!navigator.permissions) {
|
|
813
|
+
return this.createResult(false, { supported: false }, 0);
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const notificationStatus = await navigator.permissions.query({ name: "notifications" });
|
|
817
|
+
if (typeof Notification !== "undefined") {
|
|
818
|
+
const directPermission = Notification.permission;
|
|
819
|
+
if (directPermission === "granted" && notificationStatus.state !== "granted" || directPermission === "denied" && notificationStatus.state !== "denied" || directPermission === "default" && notificationStatus.state !== "prompt") {
|
|
820
|
+
anomalies.push("notification-permission-mismatch");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const geoStatus = await navigator.permissions.query({ name: "geolocation" });
|
|
825
|
+
if (geoStatus.state === "denied" && window.outerWidth === 0) {
|
|
826
|
+
anomalies.push("geo-denied-headless");
|
|
827
|
+
}
|
|
828
|
+
} catch (e) {
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
await navigator.permissions.query({ name: "camera" });
|
|
832
|
+
} catch (e) {
|
|
833
|
+
if (e.name === "TypeError") {
|
|
834
|
+
anomalies.push("camera-permission-error");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} catch (e) {
|
|
838
|
+
if (e.name !== "TypeError") {
|
|
839
|
+
anomalies.push("permissions-query-error");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const triggered = anomalies.length > 0;
|
|
843
|
+
const confidence = Math.min(1, anomalies.length * 0.4);
|
|
844
|
+
return this.createResult(triggered, { anomalies }, confidence);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
__publicField(PermissionsSignal, "id", "permissions");
|
|
848
|
+
__publicField(PermissionsSignal, "category", "environment");
|
|
849
|
+
__publicField(PermissionsSignal, "weight", 0.5);
|
|
850
|
+
__publicField(PermissionsSignal, "description", "Detects Permissions API anomalies");
|
|
851
|
+
|
|
852
|
+
// src/signals/behavior/MouseMovementSignal.js
|
|
853
|
+
var MouseMovementSignal = class extends Signal {
|
|
854
|
+
constructor(options = {}) {
|
|
855
|
+
super(options);
|
|
856
|
+
this._movements = [];
|
|
857
|
+
this._isTracking = false;
|
|
858
|
+
this._trackingDuration = options.trackingDuration || 3e3;
|
|
859
|
+
this._minMovements = options.minMovements || 5;
|
|
860
|
+
this._boundHandler = null;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Start tracking mouse movements.
|
|
864
|
+
* @returns {Promise<void>}
|
|
865
|
+
*/
|
|
866
|
+
startTracking() {
|
|
867
|
+
if (this._isTracking) return;
|
|
868
|
+
this._movements = [];
|
|
869
|
+
this._isTracking = true;
|
|
870
|
+
this._boundHandler = (e) => {
|
|
871
|
+
this._movements.push({
|
|
872
|
+
x: e.clientX,
|
|
873
|
+
y: e.clientY,
|
|
874
|
+
t: performance.now()
|
|
875
|
+
});
|
|
876
|
+
};
|
|
877
|
+
document.addEventListener("mousemove", this._boundHandler, { passive: true });
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Stop tracking mouse movements.
|
|
881
|
+
*/
|
|
882
|
+
stopTracking() {
|
|
883
|
+
if (!this._isTracking) return;
|
|
884
|
+
this._isTracking = false;
|
|
885
|
+
if (this._boundHandler) {
|
|
886
|
+
document.removeEventListener("mousemove", this._boundHandler);
|
|
887
|
+
this._boundHandler = null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async detect() {
|
|
891
|
+
const anomalies = [];
|
|
892
|
+
let confidence = 0;
|
|
893
|
+
if (this._movements.length === 0) {
|
|
894
|
+
this.startTracking();
|
|
895
|
+
await new Promise((resolve) => setTimeout(resolve, this._trackingDuration));
|
|
896
|
+
this.stopTracking();
|
|
897
|
+
}
|
|
898
|
+
const movements = this._movements;
|
|
899
|
+
if (movements.length < this._minMovements) {
|
|
900
|
+
anomalies.push("no-mouse-movement");
|
|
901
|
+
confidence = Math.max(confidence, 0.6);
|
|
902
|
+
return this.createResult(true, { anomalies, movements: movements.length }, confidence);
|
|
903
|
+
}
|
|
904
|
+
const analysis = this._analyzeMovements(movements);
|
|
905
|
+
if (analysis.teleportCount > 0) {
|
|
906
|
+
anomalies.push("mouse-teleportation");
|
|
907
|
+
confidence = Math.max(confidence, 0.7);
|
|
908
|
+
}
|
|
909
|
+
if (analysis.linearPathRatio > 0.9) {
|
|
910
|
+
anomalies.push("linear-path");
|
|
911
|
+
confidence = Math.max(confidence, 0.8);
|
|
912
|
+
}
|
|
913
|
+
if (analysis.velocityVariance < 0.01 && movements.length > 10) {
|
|
914
|
+
anomalies.push("constant-velocity");
|
|
915
|
+
confidence = Math.max(confidence, 0.7);
|
|
916
|
+
}
|
|
917
|
+
if (analysis.accelerationChanges === 0 && movements.length > 10) {
|
|
918
|
+
anomalies.push("no-acceleration-variance");
|
|
919
|
+
confidence = Math.max(confidence, 0.6);
|
|
920
|
+
}
|
|
921
|
+
if (analysis.timingVariance < 1 && movements.length > 10) {
|
|
922
|
+
anomalies.push("robotic-timing");
|
|
923
|
+
confidence = Math.max(confidence, 0.8);
|
|
924
|
+
}
|
|
925
|
+
const triggered = anomalies.length > 0;
|
|
926
|
+
return this.createResult(triggered, {
|
|
927
|
+
anomalies,
|
|
928
|
+
movementCount: movements.length,
|
|
929
|
+
analysis
|
|
930
|
+
}, confidence);
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Analyze movement patterns for anomalies.
|
|
934
|
+
* @param {Array} movements - Array of movement points
|
|
935
|
+
* @returns {Object} Analysis results
|
|
936
|
+
*/
|
|
937
|
+
_analyzeMovements(movements) {
|
|
938
|
+
if (movements.length < 3) {
|
|
939
|
+
return {
|
|
940
|
+
teleportCount: 0,
|
|
941
|
+
linearPathRatio: 0,
|
|
942
|
+
velocityVariance: 0,
|
|
943
|
+
accelerationChanges: 0,
|
|
944
|
+
timingVariance: 0
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
let teleportCount = 0;
|
|
948
|
+
const velocities = [];
|
|
949
|
+
const angles = [];
|
|
950
|
+
const timeIntervals = [];
|
|
951
|
+
for (let i = 1; i < movements.length; i++) {
|
|
952
|
+
const prev = movements[i - 1];
|
|
953
|
+
const curr = movements[i];
|
|
954
|
+
const dx = curr.x - prev.x;
|
|
955
|
+
const dy = curr.y - prev.y;
|
|
956
|
+
const dt = curr.t - prev.t;
|
|
957
|
+
if (dt === 0) continue;
|
|
958
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
959
|
+
const velocity = distance / dt;
|
|
960
|
+
velocities.push(velocity);
|
|
961
|
+
angles.push(Math.atan2(dy, dx));
|
|
962
|
+
timeIntervals.push(dt);
|
|
963
|
+
if (distance > 300 && dt < 10) {
|
|
964
|
+
teleportCount++;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const avgVelocity = velocities.reduce((a, b) => a + b, 0) / velocities.length;
|
|
968
|
+
const velocityVariance = velocities.reduce((acc, v) => acc + Math.pow(v - avgVelocity, 2), 0) / velocities.length;
|
|
969
|
+
let angleConsistency = 0;
|
|
970
|
+
if (angles.length > 1) {
|
|
971
|
+
let consistentAngles = 0;
|
|
972
|
+
for (let i = 1; i < angles.length; i++) {
|
|
973
|
+
const angleDiff = Math.abs(angles[i] - angles[i - 1]);
|
|
974
|
+
if (angleDiff < 0.1) consistentAngles++;
|
|
975
|
+
}
|
|
976
|
+
angleConsistency = consistentAngles / (angles.length - 1);
|
|
977
|
+
}
|
|
978
|
+
const avgInterval = timeIntervals.reduce((a, b) => a + b, 0) / timeIntervals.length;
|
|
979
|
+
const timingVariance = timeIntervals.reduce((acc, t) => acc + Math.pow(t - avgInterval, 2), 0) / timeIntervals.length;
|
|
980
|
+
let accelerationChanges = 0;
|
|
981
|
+
for (let i = 1; i < velocities.length; i++) {
|
|
982
|
+
if ((velocities[i] - velocities[i - 1]) * (velocities[i - 1] - (velocities[i - 2] || 0)) < 0) {
|
|
983
|
+
accelerationChanges++;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
teleportCount,
|
|
988
|
+
linearPathRatio: angleConsistency,
|
|
989
|
+
velocityVariance,
|
|
990
|
+
accelerationChanges,
|
|
991
|
+
timingVariance
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
reset() {
|
|
995
|
+
super.reset();
|
|
996
|
+
this.stopTracking();
|
|
997
|
+
this._movements = [];
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
__publicField(MouseMovementSignal, "id", "mouse-movement");
|
|
1001
|
+
__publicField(MouseMovementSignal, "category", "behavior");
|
|
1002
|
+
__publicField(MouseMovementSignal, "weight", 0.9);
|
|
1003
|
+
__publicField(MouseMovementSignal, "description", "Detects non-human mouse movement patterns");
|
|
1004
|
+
__publicField(MouseMovementSignal, "requiresInteraction", true);
|
|
1005
|
+
|
|
1006
|
+
// src/signals/behavior/KeyboardPatternSignal.js
|
|
1007
|
+
var KeyboardPatternSignal = class extends Signal {
|
|
1008
|
+
constructor(options = {}) {
|
|
1009
|
+
super(options);
|
|
1010
|
+
this._keystrokes = [];
|
|
1011
|
+
this._isTracking = false;
|
|
1012
|
+
this._trackingDuration = options.trackingDuration || 5e3;
|
|
1013
|
+
this._minKeystrokes = options.minKeystrokes || 10;
|
|
1014
|
+
this._boundKeydownHandler = null;
|
|
1015
|
+
this._boundKeyupHandler = null;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Start tracking keyboard events.
|
|
1019
|
+
*/
|
|
1020
|
+
startTracking() {
|
|
1021
|
+
if (this._isTracking) return;
|
|
1022
|
+
this._keystrokes = [];
|
|
1023
|
+
this._isTracking = true;
|
|
1024
|
+
this._boundKeydownHandler = (e) => {
|
|
1025
|
+
this._keystrokes.push({
|
|
1026
|
+
type: "down",
|
|
1027
|
+
key: e.key,
|
|
1028
|
+
code: e.code,
|
|
1029
|
+
t: performance.now()
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
this._boundKeyupHandler = (e) => {
|
|
1033
|
+
this._keystrokes.push({
|
|
1034
|
+
type: "up",
|
|
1035
|
+
key: e.key,
|
|
1036
|
+
code: e.code,
|
|
1037
|
+
t: performance.now()
|
|
1038
|
+
});
|
|
1039
|
+
};
|
|
1040
|
+
document.addEventListener("keydown", this._boundKeydownHandler, { passive: true });
|
|
1041
|
+
document.addEventListener("keyup", this._boundKeyupHandler, { passive: true });
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Stop tracking keyboard events.
|
|
1045
|
+
*/
|
|
1046
|
+
stopTracking() {
|
|
1047
|
+
if (!this._isTracking) return;
|
|
1048
|
+
this._isTracking = false;
|
|
1049
|
+
if (this._boundKeydownHandler) {
|
|
1050
|
+
document.removeEventListener("keydown", this._boundKeydownHandler);
|
|
1051
|
+
this._boundKeydownHandler = null;
|
|
1052
|
+
}
|
|
1053
|
+
if (this._boundKeyupHandler) {
|
|
1054
|
+
document.removeEventListener("keyup", this._boundKeyupHandler);
|
|
1055
|
+
this._boundKeyupHandler = null;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async detect() {
|
|
1059
|
+
const anomalies = [];
|
|
1060
|
+
let confidence = 0;
|
|
1061
|
+
if (this._keystrokes.length === 0) {
|
|
1062
|
+
this.startTracking();
|
|
1063
|
+
await new Promise((resolve) => setTimeout(resolve, this._trackingDuration));
|
|
1064
|
+
this.stopTracking();
|
|
1065
|
+
}
|
|
1066
|
+
const keystrokes = this._keystrokes;
|
|
1067
|
+
const keydowns = keystrokes.filter((k) => k.type === "down");
|
|
1068
|
+
if (keydowns.length < this._minKeystrokes) {
|
|
1069
|
+
return this.createResult(false, {
|
|
1070
|
+
reason: "insufficient-data",
|
|
1071
|
+
keystrokes: keydowns.length
|
|
1072
|
+
}, 0);
|
|
1073
|
+
}
|
|
1074
|
+
const analysis = this._analyzeKeystrokes(keystrokes);
|
|
1075
|
+
if (analysis.avgInterKeystrokeTime < 50 && keydowns.length > 20) {
|
|
1076
|
+
anomalies.push("inhuman-speed");
|
|
1077
|
+
confidence = Math.max(confidence, 0.9);
|
|
1078
|
+
}
|
|
1079
|
+
if (analysis.timingVariance < 5 && keydowns.length > 15) {
|
|
1080
|
+
anomalies.push("robotic-timing");
|
|
1081
|
+
confidence = Math.max(confidence, 0.8);
|
|
1082
|
+
}
|
|
1083
|
+
if (analysis.missingKeyups > keydowns.length * 0.5) {
|
|
1084
|
+
anomalies.push("missing-keyups");
|
|
1085
|
+
confidence = Math.max(confidence, 0.7);
|
|
1086
|
+
}
|
|
1087
|
+
if (analysis.holdTimeVariance < 2 && analysis.holdTimes.length > 10) {
|
|
1088
|
+
anomalies.push("constant-hold-time");
|
|
1089
|
+
confidence = Math.max(confidence, 0.6);
|
|
1090
|
+
}
|
|
1091
|
+
if (analysis.sequentialKeys > keydowns.length * 0.8 && keydowns.length > 10) {
|
|
1092
|
+
anomalies.push("sequential-input");
|
|
1093
|
+
confidence = Math.max(confidence, 0.5);
|
|
1094
|
+
}
|
|
1095
|
+
if (analysis.rhythmScore < 0.1 && keydowns.length > 20) {
|
|
1096
|
+
anomalies.push("no-rhythm-variation");
|
|
1097
|
+
confidence = Math.max(confidence, 0.6);
|
|
1098
|
+
}
|
|
1099
|
+
const triggered = anomalies.length > 0;
|
|
1100
|
+
return this.createResult(triggered, {
|
|
1101
|
+
anomalies,
|
|
1102
|
+
keystrokeCount: keydowns.length,
|
|
1103
|
+
analysis
|
|
1104
|
+
}, confidence);
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Analyze keystroke patterns.
|
|
1108
|
+
* @param {Array} keystrokes - Array of keystroke events
|
|
1109
|
+
* @returns {Object} Analysis results
|
|
1110
|
+
*/
|
|
1111
|
+
_analyzeKeystrokes(keystrokes) {
|
|
1112
|
+
const keydowns = keystrokes.filter((k) => k.type === "down");
|
|
1113
|
+
const keyups = keystrokes.filter((k) => k.type === "up");
|
|
1114
|
+
if (keydowns.length < 2) {
|
|
1115
|
+
return {
|
|
1116
|
+
avgInterKeystrokeTime: Infinity,
|
|
1117
|
+
timingVariance: Infinity,
|
|
1118
|
+
missingKeyups: 0,
|
|
1119
|
+
holdTimeVariance: Infinity,
|
|
1120
|
+
holdTimes: [],
|
|
1121
|
+
sequentialKeys: 0,
|
|
1122
|
+
rhythmScore: 1
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
const interTimes = [];
|
|
1126
|
+
for (let i = 1; i < keydowns.length; i++) {
|
|
1127
|
+
interTimes.push(keydowns[i].t - keydowns[i - 1].t);
|
|
1128
|
+
}
|
|
1129
|
+
const avgInterKeystrokeTime = interTimes.reduce((a, b) => a + b, 0) / interTimes.length;
|
|
1130
|
+
const timingVariance = interTimes.reduce((acc, t) => acc + Math.pow(t - avgInterKeystrokeTime, 2), 0) / interTimes.length;
|
|
1131
|
+
const holdTimes = [];
|
|
1132
|
+
for (const down of keydowns) {
|
|
1133
|
+
const up = keyups.find((u) => u.key === down.key && u.t > down.t);
|
|
1134
|
+
if (up) {
|
|
1135
|
+
holdTimes.push(up.t - down.t);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const avgHoldTime = holdTimes.length > 0 ? holdTimes.reduce((a, b) => a + b, 0) / holdTimes.length : 0;
|
|
1139
|
+
const holdTimeVariance = holdTimes.length > 0 ? holdTimes.reduce((acc, t) => acc + Math.pow(t - avgHoldTime, 2), 0) / holdTimes.length : Infinity;
|
|
1140
|
+
const missingKeyups = keydowns.length - holdTimes.length;
|
|
1141
|
+
let sequentialKeys = 0;
|
|
1142
|
+
for (let i = 1; i < keydowns.length; i++) {
|
|
1143
|
+
const prevCode = keydowns[i - 1].key.charCodeAt(0);
|
|
1144
|
+
const currCode = keydowns[i].key.charCodeAt(0);
|
|
1145
|
+
if (Math.abs(currCode - prevCode) === 1) {
|
|
1146
|
+
sequentialKeys++;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
let rhythmScore = 0;
|
|
1150
|
+
if (interTimes.length > 5) {
|
|
1151
|
+
const sortedTimes = [...interTimes].sort((a, b) => a - b);
|
|
1152
|
+
const median = sortedTimes[Math.floor(sortedTimes.length / 2)];
|
|
1153
|
+
const deviations = interTimes.filter((t) => Math.abs(t - median) > median * 0.3).length;
|
|
1154
|
+
rhythmScore = deviations / interTimes.length;
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
avgInterKeystrokeTime,
|
|
1158
|
+
timingVariance,
|
|
1159
|
+
missingKeyups,
|
|
1160
|
+
holdTimeVariance,
|
|
1161
|
+
holdTimes,
|
|
1162
|
+
sequentialKeys,
|
|
1163
|
+
rhythmScore
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
reset() {
|
|
1167
|
+
super.reset();
|
|
1168
|
+
this.stopTracking();
|
|
1169
|
+
this._keystrokes = [];
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
__publicField(KeyboardPatternSignal, "id", "keyboard-pattern");
|
|
1173
|
+
__publicField(KeyboardPatternSignal, "category", "behavior");
|
|
1174
|
+
__publicField(KeyboardPatternSignal, "weight", 0.8);
|
|
1175
|
+
__publicField(KeyboardPatternSignal, "description", "Detects non-human keystroke patterns");
|
|
1176
|
+
__publicField(KeyboardPatternSignal, "requiresInteraction", true);
|
|
1177
|
+
|
|
1178
|
+
// src/signals/behavior/InteractionTimingSignal.js
|
|
1179
|
+
var InteractionTimingSignal = class extends Signal {
|
|
1180
|
+
constructor(options = {}) {
|
|
1181
|
+
super(options);
|
|
1182
|
+
this._pageLoadTime = performance.now();
|
|
1183
|
+
this._firstInteractionTime = null;
|
|
1184
|
+
this._interactions = [];
|
|
1185
|
+
this._isTracking = false;
|
|
1186
|
+
this._trackingDuration = options.trackingDuration || 5e3;
|
|
1187
|
+
this._boundHandler = null;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Start tracking interactions.
|
|
1191
|
+
*/
|
|
1192
|
+
startTracking() {
|
|
1193
|
+
if (this._isTracking) return;
|
|
1194
|
+
this._interactions = [];
|
|
1195
|
+
this._isTracking = true;
|
|
1196
|
+
const interactionEvents = ["click", "mousedown", "touchstart", "keydown", "scroll"];
|
|
1197
|
+
this._boundHandler = (e) => {
|
|
1198
|
+
const now = performance.now();
|
|
1199
|
+
if (this._firstInteractionTime === null) {
|
|
1200
|
+
this._firstInteractionTime = now;
|
|
1201
|
+
}
|
|
1202
|
+
this._interactions.push({
|
|
1203
|
+
type: e.type,
|
|
1204
|
+
t: now,
|
|
1205
|
+
timeSinceLoad: now - this._pageLoadTime
|
|
1206
|
+
});
|
|
1207
|
+
};
|
|
1208
|
+
for (const event of interactionEvents) {
|
|
1209
|
+
document.addEventListener(event, this._boundHandler, { passive: true, capture: true });
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Stop tracking interactions.
|
|
1214
|
+
*/
|
|
1215
|
+
stopTracking() {
|
|
1216
|
+
if (!this._isTracking) return;
|
|
1217
|
+
this._isTracking = false;
|
|
1218
|
+
const interactionEvents = ["click", "mousedown", "touchstart", "keydown", "scroll"];
|
|
1219
|
+
if (this._boundHandler) {
|
|
1220
|
+
for (const event of interactionEvents) {
|
|
1221
|
+
document.removeEventListener(event, this._boundHandler, { capture: true });
|
|
1222
|
+
}
|
|
1223
|
+
this._boundHandler = null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async detect() {
|
|
1227
|
+
const anomalies = [];
|
|
1228
|
+
let confidence = 0;
|
|
1229
|
+
if (!this._isTracking && this._interactions.length === 0) {
|
|
1230
|
+
this.startTracking();
|
|
1231
|
+
await new Promise((resolve) => setTimeout(resolve, this._trackingDuration));
|
|
1232
|
+
this.stopTracking();
|
|
1233
|
+
}
|
|
1234
|
+
const interactions = this._interactions;
|
|
1235
|
+
if (interactions.length === 0) {
|
|
1236
|
+
return this.createResult(false, {
|
|
1237
|
+
reason: "no-interactions"
|
|
1238
|
+
}, 0);
|
|
1239
|
+
}
|
|
1240
|
+
const firstInteraction = interactions[0];
|
|
1241
|
+
if (firstInteraction.timeSinceLoad < 100) {
|
|
1242
|
+
anomalies.push("instant-interaction");
|
|
1243
|
+
confidence = Math.max(confidence, 0.9);
|
|
1244
|
+
} else if (firstInteraction.timeSinceLoad < 300) {
|
|
1245
|
+
anomalies.push("very-fast-interaction");
|
|
1246
|
+
confidence = Math.max(confidence, 0.6);
|
|
1247
|
+
}
|
|
1248
|
+
if (interactions.length > 3) {
|
|
1249
|
+
const intervals = [];
|
|
1250
|
+
for (let i = 1; i < interactions.length; i++) {
|
|
1251
|
+
intervals.push(interactions[i].t - interactions[i - 1].t);
|
|
1252
|
+
}
|
|
1253
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1254
|
+
const variance = intervals.reduce((acc, t) => acc + Math.pow(t - avgInterval, 2), 0) / intervals.length;
|
|
1255
|
+
if (variance < 10 && interactions.length > 5) {
|
|
1256
|
+
anomalies.push("robotic-intervals");
|
|
1257
|
+
confidence = Math.max(confidence, 0.8);
|
|
1258
|
+
}
|
|
1259
|
+
const burstThreshold = 50;
|
|
1260
|
+
let burstCount = 0;
|
|
1261
|
+
for (const interval of intervals) {
|
|
1262
|
+
if (interval < burstThreshold) burstCount++;
|
|
1263
|
+
}
|
|
1264
|
+
if (burstCount > intervals.length * 0.7) {
|
|
1265
|
+
anomalies.push("burst-interactions");
|
|
1266
|
+
confidence = Math.max(confidence, 0.7);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const typeSequence = interactions.map((i) => i.type).join(",");
|
|
1270
|
+
if (interactions.length >= 6) {
|
|
1271
|
+
const halfLength = Math.floor(interactions.length / 2);
|
|
1272
|
+
const firstHalf = interactions.slice(0, halfLength).map((i) => i.type).join(",");
|
|
1273
|
+
const secondHalf = interactions.slice(halfLength, halfLength * 2).map((i) => i.type).join(",");
|
|
1274
|
+
if (firstHalf === secondHalf && firstHalf.length > 0) {
|
|
1275
|
+
anomalies.push("repeated-sequence");
|
|
1276
|
+
confidence = Math.max(confidence, 0.6);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const triggered = anomalies.length > 0;
|
|
1280
|
+
return this.createResult(triggered, {
|
|
1281
|
+
anomalies,
|
|
1282
|
+
interactionCount: interactions.length,
|
|
1283
|
+
timeToFirstInteraction: firstInteraction.timeSinceLoad,
|
|
1284
|
+
firstInteractionType: firstInteraction.type
|
|
1285
|
+
}, confidence);
|
|
1286
|
+
}
|
|
1287
|
+
reset() {
|
|
1288
|
+
super.reset();
|
|
1289
|
+
this.stopTracking();
|
|
1290
|
+
this._pageLoadTime = performance.now();
|
|
1291
|
+
this._firstInteractionTime = null;
|
|
1292
|
+
this._interactions = [];
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
__publicField(InteractionTimingSignal, "id", "interaction-timing");
|
|
1296
|
+
__publicField(InteractionTimingSignal, "category", "behavior");
|
|
1297
|
+
__publicField(InteractionTimingSignal, "weight", 0.6);
|
|
1298
|
+
__publicField(InteractionTimingSignal, "description", "Detects suspicious interaction timing");
|
|
1299
|
+
__publicField(InteractionTimingSignal, "requiresInteraction", true);
|
|
1300
|
+
|
|
1301
|
+
// src/signals/behavior/ScrollBehaviorSignal.js
|
|
1302
|
+
var ScrollBehaviorSignal = class extends Signal {
|
|
1303
|
+
constructor(options = {}) {
|
|
1304
|
+
super(options);
|
|
1305
|
+
this._scrollEvents = [];
|
|
1306
|
+
this._isTracking = false;
|
|
1307
|
+
this._trackingDuration = options.trackingDuration || 3e3;
|
|
1308
|
+
this._boundHandler = null;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Start tracking scroll events.
|
|
1312
|
+
*/
|
|
1313
|
+
startTracking() {
|
|
1314
|
+
if (this._isTracking) return;
|
|
1315
|
+
this._scrollEvents = [];
|
|
1316
|
+
this._isTracking = true;
|
|
1317
|
+
this._boundHandler = () => {
|
|
1318
|
+
this._scrollEvents.push({
|
|
1319
|
+
scrollY: window.scrollY,
|
|
1320
|
+
scrollX: window.scrollX,
|
|
1321
|
+
t: performance.now()
|
|
1322
|
+
});
|
|
1323
|
+
};
|
|
1324
|
+
window.addEventListener("scroll", this._boundHandler, { passive: true });
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Stop tracking scroll events.
|
|
1328
|
+
*/
|
|
1329
|
+
stopTracking() {
|
|
1330
|
+
if (!this._isTracking) return;
|
|
1331
|
+
this._isTracking = false;
|
|
1332
|
+
if (this._boundHandler) {
|
|
1333
|
+
window.removeEventListener("scroll", this._boundHandler);
|
|
1334
|
+
this._boundHandler = null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async detect() {
|
|
1338
|
+
const anomalies = [];
|
|
1339
|
+
let confidence = 0;
|
|
1340
|
+
if (this._scrollEvents.length === 0) {
|
|
1341
|
+
this.startTracking();
|
|
1342
|
+
await new Promise((resolve) => setTimeout(resolve, this._trackingDuration));
|
|
1343
|
+
this.stopTracking();
|
|
1344
|
+
}
|
|
1345
|
+
const events = this._scrollEvents;
|
|
1346
|
+
if (events.length < 3) {
|
|
1347
|
+
return this.createResult(false, {
|
|
1348
|
+
reason: "insufficient-scroll-data",
|
|
1349
|
+
scrollEvents: events.length
|
|
1350
|
+
}, 0);
|
|
1351
|
+
}
|
|
1352
|
+
const analysis = this._analyzeScrollPatterns(events);
|
|
1353
|
+
if (analysis.instantJumps > 0) {
|
|
1354
|
+
anomalies.push("instant-scroll-jumps");
|
|
1355
|
+
confidence = Math.max(confidence, 0.7);
|
|
1356
|
+
}
|
|
1357
|
+
if (analysis.velocityVariance < 0.1 && events.length > 10) {
|
|
1358
|
+
anomalies.push("constant-scroll-velocity");
|
|
1359
|
+
confidence = Math.max(confidence, 0.6);
|
|
1360
|
+
}
|
|
1361
|
+
if (analysis.momentumEvents === 0 && events.length > 5) {
|
|
1362
|
+
anomalies.push("no-scroll-momentum");
|
|
1363
|
+
confidence = Math.max(confidence, 0.5);
|
|
1364
|
+
}
|
|
1365
|
+
if (analysis.intervalVariance < 5 && events.length > 10) {
|
|
1366
|
+
anomalies.push("robotic-scroll-timing");
|
|
1367
|
+
confidence = Math.max(confidence, 0.7);
|
|
1368
|
+
}
|
|
1369
|
+
if (analysis.scrollDirections === 1 && Math.abs(analysis.totalScrollY) > 1e3) {
|
|
1370
|
+
if (analysis.velocityVariance < 1) {
|
|
1371
|
+
anomalies.push("one-dimensional-scroll");
|
|
1372
|
+
confidence = Math.max(confidence, 0.4);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (analysis.exactPositionScrolls > 2) {
|
|
1376
|
+
anomalies.push("exact-position-scrolls");
|
|
1377
|
+
confidence = Math.max(confidence, 0.6);
|
|
1378
|
+
}
|
|
1379
|
+
const triggered = anomalies.length > 0;
|
|
1380
|
+
return this.createResult(triggered, {
|
|
1381
|
+
anomalies,
|
|
1382
|
+
scrollEventCount: events.length,
|
|
1383
|
+
analysis
|
|
1384
|
+
}, confidence);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Analyze scroll patterns for anomalies.
|
|
1388
|
+
* @param {Array} events - Array of scroll events
|
|
1389
|
+
* @returns {Object} Analysis results
|
|
1390
|
+
*/
|
|
1391
|
+
_analyzeScrollPatterns(events) {
|
|
1392
|
+
if (events.length < 2) {
|
|
1393
|
+
return {
|
|
1394
|
+
instantJumps: 0,
|
|
1395
|
+
velocityVariance: 0,
|
|
1396
|
+
momentumEvents: 0,
|
|
1397
|
+
intervalVariance: 0,
|
|
1398
|
+
scrollDirections: 0,
|
|
1399
|
+
totalScrollY: 0,
|
|
1400
|
+
exactPositionScrolls: 0
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
let instantJumps = 0;
|
|
1404
|
+
let momentumEvents = 0;
|
|
1405
|
+
const velocities = [];
|
|
1406
|
+
const intervals = [];
|
|
1407
|
+
let hasVertical = false;
|
|
1408
|
+
let hasHorizontal = false;
|
|
1409
|
+
let exactPositionScrolls = 0;
|
|
1410
|
+
const commonPositions = [0, 100, 200, 300, 400, 500, 600, 800, 1e3];
|
|
1411
|
+
for (let i = 1; i < events.length; i++) {
|
|
1412
|
+
const prev = events[i - 1];
|
|
1413
|
+
const curr = events[i];
|
|
1414
|
+
const dy = curr.scrollY - prev.scrollY;
|
|
1415
|
+
const dx = curr.scrollX - prev.scrollX;
|
|
1416
|
+
const dt = curr.t - prev.t;
|
|
1417
|
+
intervals.push(dt);
|
|
1418
|
+
if (Math.abs(dy) > 0) hasVertical = true;
|
|
1419
|
+
if (Math.abs(dx) > 0) hasHorizontal = true;
|
|
1420
|
+
if (dt === 0) continue;
|
|
1421
|
+
const velocity = Math.sqrt(dy * dy + dx * dx) / dt;
|
|
1422
|
+
velocities.push(velocity);
|
|
1423
|
+
const distance = Math.abs(dy) + Math.abs(dx);
|
|
1424
|
+
if (distance > 200 && dt < 20) {
|
|
1425
|
+
instantJumps++;
|
|
1426
|
+
}
|
|
1427
|
+
if (i > 1 && velocities.length > 1) {
|
|
1428
|
+
const prevVelocity = velocities[velocities.length - 2];
|
|
1429
|
+
if (velocity < prevVelocity * 0.9 && velocity > 0) {
|
|
1430
|
+
momentumEvents++;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (commonPositions.includes(Math.round(curr.scrollY))) {
|
|
1434
|
+
exactPositionScrolls++;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const avgVelocity = velocities.length > 0 ? velocities.reduce((a, b) => a + b, 0) / velocities.length : 0;
|
|
1438
|
+
const velocityVariance = velocities.length > 0 ? velocities.reduce((acc, v) => acc + Math.pow(v - avgVelocity, 2), 0) / velocities.length : 0;
|
|
1439
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1440
|
+
const intervalVariance = intervals.reduce((acc, t) => acc + Math.pow(t - avgInterval, 2), 0) / intervals.length;
|
|
1441
|
+
let scrollDirections = 0;
|
|
1442
|
+
if (hasVertical) scrollDirections++;
|
|
1443
|
+
if (hasHorizontal) scrollDirections++;
|
|
1444
|
+
const totalScrollY = events[events.length - 1].scrollY - events[0].scrollY;
|
|
1445
|
+
return {
|
|
1446
|
+
instantJumps,
|
|
1447
|
+
velocityVariance,
|
|
1448
|
+
momentumEvents,
|
|
1449
|
+
intervalVariance,
|
|
1450
|
+
scrollDirections,
|
|
1451
|
+
totalScrollY,
|
|
1452
|
+
exactPositionScrolls
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
reset() {
|
|
1456
|
+
super.reset();
|
|
1457
|
+
this.stopTracking();
|
|
1458
|
+
this._scrollEvents = [];
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
__publicField(ScrollBehaviorSignal, "id", "scroll-behavior");
|
|
1462
|
+
__publicField(ScrollBehaviorSignal, "category", "behavior");
|
|
1463
|
+
__publicField(ScrollBehaviorSignal, "weight", 0.5);
|
|
1464
|
+
__publicField(ScrollBehaviorSignal, "description", "Detects programmatic scroll patterns");
|
|
1465
|
+
__publicField(ScrollBehaviorSignal, "requiresInteraction", true);
|
|
1466
|
+
|
|
1467
|
+
// src/signals/fingerprint/PluginsSignal.js
|
|
1468
|
+
var PluginsSignal = class extends Signal {
|
|
1469
|
+
async detect() {
|
|
1470
|
+
const anomalies = [];
|
|
1471
|
+
let confidence = 0;
|
|
1472
|
+
const plugins = navigator.plugins;
|
|
1473
|
+
const mimeTypes = navigator.mimeTypes;
|
|
1474
|
+
if (!plugins) {
|
|
1475
|
+
anomalies.push("no-plugins-object");
|
|
1476
|
+
confidence = Math.max(confidence, 0.6);
|
|
1477
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
1478
|
+
}
|
|
1479
|
+
if (plugins.length === 0) {
|
|
1480
|
+
anomalies.push("empty-plugins");
|
|
1481
|
+
confidence = Math.max(confidence, 0.5);
|
|
1482
|
+
}
|
|
1483
|
+
const ua = navigator.userAgent || "";
|
|
1484
|
+
if (ua.includes("Chrome") && !ua.includes("Chromium")) {
|
|
1485
|
+
const hasChromePdf = Array.from(plugins).some((p) => p.name.includes("PDF") || p.name.includes("Chromium PDF"));
|
|
1486
|
+
if (!hasChromePdf && plugins.length === 0) {
|
|
1487
|
+
anomalies.push("chrome-missing-pdf-plugin");
|
|
1488
|
+
confidence = Math.max(confidence, 0.4);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (plugins.length > 0 && mimeTypes) {
|
|
1492
|
+
let totalMimeTypes = 0;
|
|
1493
|
+
for (let i = 0; i < plugins.length; i++) {
|
|
1494
|
+
totalMimeTypes += plugins[i].length || 0;
|
|
1495
|
+
}
|
|
1496
|
+
if (mimeTypes.length === 0 && totalMimeTypes > 0) {
|
|
1497
|
+
anomalies.push("mimetypes-mismatch");
|
|
1498
|
+
confidence = Math.max(confidence, 0.5);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (plugins.length > 1) {
|
|
1502
|
+
const names = Array.from(plugins).map((p) => p.name);
|
|
1503
|
+
const uniqueNames = new Set(names);
|
|
1504
|
+
if (uniqueNames.size < names.length) {
|
|
1505
|
+
anomalies.push("duplicate-plugins");
|
|
1506
|
+
confidence = Math.max(confidence, 0.6);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
try {
|
|
1510
|
+
const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "plugins");
|
|
1511
|
+
if (desc && desc.get) {
|
|
1512
|
+
const nativeToString = desc.get.toString();
|
|
1513
|
+
if (!nativeToString.includes("[native code]")) {
|
|
1514
|
+
anomalies.push("plugins-getter-overridden");
|
|
1515
|
+
confidence = Math.max(confidence, 0.7);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
}
|
|
1520
|
+
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
|
|
1521
|
+
if (!isMobile && plugins.length === 1) {
|
|
1522
|
+
anomalies.push("minimal-plugins");
|
|
1523
|
+
confidence = Math.max(confidence, 0.3);
|
|
1524
|
+
}
|
|
1525
|
+
const triggered = anomalies.length > 0;
|
|
1526
|
+
return this.createResult(triggered, {
|
|
1527
|
+
anomalies,
|
|
1528
|
+
pluginCount: plugins.length,
|
|
1529
|
+
mimeTypeCount: (mimeTypes == null ? void 0 : mimeTypes.length) || 0
|
|
1530
|
+
}, confidence);
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
__publicField(PluginsSignal, "id", "plugins");
|
|
1534
|
+
__publicField(PluginsSignal, "category", "fingerprint");
|
|
1535
|
+
__publicField(PluginsSignal, "weight", 0.6);
|
|
1536
|
+
__publicField(PluginsSignal, "description", "Detects browser plugin anomalies");
|
|
1537
|
+
|
|
1538
|
+
// src/signals/fingerprint/WebGLSignal.js
|
|
1539
|
+
var WebGLSignal = class extends Signal {
|
|
1540
|
+
async detect() {
|
|
1541
|
+
const anomalies = [];
|
|
1542
|
+
let confidence = 0;
|
|
1543
|
+
const canvas = document.createElement("canvas");
|
|
1544
|
+
let gl = null;
|
|
1545
|
+
try {
|
|
1546
|
+
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1547
|
+
} catch (e) {
|
|
1548
|
+
anomalies.push("webgl-error");
|
|
1549
|
+
confidence = Math.max(confidence, 0.5);
|
|
1550
|
+
}
|
|
1551
|
+
if (!gl) {
|
|
1552
|
+
anomalies.push("webgl-unavailable");
|
|
1553
|
+
confidence = Math.max(confidence, 0.4);
|
|
1554
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
1555
|
+
}
|
|
1556
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1557
|
+
let vendor = "";
|
|
1558
|
+
let renderer = "";
|
|
1559
|
+
if (debugInfo) {
|
|
1560
|
+
vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "";
|
|
1561
|
+
renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "";
|
|
1562
|
+
}
|
|
1563
|
+
if (!vendor && !renderer) {
|
|
1564
|
+
anomalies.push("no-webgl-renderer-info");
|
|
1565
|
+
confidence = Math.max(confidence, 0.6);
|
|
1566
|
+
}
|
|
1567
|
+
const suspiciousRenderers = [
|
|
1568
|
+
"swiftshader",
|
|
1569
|
+
"llvmpipe",
|
|
1570
|
+
"software",
|
|
1571
|
+
"mesa",
|
|
1572
|
+
"google swiftshader",
|
|
1573
|
+
"vmware",
|
|
1574
|
+
"virtualbox"
|
|
1575
|
+
];
|
|
1576
|
+
const rendererLower = renderer.toLowerCase();
|
|
1577
|
+
for (const sus of suspiciousRenderers) {
|
|
1578
|
+
if (rendererLower.includes(sus)) {
|
|
1579
|
+
anomalies.push(`suspicious-renderer-${sus.replace(/\s+/g, "-")}`);
|
|
1580
|
+
confidence = Math.max(confidence, 0.7);
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (vendor && renderer) {
|
|
1585
|
+
if (rendererLower.includes("nvidia") && !vendor.toLowerCase().includes("nvidia")) {
|
|
1586
|
+
anomalies.push("vendor-renderer-mismatch");
|
|
1587
|
+
confidence = Math.max(confidence, 0.6);
|
|
1588
|
+
}
|
|
1589
|
+
if ((rendererLower.includes("amd") || rendererLower.includes("radeon")) && !vendor.toLowerCase().includes("amd") && !vendor.toLowerCase().includes("ati")) {
|
|
1590
|
+
anomalies.push("vendor-renderer-mismatch");
|
|
1591
|
+
confidence = Math.max(confidence, 0.6);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const extensions = gl.getSupportedExtensions() || [];
|
|
1595
|
+
if (extensions.length < 5) {
|
|
1596
|
+
anomalies.push("few-webgl-extensions");
|
|
1597
|
+
confidence = Math.max(confidence, 0.4);
|
|
1598
|
+
}
|
|
1599
|
+
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
1600
|
+
const maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
|
|
1601
|
+
if (maxTextureSize < 1024 || maxTextureSize > 65536) {
|
|
1602
|
+
anomalies.push("unrealistic-max-texture");
|
|
1603
|
+
confidence = Math.max(confidence, 0.5);
|
|
1604
|
+
}
|
|
1605
|
+
try {
|
|
1606
|
+
gl.clearColor(0, 0, 0, 1);
|
|
1607
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1608
|
+
const pixels = new Uint8Array(4);
|
|
1609
|
+
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
1610
|
+
if (pixels[3] !== 255) {
|
|
1611
|
+
anomalies.push("webgl-render-failure");
|
|
1612
|
+
confidence = Math.max(confidence, 0.6);
|
|
1613
|
+
}
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
anomalies.push("webgl-render-error");
|
|
1616
|
+
confidence = Math.max(confidence, 0.5);
|
|
1617
|
+
}
|
|
1618
|
+
const loseContext = gl.getExtension("WEBGL_lose_context");
|
|
1619
|
+
if (loseContext) {
|
|
1620
|
+
loseContext.loseContext();
|
|
1621
|
+
}
|
|
1622
|
+
const triggered = anomalies.length > 0;
|
|
1623
|
+
return this.createResult(triggered, {
|
|
1624
|
+
anomalies,
|
|
1625
|
+
vendor,
|
|
1626
|
+
renderer,
|
|
1627
|
+
extensionCount: extensions.length,
|
|
1628
|
+
maxTextureSize
|
|
1629
|
+
}, confidence);
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
__publicField(WebGLSignal, "id", "webgl");
|
|
1633
|
+
__publicField(WebGLSignal, "category", "fingerprint");
|
|
1634
|
+
__publicField(WebGLSignal, "weight", 0.7);
|
|
1635
|
+
__publicField(WebGLSignal, "description", "Detects WebGL rendering anomalies");
|
|
1636
|
+
|
|
1637
|
+
// src/signals/fingerprint/CanvasSignal.js
|
|
1638
|
+
var CanvasSignal = class extends Signal {
|
|
1639
|
+
async detect() {
|
|
1640
|
+
const anomalies = [];
|
|
1641
|
+
let confidence = 0;
|
|
1642
|
+
try {
|
|
1643
|
+
const canvas = document.createElement("canvas");
|
|
1644
|
+
canvas.width = 200;
|
|
1645
|
+
canvas.height = 50;
|
|
1646
|
+
const ctx = canvas.getContext("2d");
|
|
1647
|
+
if (!ctx) {
|
|
1648
|
+
anomalies.push("canvas-context-unavailable");
|
|
1649
|
+
confidence = Math.max(confidence, 0.5);
|
|
1650
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
1651
|
+
}
|
|
1652
|
+
ctx.textBaseline = "alphabetic";
|
|
1653
|
+
ctx.font = "14px Arial";
|
|
1654
|
+
ctx.fillStyle = "#f60";
|
|
1655
|
+
ctx.fillRect(0, 0, 200, 50);
|
|
1656
|
+
ctx.fillStyle = "#069";
|
|
1657
|
+
ctx.fillText("Bot Detection Test \u{1F916}", 2, 15);
|
|
1658
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
1659
|
+
ctx.fillText("Canvas Fingerprint", 4, 30);
|
|
1660
|
+
ctx.beginPath();
|
|
1661
|
+
ctx.arc(100, 25, 10, 0, Math.PI * 2, true);
|
|
1662
|
+
ctx.closePath();
|
|
1663
|
+
ctx.fill();
|
|
1664
|
+
const dataUrl1 = canvas.toDataURL();
|
|
1665
|
+
ctx.clearRect(0, 0, 200, 50);
|
|
1666
|
+
ctx.fillStyle = "#f60";
|
|
1667
|
+
ctx.fillRect(0, 0, 200, 50);
|
|
1668
|
+
ctx.fillStyle = "#069";
|
|
1669
|
+
ctx.fillText("Bot Detection Test \u{1F916}", 2, 15);
|
|
1670
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
1671
|
+
ctx.fillText("Canvas Fingerprint", 4, 30);
|
|
1672
|
+
ctx.beginPath();
|
|
1673
|
+
ctx.arc(100, 25, 10, 0, Math.PI * 2, true);
|
|
1674
|
+
ctx.closePath();
|
|
1675
|
+
ctx.fill();
|
|
1676
|
+
const dataUrl2 = canvas.toDataURL();
|
|
1677
|
+
if (dataUrl1 !== dataUrl2) {
|
|
1678
|
+
anomalies.push("canvas-randomized");
|
|
1679
|
+
confidence = Math.max(confidence, 0.6);
|
|
1680
|
+
}
|
|
1681
|
+
if (dataUrl1.length < 1e3) {
|
|
1682
|
+
anomalies.push("canvas-possibly-blank");
|
|
1683
|
+
confidence = Math.max(confidence, 0.4);
|
|
1684
|
+
}
|
|
1685
|
+
const blankCanvas = document.createElement("canvas");
|
|
1686
|
+
blankCanvas.width = 200;
|
|
1687
|
+
blankCanvas.height = 50;
|
|
1688
|
+
const blankUrl = blankCanvas.toDataURL();
|
|
1689
|
+
if (dataUrl1 === blankUrl) {
|
|
1690
|
+
anomalies.push("canvas-rendering-blocked");
|
|
1691
|
+
confidence = Math.max(confidence, 0.7);
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
const toDataURLStr = canvas.toDataURL.toString();
|
|
1695
|
+
if (!toDataURLStr.includes("[native code]")) {
|
|
1696
|
+
anomalies.push("toDataURL-overridden");
|
|
1697
|
+
confidence = Math.max(confidence, 0.8);
|
|
1698
|
+
}
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
}
|
|
1701
|
+
const imageData = ctx.getImageData(0, 0, 200, 50);
|
|
1702
|
+
const pixels = imageData.data;
|
|
1703
|
+
let allSame = true;
|
|
1704
|
+
const firstPixel = [pixels[0], pixels[1], pixels[2], pixels[3]];
|
|
1705
|
+
for (let i = 4; i < pixels.length; i += 4) {
|
|
1706
|
+
if (pixels[i] !== firstPixel[0] || pixels[i + 1] !== firstPixel[1] || pixels[i + 2] !== firstPixel[2]) {
|
|
1707
|
+
allSame = false;
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (allSame) {
|
|
1712
|
+
anomalies.push("uniform-pixel-data");
|
|
1713
|
+
confidence = Math.max(confidence, 0.6);
|
|
1714
|
+
}
|
|
1715
|
+
} catch (e) {
|
|
1716
|
+
anomalies.push("canvas-error");
|
|
1717
|
+
confidence = Math.max(confidence, 0.4);
|
|
1718
|
+
}
|
|
1719
|
+
const triggered = anomalies.length > 0;
|
|
1720
|
+
return this.createResult(triggered, { anomalies }, confidence);
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
__publicField(CanvasSignal, "id", "canvas");
|
|
1724
|
+
__publicField(CanvasSignal, "category", "fingerprint");
|
|
1725
|
+
__publicField(CanvasSignal, "weight", 0.5);
|
|
1726
|
+
__publicField(CanvasSignal, "description", "Detects canvas fingerprint anomalies");
|
|
1727
|
+
|
|
1728
|
+
// src/signals/fingerprint/AudioContextSignal.js
|
|
1729
|
+
var AudioContextSignal = class extends Signal {
|
|
1730
|
+
async detect() {
|
|
1731
|
+
const anomalies = [];
|
|
1732
|
+
let confidence = 0;
|
|
1733
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
1734
|
+
if (!AudioContext) {
|
|
1735
|
+
anomalies.push("audio-context-unavailable");
|
|
1736
|
+
confidence = Math.max(confidence, 0.4);
|
|
1737
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
1738
|
+
}
|
|
1739
|
+
let audioContext = null;
|
|
1740
|
+
let oscillator = null;
|
|
1741
|
+
let analyser = null;
|
|
1742
|
+
try {
|
|
1743
|
+
audioContext = new AudioContext();
|
|
1744
|
+
const sampleRate = audioContext.sampleRate;
|
|
1745
|
+
if (sampleRate !== 44100 && sampleRate !== 48e3 && sampleRate !== 96e3) {
|
|
1746
|
+
anomalies.push("unusual-sample-rate");
|
|
1747
|
+
confidence = Math.max(confidence, 0.3);
|
|
1748
|
+
}
|
|
1749
|
+
oscillator = audioContext.createOscillator();
|
|
1750
|
+
analyser = audioContext.createAnalyser();
|
|
1751
|
+
if (!oscillator || !analyser) {
|
|
1752
|
+
anomalies.push("audio-nodes-unavailable");
|
|
1753
|
+
confidence = Math.max(confidence, 0.5);
|
|
1754
|
+
} else {
|
|
1755
|
+
const fftSize = analyser.fftSize;
|
|
1756
|
+
if (fftSize !== 2048) {
|
|
1757
|
+
}
|
|
1758
|
+
const destination = audioContext.destination;
|
|
1759
|
+
if (!destination || destination.maxChannelCount === 0) {
|
|
1760
|
+
anomalies.push("no-audio-destination");
|
|
1761
|
+
confidence = Math.max(confidence, 0.6);
|
|
1762
|
+
}
|
|
1763
|
+
if (destination && destination.maxChannelCount < 2) {
|
|
1764
|
+
anomalies.push("mono-audio-only");
|
|
1765
|
+
confidence = Math.max(confidence, 0.3);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
try {
|
|
1769
|
+
const audioCtxStr = AudioContext.toString();
|
|
1770
|
+
if (!audioCtxStr.includes("[native code]")) {
|
|
1771
|
+
anomalies.push("audio-context-overridden");
|
|
1772
|
+
confidence = Math.max(confidence, 0.7);
|
|
1773
|
+
}
|
|
1774
|
+
} catch (e) {
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
if (audioContext.state === "suspended") {
|
|
1778
|
+
await audioContext.resume().catch(() => {
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
if (audioContext.state === "running") {
|
|
1782
|
+
const oscillatorNode = audioContext.createOscillator();
|
|
1783
|
+
const gainNode = audioContext.createGain();
|
|
1784
|
+
const scriptProcessor = audioContext.createScriptProcessor ? audioContext.createScriptProcessor(4096, 1, 1) : null;
|
|
1785
|
+
if (scriptProcessor) {
|
|
1786
|
+
oscillatorNode.type = "triangle";
|
|
1787
|
+
oscillatorNode.frequency.value = 1e4;
|
|
1788
|
+
gainNode.gain.value = 0;
|
|
1789
|
+
oscillatorNode.connect(gainNode);
|
|
1790
|
+
gainNode.connect(scriptProcessor);
|
|
1791
|
+
scriptProcessor.connect(audioContext.destination);
|
|
1792
|
+
oscillatorNode.start(0);
|
|
1793
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1794
|
+
oscillatorNode.stop();
|
|
1795
|
+
oscillatorNode.disconnect();
|
|
1796
|
+
gainNode.disconnect();
|
|
1797
|
+
scriptProcessor.disconnect();
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} catch (e) {
|
|
1801
|
+
anomalies.push("audio-fingerprint-blocked");
|
|
1802
|
+
confidence = Math.max(confidence, 0.4);
|
|
1803
|
+
}
|
|
1804
|
+
const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
1805
|
+
if (!OfflineAudioContext) {
|
|
1806
|
+
anomalies.push("offline-audio-context-unavailable");
|
|
1807
|
+
confidence = Math.max(confidence, 0.3);
|
|
1808
|
+
}
|
|
1809
|
+
} catch (e) {
|
|
1810
|
+
anomalies.push("audio-context-error");
|
|
1811
|
+
confidence = Math.max(confidence, 0.4);
|
|
1812
|
+
} finally {
|
|
1813
|
+
if (oscillator) {
|
|
1814
|
+
try {
|
|
1815
|
+
oscillator.disconnect();
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (analyser) {
|
|
1820
|
+
try {
|
|
1821
|
+
analyser.disconnect();
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (audioContext) {
|
|
1826
|
+
try {
|
|
1827
|
+
audioContext.close();
|
|
1828
|
+
} catch (e) {
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const triggered = anomalies.length > 0;
|
|
1833
|
+
return this.createResult(triggered, { anomalies }, confidence);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
__publicField(AudioContextSignal, "id", "audio-context");
|
|
1837
|
+
__publicField(AudioContextSignal, "category", "fingerprint");
|
|
1838
|
+
__publicField(AudioContextSignal, "weight", 0.5);
|
|
1839
|
+
__publicField(AudioContextSignal, "description", "Detects AudioContext anomalies");
|
|
1840
|
+
|
|
1841
|
+
// src/signals/fingerprint/ScreenSignal.js
|
|
1842
|
+
var ScreenSignal = class extends Signal {
|
|
1843
|
+
async detect() {
|
|
1844
|
+
const anomalies = [];
|
|
1845
|
+
let confidence = 0;
|
|
1846
|
+
const screen = window.screen;
|
|
1847
|
+
if (!screen) {
|
|
1848
|
+
anomalies.push("no-screen-object");
|
|
1849
|
+
confidence = Math.max(confidence, 0.6);
|
|
1850
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
1851
|
+
}
|
|
1852
|
+
const width = screen.width;
|
|
1853
|
+
const height = screen.height;
|
|
1854
|
+
const availWidth = screen.availWidth;
|
|
1855
|
+
const availHeight = screen.availHeight;
|
|
1856
|
+
const colorDepth = screen.colorDepth;
|
|
1857
|
+
const pixelDepth = screen.pixelDepth;
|
|
1858
|
+
const outerWidth = window.outerWidth;
|
|
1859
|
+
const outerHeight = window.outerHeight;
|
|
1860
|
+
const innerWidth = window.innerWidth;
|
|
1861
|
+
const innerHeight = window.innerHeight;
|
|
1862
|
+
if (outerWidth === 0 || outerHeight === 0) {
|
|
1863
|
+
anomalies.push("zero-outer-dimensions");
|
|
1864
|
+
confidence = Math.max(confidence, 0.8);
|
|
1865
|
+
}
|
|
1866
|
+
if (innerWidth === 0 || innerHeight === 0) {
|
|
1867
|
+
anomalies.push("zero-inner-dimensions");
|
|
1868
|
+
confidence = Math.max(confidence, 0.7);
|
|
1869
|
+
}
|
|
1870
|
+
const ua = navigator.userAgent || "";
|
|
1871
|
+
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
|
|
1872
|
+
if (!isMobile && (width < 640 || height < 480)) {
|
|
1873
|
+
anomalies.push("very-small-screen");
|
|
1874
|
+
confidence = Math.max(confidence, 0.5);
|
|
1875
|
+
}
|
|
1876
|
+
if (width > 7680 || height > 4320) {
|
|
1877
|
+
anomalies.push("unrealistic-screen-size");
|
|
1878
|
+
confidence = Math.max(confidence, 0.4);
|
|
1879
|
+
}
|
|
1880
|
+
const headlessDefaults = [
|
|
1881
|
+
{ w: 800, h: 600 },
|
|
1882
|
+
{ w: 1024, h: 768 },
|
|
1883
|
+
{ w: 1920, h: 1080 }
|
|
1884
|
+
];
|
|
1885
|
+
for (const def of headlessDefaults) {
|
|
1886
|
+
if (width === def.w && height === def.h && outerWidth === def.w && outerHeight === def.h) {
|
|
1887
|
+
anomalies.push("headless-default-dimensions");
|
|
1888
|
+
confidence = Math.max(confidence, 0.5);
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
if (availWidth > width || availHeight > height) {
|
|
1893
|
+
anomalies.push("available-exceeds-total");
|
|
1894
|
+
confidence = Math.max(confidence, 0.7);
|
|
1895
|
+
}
|
|
1896
|
+
if (outerWidth > width || outerHeight > height) {
|
|
1897
|
+
anomalies.push("window-exceeds-screen");
|
|
1898
|
+
confidence = Math.max(confidence, 0.6);
|
|
1899
|
+
}
|
|
1900
|
+
if (colorDepth !== 24 && colorDepth !== 32 && colorDepth !== 30 && colorDepth !== 48) {
|
|
1901
|
+
anomalies.push("unusual-color-depth");
|
|
1902
|
+
confidence = Math.max(confidence, 0.3);
|
|
1903
|
+
}
|
|
1904
|
+
if (colorDepth !== pixelDepth) {
|
|
1905
|
+
anomalies.push("depth-mismatch");
|
|
1906
|
+
confidence = Math.max(confidence, 0.3);
|
|
1907
|
+
}
|
|
1908
|
+
const dpr = window.devicePixelRatio;
|
|
1909
|
+
if (dpr === 0 || dpr === void 0) {
|
|
1910
|
+
anomalies.push("missing-device-pixel-ratio");
|
|
1911
|
+
confidence = Math.max(confidence, 0.5);
|
|
1912
|
+
} else if (dpr < 0.5 || dpr > 5) {
|
|
1913
|
+
anomalies.push("unusual-device-pixel-ratio");
|
|
1914
|
+
confidence = Math.max(confidence, 0.4);
|
|
1915
|
+
}
|
|
1916
|
+
if (screen.orientation) {
|
|
1917
|
+
const orientationType = screen.orientation.type;
|
|
1918
|
+
const orientationAngle = screen.orientation.angle;
|
|
1919
|
+
if (orientationType.includes("landscape") && width < height) {
|
|
1920
|
+
anomalies.push("orientation-dimension-mismatch");
|
|
1921
|
+
confidence = Math.max(confidence, 0.4);
|
|
1922
|
+
}
|
|
1923
|
+
if (orientationType.includes("portrait") && width > height) {
|
|
1924
|
+
anomalies.push("orientation-dimension-mismatch");
|
|
1925
|
+
confidence = Math.max(confidence, 0.4);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (innerWidth === outerWidth && innerHeight === outerHeight && outerWidth > 0 && outerHeight > 0) {
|
|
1929
|
+
anomalies.push("no-browser-chrome");
|
|
1930
|
+
confidence = Math.max(confidence, 0.5);
|
|
1931
|
+
}
|
|
1932
|
+
const triggered = anomalies.length > 0;
|
|
1933
|
+
return this.createResult(triggered, {
|
|
1934
|
+
anomalies,
|
|
1935
|
+
dimensions: {
|
|
1936
|
+
screen: { width, height },
|
|
1937
|
+
available: { width: availWidth, height: availHeight },
|
|
1938
|
+
window: {
|
|
1939
|
+
outer: { width: outerWidth, height: outerHeight },
|
|
1940
|
+
inner: { width: innerWidth, height: innerHeight }
|
|
1941
|
+
},
|
|
1942
|
+
colorDepth,
|
|
1943
|
+
devicePixelRatio: dpr
|
|
1944
|
+
}
|
|
1945
|
+
}, confidence);
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
__publicField(ScreenSignal, "id", "screen");
|
|
1949
|
+
__publicField(ScreenSignal, "category", "fingerprint");
|
|
1950
|
+
__publicField(ScreenSignal, "weight", 0.4);
|
|
1951
|
+
__publicField(ScreenSignal, "description", "Detects unusual screen dimensions");
|
|
1952
|
+
|
|
1953
|
+
// src/signals/timing/PageLoadSignal.js
|
|
1954
|
+
var PageLoadSignal = class extends Signal {
|
|
1955
|
+
async detect() {
|
|
1956
|
+
const anomalies = [];
|
|
1957
|
+
let confidence = 0;
|
|
1958
|
+
if (!window.performance || !performance.timing) {
|
|
1959
|
+
if (performance.getEntriesByType) {
|
|
1960
|
+
const navEntries = performance.getEntriesByType("navigation");
|
|
1961
|
+
if (navEntries.length > 0) {
|
|
1962
|
+
return this._analyzeNavigationTiming(navEntries[0]);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
anomalies.push("no-performance-api");
|
|
1966
|
+
confidence = Math.max(confidence, 0.3);
|
|
1967
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
1968
|
+
}
|
|
1969
|
+
const timing = performance.timing;
|
|
1970
|
+
const navigationStart = timing.navigationStart;
|
|
1971
|
+
const domContentLoaded = timing.domContentLoadedEventEnd - navigationStart;
|
|
1972
|
+
const domComplete = timing.domComplete - navigationStart;
|
|
1973
|
+
const loadComplete = timing.loadEventEnd - navigationStart;
|
|
1974
|
+
const dnsLookup = timing.domainLookupEnd - timing.domainLookupStart;
|
|
1975
|
+
const tcpConnection = timing.connectEnd - timing.connectStart;
|
|
1976
|
+
const serverResponse = timing.responseEnd - timing.requestStart;
|
|
1977
|
+
const domProcessing = timing.domComplete - timing.domLoading;
|
|
1978
|
+
if (domContentLoaded > 0 && domContentLoaded < 10) {
|
|
1979
|
+
anomalies.push("instant-dom-content-loaded");
|
|
1980
|
+
confidence = Math.max(confidence, 0.7);
|
|
1981
|
+
}
|
|
1982
|
+
if (dnsLookup === 0 && tcpConnection === 0 && serverResponse < 5) {
|
|
1983
|
+
anomalies.push("zero-network-timing");
|
|
1984
|
+
confidence = Math.max(confidence, 0.4);
|
|
1985
|
+
}
|
|
1986
|
+
if (domContentLoaded < 0 || domComplete < 0 || loadComplete < 0) {
|
|
1987
|
+
anomalies.push("negative-timing");
|
|
1988
|
+
confidence = Math.max(confidence, 0.8);
|
|
1989
|
+
}
|
|
1990
|
+
if (timing.domContentLoadedEventEnd > 0 && timing.loadEventEnd > 0) {
|
|
1991
|
+
if (timing.domContentLoadedEventEnd > timing.loadEventEnd) {
|
|
1992
|
+
anomalies.push("timing-order-violation");
|
|
1993
|
+
confidence = Math.max(confidence, 0.7);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (domProcessing > 3e4) {
|
|
1997
|
+
anomalies.push("excessive-dom-processing");
|
|
1998
|
+
confidence = Math.max(confidence, 0.3);
|
|
1999
|
+
}
|
|
2000
|
+
const scriptsLoadedTime = timing.domContentLoadedEventStart - timing.responseEnd;
|
|
2001
|
+
if (scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
|
|
2002
|
+
anomalies.push("instant-script-execution");
|
|
2003
|
+
confidence = Math.max(confidence, 0.4);
|
|
2004
|
+
}
|
|
2005
|
+
const perfNow1 = performance.now();
|
|
2006
|
+
const perfNow2 = performance.now();
|
|
2007
|
+
if (perfNow1 === perfNow2 && perfNow1 > 0) {
|
|
2008
|
+
anomalies.push("frozen-performance-now");
|
|
2009
|
+
confidence = Math.max(confidence, 0.6);
|
|
2010
|
+
}
|
|
2011
|
+
const dateNow1 = Date.now();
|
|
2012
|
+
const perfNow3 = performance.now();
|
|
2013
|
+
const dateNow2 = Date.now();
|
|
2014
|
+
if (Math.abs(dateNow2 - dateNow1 - (performance.now() - perfNow3)) > 100) {
|
|
2015
|
+
anomalies.push("timing-inconsistency");
|
|
2016
|
+
confidence = Math.max(confidence, 0.5);
|
|
2017
|
+
}
|
|
2018
|
+
const triggered = anomalies.length > 0;
|
|
2019
|
+
return this.createResult(triggered, {
|
|
2020
|
+
anomalies,
|
|
2021
|
+
timings: {
|
|
2022
|
+
domContentLoaded,
|
|
2023
|
+
domComplete,
|
|
2024
|
+
loadComplete,
|
|
2025
|
+
dnsLookup,
|
|
2026
|
+
tcpConnection,
|
|
2027
|
+
serverResponse,
|
|
2028
|
+
domProcessing
|
|
2029
|
+
}
|
|
2030
|
+
}, confidence);
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Analyze Navigation Timing Level 2 API data.
|
|
2034
|
+
* @param {PerformanceNavigationTiming} entry - Navigation timing entry
|
|
2035
|
+
* @returns {SignalResult}
|
|
2036
|
+
*/
|
|
2037
|
+
_analyzeNavigationTiming(entry) {
|
|
2038
|
+
const anomalies = [];
|
|
2039
|
+
let confidence = 0;
|
|
2040
|
+
const domContentLoaded = entry.domContentLoadedEventEnd;
|
|
2041
|
+
const loadComplete = entry.loadEventEnd;
|
|
2042
|
+
const dnsLookup = entry.domainLookupEnd - entry.domainLookupStart;
|
|
2043
|
+
const serverResponse = entry.responseEnd - entry.requestStart;
|
|
2044
|
+
if (domContentLoaded > 0 && domContentLoaded < 10) {
|
|
2045
|
+
anomalies.push("instant-dom-content-loaded");
|
|
2046
|
+
confidence = Math.max(confidence, 0.7);
|
|
2047
|
+
}
|
|
2048
|
+
if (dnsLookup === 0 && serverResponse === 0) {
|
|
2049
|
+
anomalies.push("zero-network-timing");
|
|
2050
|
+
confidence = Math.max(confidence, 0.4);
|
|
2051
|
+
}
|
|
2052
|
+
const triggered = anomalies.length > 0;
|
|
2053
|
+
return this.createResult(triggered, {
|
|
2054
|
+
anomalies,
|
|
2055
|
+
timings: {
|
|
2056
|
+
domContentLoaded,
|
|
2057
|
+
loadComplete,
|
|
2058
|
+
dnsLookup,
|
|
2059
|
+
serverResponse
|
|
2060
|
+
}
|
|
2061
|
+
}, confidence);
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
__publicField(PageLoadSignal, "id", "page-load");
|
|
2065
|
+
__publicField(PageLoadSignal, "category", "timing");
|
|
2066
|
+
__publicField(PageLoadSignal, "weight", 0.5);
|
|
2067
|
+
__publicField(PageLoadSignal, "description", "Detects suspicious page load timing");
|
|
2068
|
+
|
|
2069
|
+
// src/signals/timing/DOMContentTimingSignal.js
|
|
2070
|
+
var DOMContentTimingSignal = class extends Signal {
|
|
2071
|
+
constructor(options = {}) {
|
|
2072
|
+
super(options);
|
|
2073
|
+
this._domContentLoadedTime = null;
|
|
2074
|
+
this._documentReadyState = document.readyState;
|
|
2075
|
+
this._captureTime = performance.now();
|
|
2076
|
+
if (document.readyState === "loading") {
|
|
2077
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
2078
|
+
this._domContentLoadedTime = performance.now();
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
async detect() {
|
|
2083
|
+
const anomalies = [];
|
|
2084
|
+
let confidence = 0;
|
|
2085
|
+
const now = performance.now();
|
|
2086
|
+
const readyState = document.readyState;
|
|
2087
|
+
let resourceCount = 0;
|
|
2088
|
+
let totalResourceTime = 0;
|
|
2089
|
+
let externalScriptCount = 0;
|
|
2090
|
+
if (performance.getEntriesByType) {
|
|
2091
|
+
const resources = performance.getEntriesByType("resource");
|
|
2092
|
+
resourceCount = resources.length;
|
|
2093
|
+
for (const resource of resources) {
|
|
2094
|
+
totalResourceTime += resource.duration;
|
|
2095
|
+
if (resource.initiatorType === "script" && resource.name.startsWith("http")) {
|
|
2096
|
+
externalScriptCount++;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (resourceCount === 0 && readyState === "complete") {
|
|
2101
|
+
anomalies.push("no-resources-loaded");
|
|
2102
|
+
confidence = Math.max(confidence, 0.4);
|
|
2103
|
+
}
|
|
2104
|
+
if (this._domContentLoadedTime && this._domContentLoadedTime < 50 && resourceCount === 0) {
|
|
2105
|
+
anomalies.push("instant-ready-no-resources");
|
|
2106
|
+
confidence = Math.max(confidence, 0.6);
|
|
2107
|
+
}
|
|
2108
|
+
if (document.hidden && this._documentReadyState === "loading") {
|
|
2109
|
+
anomalies.push("hidden-at-load");
|
|
2110
|
+
confidence = Math.max(confidence, 0.3);
|
|
2111
|
+
}
|
|
2112
|
+
if (typeof document.visibilityState === "undefined") {
|
|
2113
|
+
anomalies.push("no-visibility-api");
|
|
2114
|
+
confidence = Math.max(confidence, 0.4);
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
const startMutation = performance.now();
|
|
2118
|
+
const testDiv = document.createElement("div");
|
|
2119
|
+
testDiv.id = "__bot_detection_test__";
|
|
2120
|
+
document.body.appendChild(testDiv);
|
|
2121
|
+
const afterAppend = performance.now();
|
|
2122
|
+
document.body.removeChild(testDiv);
|
|
2123
|
+
const afterRemove = performance.now();
|
|
2124
|
+
const appendTime = afterAppend - startMutation;
|
|
2125
|
+
const removeTime = afterRemove - afterAppend;
|
|
2126
|
+
if (appendTime === 0 && removeTime === 0) {
|
|
2127
|
+
anomalies.push("instant-dom-operations");
|
|
2128
|
+
confidence = Math.max(confidence, 0.5);
|
|
2129
|
+
}
|
|
2130
|
+
} catch (e) {
|
|
2131
|
+
if (!document.body) {
|
|
2132
|
+
anomalies.push("no-document-body");
|
|
2133
|
+
confidence = Math.max(confidence, 0.4);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
if (typeof MutationObserver === "undefined") {
|
|
2137
|
+
anomalies.push("no-mutation-observer");
|
|
2138
|
+
confidence = Math.max(confidence, 0.5);
|
|
2139
|
+
}
|
|
2140
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
2141
|
+
anomalies.push("no-request-animation-frame");
|
|
2142
|
+
confidence = Math.max(confidence, 0.5);
|
|
2143
|
+
}
|
|
2144
|
+
if (performance.getEntriesByType) {
|
|
2145
|
+
const paintEntries = performance.getEntriesByType("paint");
|
|
2146
|
+
const firstPaint = paintEntries.find((e) => e.name === "first-paint");
|
|
2147
|
+
if (!firstPaint && readyState === "complete" && now > 1e3) {
|
|
2148
|
+
anomalies.push("no-first-paint");
|
|
2149
|
+
confidence = Math.max(confidence, 0.4);
|
|
2150
|
+
}
|
|
2151
|
+
const fcp = paintEntries.find((e) => e.name === "first-contentful-paint");
|
|
2152
|
+
if (!fcp && readyState === "complete" && now > 1e3) {
|
|
2153
|
+
anomalies.push("no-first-contentful-paint");
|
|
2154
|
+
confidence = Math.max(confidence, 0.4);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
2158
|
+
anomalies.push("no-intersection-observer");
|
|
2159
|
+
confidence = Math.max(confidence, 0.4);
|
|
2160
|
+
}
|
|
2161
|
+
const triggered = anomalies.length > 0;
|
|
2162
|
+
return this.createResult(triggered, {
|
|
2163
|
+
anomalies,
|
|
2164
|
+
metrics: {
|
|
2165
|
+
readyState,
|
|
2166
|
+
resourceCount,
|
|
2167
|
+
externalScriptCount,
|
|
2168
|
+
domContentLoadedTime: this._domContentLoadedTime,
|
|
2169
|
+
documentHidden: document.hidden
|
|
2170
|
+
}
|
|
2171
|
+
}, confidence);
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
__publicField(DOMContentTimingSignal, "id", "dom-content-timing");
|
|
2175
|
+
__publicField(DOMContentTimingSignal, "category", "timing");
|
|
2176
|
+
__publicField(DOMContentTimingSignal, "weight", 0.4);
|
|
2177
|
+
__publicField(DOMContentTimingSignal, "description", "Analyzes DOM content loaded timing patterns");
|
|
2178
|
+
|
|
2179
|
+
// src/signals/automation/PuppeteerSignal.js
|
|
2180
|
+
var PuppeteerSignal = class extends Signal {
|
|
2181
|
+
async detect() {
|
|
2182
|
+
const indicators = [];
|
|
2183
|
+
let confidence = 0;
|
|
2184
|
+
if (window.__puppeteer_evaluation_script__) {
|
|
2185
|
+
indicators.push("puppeteer-evaluation-script");
|
|
2186
|
+
confidence = Math.max(confidence, 1);
|
|
2187
|
+
}
|
|
2188
|
+
const puppeteerGlobals = [
|
|
2189
|
+
"__puppeteer_evaluation_script__",
|
|
2190
|
+
"__puppeteer",
|
|
2191
|
+
"puppeteer"
|
|
2192
|
+
];
|
|
2193
|
+
for (const global of puppeteerGlobals) {
|
|
2194
|
+
if (global in window) {
|
|
2195
|
+
indicators.push(`global-${global}`);
|
|
2196
|
+
confidence = Math.max(confidence, 1);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const ua = navigator.userAgent || "";
|
|
2200
|
+
if (ua.includes("HeadlessChrome")) {
|
|
2201
|
+
indicators.push("headless-chrome-ua");
|
|
2202
|
+
confidence = Math.max(confidence, 0.9);
|
|
2203
|
+
}
|
|
2204
|
+
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array || window.cdc_adoQpoasnfa76pfcZLmcfl_Promise || window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
|
|
2205
|
+
indicators.push("cdp-artifacts");
|
|
2206
|
+
confidence = Math.max(confidence, 1);
|
|
2207
|
+
}
|
|
2208
|
+
try {
|
|
2209
|
+
const evalTest = window.eval.toString();
|
|
2210
|
+
if (evalTest.includes("puppeteer")) {
|
|
2211
|
+
indicators.push("eval-puppeteer");
|
|
2212
|
+
confidence = Math.max(confidence, 0.9);
|
|
2213
|
+
}
|
|
2214
|
+
} catch (e) {
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
throw new Error("stack trace test");
|
|
2218
|
+
} catch (e) {
|
|
2219
|
+
const stack = e.stack || "";
|
|
2220
|
+
if (stack.includes("puppeteer") || stack.includes("pptr")) {
|
|
2221
|
+
indicators.push("stack-trace-puppeteer");
|
|
2222
|
+
confidence = Math.max(confidence, 0.8);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (window.innerWidth === 800 && window.innerHeight === 600) {
|
|
2226
|
+
indicators.push("default-viewport");
|
|
2227
|
+
confidence = Math.max(confidence, 0.3);
|
|
2228
|
+
}
|
|
2229
|
+
if (navigator.webdriver === true) {
|
|
2230
|
+
indicators.push("webdriver-flag");
|
|
2231
|
+
confidence = Math.max(confidence, 0.9);
|
|
2232
|
+
}
|
|
2233
|
+
const suspiciousBindings = Object.keys(window).filter((key) => {
|
|
2234
|
+
return key.startsWith("__") && typeof window[key] === "function";
|
|
2235
|
+
});
|
|
2236
|
+
if (suspiciousBindings.length > 3) {
|
|
2237
|
+
indicators.push("suspicious-bindings");
|
|
2238
|
+
confidence = Math.max(confidence, 0.5);
|
|
2239
|
+
}
|
|
2240
|
+
if (typeof window.chrome !== "undefined") {
|
|
2241
|
+
if (!window.chrome.runtime || !window.chrome.runtime.id) {
|
|
2242
|
+
indicators.push("incomplete-chrome-object");
|
|
2243
|
+
confidence = Math.max(confidence, 0.4);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const triggered = indicators.length > 0;
|
|
2247
|
+
return this.createResult(triggered, { indicators }, confidence);
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
__publicField(PuppeteerSignal, "id", "puppeteer");
|
|
2251
|
+
__publicField(PuppeteerSignal, "category", "automation");
|
|
2252
|
+
__publicField(PuppeteerSignal, "weight", 1);
|
|
2253
|
+
__publicField(PuppeteerSignal, "description", "Detects Puppeteer automation artifacts");
|
|
2254
|
+
|
|
2255
|
+
// src/signals/automation/PlaywrightSignal.js
|
|
2256
|
+
var PlaywrightSignal = class extends Signal {
|
|
2257
|
+
async detect() {
|
|
2258
|
+
const indicators = [];
|
|
2259
|
+
let confidence = 0;
|
|
2260
|
+
if (window.__playwright) {
|
|
2261
|
+
indicators.push("playwright-namespace");
|
|
2262
|
+
confidence = Math.max(confidence, 1);
|
|
2263
|
+
}
|
|
2264
|
+
const playwrightGlobals = [
|
|
2265
|
+
"__playwright",
|
|
2266
|
+
"__pw_manual",
|
|
2267
|
+
"__pwInitScripts",
|
|
2268
|
+
"playwright"
|
|
2269
|
+
];
|
|
2270
|
+
for (const global of playwrightGlobals) {
|
|
2271
|
+
if (global in window) {
|
|
2272
|
+
indicators.push(`global-${global}`);
|
|
2273
|
+
confidence = Math.max(confidence, 1);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (window.__playwright__binding__) {
|
|
2277
|
+
indicators.push("playwright-binding");
|
|
2278
|
+
confidence = Math.max(confidence, 1);
|
|
2279
|
+
}
|
|
2280
|
+
const ua = navigator.userAgent || "";
|
|
2281
|
+
if (ua.includes("Playwright") || ua.includes("HeadlessChrome")) {
|
|
2282
|
+
indicators.push("playwright-ua-marker");
|
|
2283
|
+
confidence = Math.max(confidence, ua.includes("Playwright") ? 1 : 0.7);
|
|
2284
|
+
}
|
|
2285
|
+
if (navigator.webdriver === true) {
|
|
2286
|
+
indicators.push("webdriver-flag");
|
|
2287
|
+
confidence = Math.max(confidence, 0.8);
|
|
2288
|
+
}
|
|
2289
|
+
try {
|
|
2290
|
+
const windowKeys = Object.keys(window);
|
|
2291
|
+
const pwBindings = windowKeys.filter((k) => k.startsWith("__pw"));
|
|
2292
|
+
if (pwBindings.length > 0) {
|
|
2293
|
+
indicators.push("pw-bindings");
|
|
2294
|
+
confidence = Math.max(confidence, 1);
|
|
2295
|
+
}
|
|
2296
|
+
} catch (e) {
|
|
2297
|
+
}
|
|
2298
|
+
if (typeof window.__pw_date_intercepted !== "undefined") {
|
|
2299
|
+
indicators.push("date-interception");
|
|
2300
|
+
confidence = Math.max(confidence, 0.9);
|
|
2301
|
+
}
|
|
2302
|
+
if (window.__pw_geolocation__) {
|
|
2303
|
+
indicators.push("geolocation-mock");
|
|
2304
|
+
confidence = Math.max(confidence, 0.9);
|
|
2305
|
+
}
|
|
2306
|
+
if (window.__pw_permissions__) {
|
|
2307
|
+
indicators.push("permissions-override");
|
|
2308
|
+
confidence = Math.max(confidence, 0.9);
|
|
2309
|
+
}
|
|
2310
|
+
if (window.__cdpSession__) {
|
|
2311
|
+
indicators.push("cdp-session");
|
|
2312
|
+
confidence = Math.max(confidence, 0.8);
|
|
2313
|
+
}
|
|
2314
|
+
try {
|
|
2315
|
+
throw new Error("stack trace test");
|
|
2316
|
+
} catch (e) {
|
|
2317
|
+
const stack = e.stack || "";
|
|
2318
|
+
if (stack.includes("playwright") || stack.includes("__pw")) {
|
|
2319
|
+
indicators.push("stack-trace-playwright");
|
|
2320
|
+
confidence = Math.max(confidence, 0.8);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
try {
|
|
2324
|
+
const date = /* @__PURE__ */ new Date();
|
|
2325
|
+
const localeString = date.toLocaleString();
|
|
2326
|
+
if (window.__pwTimezone__) {
|
|
2327
|
+
indicators.push("timezone-mock");
|
|
2328
|
+
confidence = Math.max(confidence, 0.8);
|
|
2329
|
+
}
|
|
2330
|
+
} catch (e) {
|
|
2331
|
+
}
|
|
2332
|
+
const triggered = indicators.length > 0;
|
|
2333
|
+
return this.createResult(triggered, { indicators }, confidence);
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
__publicField(PlaywrightSignal, "id", "playwright");
|
|
2337
|
+
__publicField(PlaywrightSignal, "category", "automation");
|
|
2338
|
+
__publicField(PlaywrightSignal, "weight", 1);
|
|
2339
|
+
__publicField(PlaywrightSignal, "description", "Detects Playwright automation artifacts");
|
|
2340
|
+
|
|
2341
|
+
// src/signals/automation/SeleniumSignal.js
|
|
2342
|
+
var SeleniumSignal = class extends Signal {
|
|
2343
|
+
async detect() {
|
|
2344
|
+
const indicators = [];
|
|
2345
|
+
let confidence = 0;
|
|
2346
|
+
if (navigator.webdriver === true) {
|
|
2347
|
+
indicators.push("webdriver-flag");
|
|
2348
|
+
confidence = Math.max(confidence, 1);
|
|
2349
|
+
}
|
|
2350
|
+
const seleniumGlobals = [
|
|
2351
|
+
"_selenium",
|
|
2352
|
+
"callSelenium",
|
|
2353
|
+
"_Selenium_IDE_Recorder",
|
|
2354
|
+
"__selenium_evaluate",
|
|
2355
|
+
"__selenium_unwrap",
|
|
2356
|
+
"__webdriver_evaluate",
|
|
2357
|
+
"__webdriver_unwrap",
|
|
2358
|
+
"__webdriver_script_function",
|
|
2359
|
+
"__webdriver_script_func",
|
|
2360
|
+
"__fxdriver_evaluate",
|
|
2361
|
+
"__fxdriver_unwrap",
|
|
2362
|
+
"webdriver"
|
|
2363
|
+
];
|
|
2364
|
+
for (const global of seleniumGlobals) {
|
|
2365
|
+
if (global in window) {
|
|
2366
|
+
indicators.push(`global-${global}`);
|
|
2367
|
+
confidence = Math.max(confidence, 1);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
const seleniumDocProps = [
|
|
2371
|
+
"__webdriver_script_fn",
|
|
2372
|
+
"__driver_evaluate",
|
|
2373
|
+
"__webdriver_evaluate",
|
|
2374
|
+
"__selenium_evaluate",
|
|
2375
|
+
"__fxdriver_evaluate",
|
|
2376
|
+
"__driver_unwrap",
|
|
2377
|
+
"__webdriver_unwrap",
|
|
2378
|
+
"__selenium_unwrap",
|
|
2379
|
+
"__fxdriver_unwrap"
|
|
2380
|
+
];
|
|
2381
|
+
for (const prop of seleniumDocProps) {
|
|
2382
|
+
if (prop in document) {
|
|
2383
|
+
indicators.push(`document-${prop}`);
|
|
2384
|
+
confidence = Math.max(confidence, 1);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const windowKeys = Object.keys(window);
|
|
2388
|
+
const cdcVars = windowKeys.filter(
|
|
2389
|
+
(key) => key.startsWith("$cdc_") || key.startsWith("$wdc_") || key.startsWith("$chrome_asyncScriptInfo")
|
|
2390
|
+
);
|
|
2391
|
+
if (cdcVars.length > 0) {
|
|
2392
|
+
indicators.push("chromedriver-variables");
|
|
2393
|
+
confidence = Math.max(confidence, 1);
|
|
2394
|
+
}
|
|
2395
|
+
if (window.webdriverCallback || document.documentElement.getAttribute("webdriver")) {
|
|
2396
|
+
indicators.push("geckodriver-artifacts");
|
|
2397
|
+
confidence = Math.max(confidence, 1);
|
|
2398
|
+
}
|
|
2399
|
+
try {
|
|
2400
|
+
const docElement = document.documentElement;
|
|
2401
|
+
if (docElement.hasAttribute("webdriver") || docElement.getAttribute("selenium") || docElement.getAttribute("driver")) {
|
|
2402
|
+
indicators.push("document-webdriver-attr");
|
|
2403
|
+
confidence = Math.max(confidence, 1);
|
|
2404
|
+
}
|
|
2405
|
+
} catch (e) {
|
|
2406
|
+
}
|
|
2407
|
+
if (window.selenium || window.sideex) {
|
|
2408
|
+
indicators.push("selenium-ide");
|
|
2409
|
+
confidence = Math.max(confidence, 1);
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
|
|
2413
|
+
if (descriptor) {
|
|
2414
|
+
if (descriptor.get) {
|
|
2415
|
+
const getterStr = descriptor.get.toString();
|
|
2416
|
+
if (!getterStr.includes("[native code]")) {
|
|
2417
|
+
indicators.push("webdriver-getter-modified");
|
|
2418
|
+
confidence = Math.max(confidence, 0.7);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
} catch (e) {
|
|
2423
|
+
}
|
|
2424
|
+
if (window.domAutomation || window.domAutomationController) {
|
|
2425
|
+
indicators.push("dom-automation");
|
|
2426
|
+
confidence = Math.max(confidence, 1);
|
|
2427
|
+
}
|
|
2428
|
+
if (window.awesomium) {
|
|
2429
|
+
indicators.push("awesomium");
|
|
2430
|
+
confidence = Math.max(confidence, 0.9);
|
|
2431
|
+
}
|
|
2432
|
+
if (window.external && window.external.toString().includes("Selenium")) {
|
|
2433
|
+
indicators.push("external-selenium");
|
|
2434
|
+
confidence = Math.max(confidence, 1);
|
|
2435
|
+
}
|
|
2436
|
+
const triggered = indicators.length > 0;
|
|
2437
|
+
return this.createResult(triggered, { indicators }, confidence);
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
__publicField(SeleniumSignal, "id", "selenium");
|
|
2441
|
+
__publicField(SeleniumSignal, "category", "automation");
|
|
2442
|
+
__publicField(SeleniumSignal, "weight", 1);
|
|
2443
|
+
__publicField(SeleniumSignal, "description", "Detects Selenium WebDriver artifacts");
|
|
2444
|
+
|
|
2445
|
+
// src/signals/automation/PhantomJSSignal.js
|
|
2446
|
+
var PhantomJSSignal = class extends Signal {
|
|
2447
|
+
async detect() {
|
|
2448
|
+
const indicators = [];
|
|
2449
|
+
let confidence = 0;
|
|
2450
|
+
if (window.callPhantom) {
|
|
2451
|
+
indicators.push("callPhantom");
|
|
2452
|
+
confidence = Math.max(confidence, 1);
|
|
2453
|
+
}
|
|
2454
|
+
if (window._phantom) {
|
|
2455
|
+
indicators.push("_phantom");
|
|
2456
|
+
confidence = Math.max(confidence, 1);
|
|
2457
|
+
}
|
|
2458
|
+
if (window.phantom) {
|
|
2459
|
+
indicators.push("phantom");
|
|
2460
|
+
confidence = Math.max(confidence, 1);
|
|
2461
|
+
}
|
|
2462
|
+
const ua = navigator.userAgent || "";
|
|
2463
|
+
if (ua.includes("PhantomJS")) {
|
|
2464
|
+
indicators.push("phantomjs-ua");
|
|
2465
|
+
confidence = Math.max(confidence, 1);
|
|
2466
|
+
}
|
|
2467
|
+
if (window.__phantomas) {
|
|
2468
|
+
indicators.push("phantomas");
|
|
2469
|
+
confidence = Math.max(confidence, 1);
|
|
2470
|
+
}
|
|
2471
|
+
if (window.__casper) {
|
|
2472
|
+
indicators.push("casperjs");
|
|
2473
|
+
confidence = Math.max(confidence, 1);
|
|
2474
|
+
}
|
|
2475
|
+
if (window.casper) {
|
|
2476
|
+
indicators.push("casper-global");
|
|
2477
|
+
confidence = Math.max(confidence, 1);
|
|
2478
|
+
}
|
|
2479
|
+
if (window.slimer) {
|
|
2480
|
+
indicators.push("slimerjs");
|
|
2481
|
+
confidence = Math.max(confidence, 1);
|
|
2482
|
+
}
|
|
2483
|
+
if (window.__nightmare) {
|
|
2484
|
+
indicators.push("nightmare");
|
|
2485
|
+
confidence = Math.max(confidence, 1);
|
|
2486
|
+
}
|
|
2487
|
+
if (window.nightmare) {
|
|
2488
|
+
indicators.push("nightmare-global");
|
|
2489
|
+
confidence = Math.max(confidence, 1);
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const funcString = Function.prototype.toString.call(Function);
|
|
2493
|
+
if (funcString.includes("phantom") || funcString.includes("Phantom")) {
|
|
2494
|
+
indicators.push("function-prototype-phantom");
|
|
2495
|
+
confidence = Math.max(confidence, 0.8);
|
|
2496
|
+
}
|
|
2497
|
+
} catch (e) {
|
|
2498
|
+
}
|
|
2499
|
+
try {
|
|
2500
|
+
throw new Error("test");
|
|
2501
|
+
} catch (e) {
|
|
2502
|
+
const stack = e.stack || "";
|
|
2503
|
+
if (stack.includes("phantom")) {
|
|
2504
|
+
indicators.push("stack-trace-phantom");
|
|
2505
|
+
confidence = Math.max(confidence, 0.9);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
2509
|
+
if (indicators.length > 0) {
|
|
2510
|
+
indicators.push("no-plugins-phantom");
|
|
2511
|
+
confidence = Math.max(confidence, 0.5);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const phantomProps = [
|
|
2515
|
+
"__PHANTOM__",
|
|
2516
|
+
"PHANTOM",
|
|
2517
|
+
"Buffer",
|
|
2518
|
+
// PhantomJS exposes Node.js Buffer
|
|
2519
|
+
"process"
|
|
2520
|
+
// May expose Node.js process
|
|
2521
|
+
];
|
|
2522
|
+
for (const prop of phantomProps) {
|
|
2523
|
+
if (prop in window && prop !== "Buffer" && prop !== "process") {
|
|
2524
|
+
indicators.push(`phantom-prop-${prop.toLowerCase()}`);
|
|
2525
|
+
confidence = Math.max(confidence, 0.9);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (ua.includes("QtWebKit")) {
|
|
2529
|
+
indicators.push("qtwebkit");
|
|
2530
|
+
confidence = Math.max(confidence, 0.7);
|
|
2531
|
+
}
|
|
2532
|
+
const triggered = indicators.length > 0;
|
|
2533
|
+
return this.createResult(triggered, { indicators }, confidence);
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
__publicField(PhantomJSSignal, "id", "phantomjs");
|
|
2537
|
+
__publicField(PhantomJSSignal, "category", "automation");
|
|
2538
|
+
__publicField(PhantomJSSignal, "weight", 1);
|
|
2539
|
+
__publicField(PhantomJSSignal, "description", "Detects PhantomJS automation artifacts");
|
|
2540
|
+
|
|
2541
|
+
// src/index.js
|
|
2542
|
+
var Signals = {
|
|
2543
|
+
// Environment signals
|
|
2544
|
+
WebDriverSignal,
|
|
2545
|
+
HeadlessSignal,
|
|
2546
|
+
NavigatorAnomalySignal,
|
|
2547
|
+
PermissionsSignal,
|
|
2548
|
+
// Behavioral signals
|
|
2549
|
+
MouseMovementSignal,
|
|
2550
|
+
KeyboardPatternSignal,
|
|
2551
|
+
InteractionTimingSignal,
|
|
2552
|
+
ScrollBehaviorSignal,
|
|
2553
|
+
// Fingerprint signals
|
|
2554
|
+
PluginsSignal,
|
|
2555
|
+
WebGLSignal,
|
|
2556
|
+
CanvasSignal,
|
|
2557
|
+
AudioContextSignal,
|
|
2558
|
+
ScreenSignal,
|
|
2559
|
+
// Timing signals
|
|
2560
|
+
PageLoadSignal,
|
|
2561
|
+
DOMContentTimingSignal,
|
|
2562
|
+
// Automation framework signals
|
|
2563
|
+
PuppeteerSignal,
|
|
2564
|
+
PlaywrightSignal,
|
|
2565
|
+
SeleniumSignal,
|
|
2566
|
+
PhantomJSSignal
|
|
2567
|
+
};
|
|
2568
|
+
var defaultInstantSignals = [
|
|
2569
|
+
new WebDriverSignal(),
|
|
2570
|
+
new HeadlessSignal(),
|
|
2571
|
+
new NavigatorAnomalySignal(),
|
|
2572
|
+
new PermissionsSignal(),
|
|
2573
|
+
new PluginsSignal(),
|
|
2574
|
+
new WebGLSignal(),
|
|
2575
|
+
new CanvasSignal(),
|
|
2576
|
+
new AudioContextSignal(),
|
|
2577
|
+
new ScreenSignal(),
|
|
2578
|
+
new PageLoadSignal(),
|
|
2579
|
+
new DOMContentTimingSignal(),
|
|
2580
|
+
new PuppeteerSignal(),
|
|
2581
|
+
new PlaywrightSignal(),
|
|
2582
|
+
new SeleniumSignal(),
|
|
2583
|
+
new PhantomJSSignal()
|
|
2584
|
+
];
|
|
2585
|
+
var defaultInteractionSignals = [
|
|
2586
|
+
new MouseMovementSignal(),
|
|
2587
|
+
new KeyboardPatternSignal(),
|
|
2588
|
+
new InteractionTimingSignal(),
|
|
2589
|
+
new ScrollBehaviorSignal()
|
|
2590
|
+
];
|
|
2591
|
+
var defaultSignals = [...defaultInstantSignals, ...defaultInteractionSignals];
|
|
2592
|
+
function createDetector(options = {}) {
|
|
2593
|
+
const {
|
|
2594
|
+
includeInteractionSignals = true,
|
|
2595
|
+
instantBotSignals = ["webdriver", "puppeteer", "playwright", "selenium", "phantomjs"],
|
|
2596
|
+
...detectorOptions
|
|
2597
|
+
} = options;
|
|
2598
|
+
const signals = includeInteractionSignals ? [...defaultInstantSignals, ...defaultInteractionSignals.map((s) => {
|
|
2599
|
+
const SignalClass = s.constructor;
|
|
2600
|
+
return new SignalClass(s.options);
|
|
2601
|
+
})] : defaultInstantSignals.map((s) => {
|
|
2602
|
+
const SignalClass = s.constructor;
|
|
2603
|
+
return new SignalClass(s.options);
|
|
2604
|
+
});
|
|
2605
|
+
return new BotDetector({
|
|
2606
|
+
signals,
|
|
2607
|
+
instantBotSignals,
|
|
2608
|
+
...detectorOptions
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
async function detect(options = {}) {
|
|
2612
|
+
const detector = createDetector({
|
|
2613
|
+
includeInteractionSignals: !options.skipInteractionSignals
|
|
2614
|
+
});
|
|
2615
|
+
return detector.detect(options);
|
|
2616
|
+
}
|
|
2617
|
+
async function detectInstant() {
|
|
2618
|
+
return detect({ skipInteractionSignals: true });
|
|
2619
|
+
}
|
|
2620
|
+
var index_default = {
|
|
2621
|
+
BotDetector,
|
|
2622
|
+
createDetector,
|
|
2623
|
+
detect,
|
|
2624
|
+
detectInstant,
|
|
2625
|
+
Signal,
|
|
2626
|
+
Signals,
|
|
2627
|
+
Verdict
|
|
2628
|
+
};
|
|
2629
|
+
//# sourceMappingURL=bot-detector.cjs.js.map
|