@niksbanna/bot-detector 1.0.2 → 1.0.4
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 +22 -0
- package/dist/bot-detector.cjs.js +115 -75
- package/dist/bot-detector.cjs.js.map +2 -2
- package/dist/bot-detector.esm.js +115 -75
- package/dist/bot-detector.esm.js.map +2 -2
- package/dist/bot-detector.iife.js +115 -75
- package/dist/bot-detector.iife.js.map +2 -2
- package/dist/bot-detector.iife.min.js +3 -1
- package/package.json +1 -2
- package/src/core/BotDetector.js +0 -284
- package/src/core/ScoringEngine.js +0 -134
- package/src/core/Signal.js +0 -181
- package/src/core/VerdictEngine.js +0 -132
- package/src/index.js +0 -273
- package/src/signals/automation/PhantomJSSignal.js +0 -137
- package/src/signals/automation/PlaywrightSignal.js +0 -129
- package/src/signals/automation/PuppeteerSignal.js +0 -122
- package/src/signals/automation/SeleniumSignal.js +0 -151
- package/src/signals/automation/index.js +0 -8
- package/src/signals/behavior/InteractionTimingSignal.js +0 -170
- package/src/signals/behavior/KeyboardPatternSignal.js +0 -235
- package/src/signals/behavior/MouseMovementSignal.js +0 -215
- package/src/signals/behavior/ScrollBehaviorSignal.js +0 -236
- package/src/signals/behavior/index.js +0 -8
- package/src/signals/environment/HeadlessSignal.js +0 -97
- package/src/signals/environment/NavigatorAnomalySignal.js +0 -117
- package/src/signals/environment/PermissionsSignal.js +0 -76
- package/src/signals/environment/WebDriverSignal.js +0 -58
- package/src/signals/environment/index.js +0 -8
- package/src/signals/fingerprint/AudioContextSignal.js +0 -158
- package/src/signals/fingerprint/CanvasSignal.js +0 -133
- package/src/signals/fingerprint/PluginsSignal.js +0 -106
- package/src/signals/fingerprint/ScreenSignal.js +0 -157
- package/src/signals/fingerprint/WebGLSignal.js +0 -146
- package/src/signals/fingerprint/index.js +0 -9
- package/src/signals/timing/DOMContentTimingSignal.js +0 -159
- package/src/signals/timing/PageLoadSignal.js +0 -165
- package/src/signals/timing/index.js +0 -6
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects suspicious interaction timing patterns.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Measures timing between page load and first interaction.
|
|
9
|
-
* Bots often interact too fast or with perfect timing patterns.
|
|
10
|
-
*/
|
|
11
|
-
class InteractionTimingSignal extends Signal {
|
|
12
|
-
static id = 'interaction-timing';
|
|
13
|
-
static category = 'behavior';
|
|
14
|
-
static weight = 0.6;
|
|
15
|
-
static description = 'Detects suspicious interaction timing';
|
|
16
|
-
static requiresInteraction = true;
|
|
17
|
-
|
|
18
|
-
constructor(options = {}) {
|
|
19
|
-
super(options);
|
|
20
|
-
this._pageLoadTime = performance.now();
|
|
21
|
-
this._firstInteractionTime = null;
|
|
22
|
-
this._interactions = [];
|
|
23
|
-
this._isTracking = false;
|
|
24
|
-
this._trackingDuration = options.trackingDuration || 5000;
|
|
25
|
-
this._boundHandler = null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Start tracking interactions.
|
|
30
|
-
*/
|
|
31
|
-
startTracking() {
|
|
32
|
-
if (this._isTracking) return;
|
|
33
|
-
|
|
34
|
-
this._interactions = [];
|
|
35
|
-
this._isTracking = true;
|
|
36
|
-
|
|
37
|
-
const interactionEvents = ['click', 'mousedown', 'touchstart', 'keydown', 'scroll'];
|
|
38
|
-
|
|
39
|
-
this._boundHandler = (e) => {
|
|
40
|
-
const now = performance.now();
|
|
41
|
-
if (this._firstInteractionTime === null) {
|
|
42
|
-
this._firstInteractionTime = now;
|
|
43
|
-
}
|
|
44
|
-
this._interactions.push({
|
|
45
|
-
type: e.type,
|
|
46
|
-
t: now,
|
|
47
|
-
timeSinceLoad: now - this._pageLoadTime,
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
for (const event of interactionEvents) {
|
|
52
|
-
document.addEventListener(event, this._boundHandler, { passive: true, capture: true });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Stop tracking interactions.
|
|
58
|
-
*/
|
|
59
|
-
stopTracking() {
|
|
60
|
-
if (!this._isTracking) return;
|
|
61
|
-
|
|
62
|
-
this._isTracking = false;
|
|
63
|
-
const interactionEvents = ['click', 'mousedown', 'touchstart', 'keydown', 'scroll'];
|
|
64
|
-
|
|
65
|
-
if (this._boundHandler) {
|
|
66
|
-
for (const event of interactionEvents) {
|
|
67
|
-
document.removeEventListener(event, this._boundHandler, { capture: true });
|
|
68
|
-
}
|
|
69
|
-
this._boundHandler = null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async detect() {
|
|
74
|
-
const anomalies = [];
|
|
75
|
-
let confidence = 0;
|
|
76
|
-
|
|
77
|
-
// Start tracking if not already
|
|
78
|
-
if (!this._isTracking && this._interactions.length === 0) {
|
|
79
|
-
this.startTracking();
|
|
80
|
-
await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
|
|
81
|
-
this.stopTracking();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const interactions = this._interactions;
|
|
85
|
-
|
|
86
|
-
// No interactions - not necessarily suspicious, could be passive viewing
|
|
87
|
-
if (interactions.length === 0) {
|
|
88
|
-
return this.createResult(false, {
|
|
89
|
-
reason: 'no-interactions',
|
|
90
|
-
}, 0);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Check time to first interaction
|
|
94
|
-
const firstInteraction = interactions[0];
|
|
95
|
-
|
|
96
|
-
// Suspiciously fast first interaction (< 100ms after page load)
|
|
97
|
-
if (firstInteraction.timeSinceLoad < 100) {
|
|
98
|
-
anomalies.push('instant-interaction');
|
|
99
|
-
confidence = Math.max(confidence, 0.9);
|
|
100
|
-
}
|
|
101
|
-
// Very fast interaction (< 300ms)
|
|
102
|
-
else if (firstInteraction.timeSinceLoad < 300) {
|
|
103
|
-
anomalies.push('very-fast-interaction');
|
|
104
|
-
confidence = Math.max(confidence, 0.6);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Analyze interaction intervals
|
|
108
|
-
if (interactions.length > 3) {
|
|
109
|
-
const intervals = [];
|
|
110
|
-
for (let i = 1; i < interactions.length; i++) {
|
|
111
|
-
intervals.push(interactions[i].t - interactions[i - 1].t);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
115
|
-
const variance = intervals.reduce((acc, t) =>
|
|
116
|
-
acc + Math.pow(t - avgInterval, 2), 0) / intervals.length;
|
|
117
|
-
|
|
118
|
-
// Perfectly timed intervals (robotic)
|
|
119
|
-
if (variance < 10 && interactions.length > 5) {
|
|
120
|
-
anomalies.push('robotic-intervals');
|
|
121
|
-
confidence = Math.max(confidence, 0.8);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check for burst interactions (many in short time)
|
|
125
|
-
const burstThreshold = 50; // ms
|
|
126
|
-
let burstCount = 0;
|
|
127
|
-
for (const interval of intervals) {
|
|
128
|
-
if (interval < burstThreshold) burstCount++;
|
|
129
|
-
}
|
|
130
|
-
if (burstCount > intervals.length * 0.7) {
|
|
131
|
-
anomalies.push('burst-interactions');
|
|
132
|
-
confidence = Math.max(confidence, 0.7);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Check interaction sequence (bots often follow predictable patterns)
|
|
137
|
-
const typeSequence = interactions.map(i => i.type).join(',');
|
|
138
|
-
|
|
139
|
-
// Repeated identical sequences
|
|
140
|
-
if (interactions.length >= 6) {
|
|
141
|
-
const halfLength = Math.floor(interactions.length / 2);
|
|
142
|
-
const firstHalf = interactions.slice(0, halfLength).map(i => i.type).join(',');
|
|
143
|
-
const secondHalf = interactions.slice(halfLength, halfLength * 2).map(i => i.type).join(',');
|
|
144
|
-
|
|
145
|
-
if (firstHalf === secondHalf && firstHalf.length > 0) {
|
|
146
|
-
anomalies.push('repeated-sequence');
|
|
147
|
-
confidence = Math.max(confidence, 0.6);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const triggered = anomalies.length > 0;
|
|
152
|
-
|
|
153
|
-
return this.createResult(triggered, {
|
|
154
|
-
anomalies,
|
|
155
|
-
interactionCount: interactions.length,
|
|
156
|
-
timeToFirstInteraction: firstInteraction.timeSinceLoad,
|
|
157
|
-
firstInteractionType: firstInteraction.type,
|
|
158
|
-
}, confidence);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
reset() {
|
|
162
|
-
super.reset();
|
|
163
|
-
this.stopTracking();
|
|
164
|
-
this._pageLoadTime = performance.now();
|
|
165
|
-
this._firstInteractionTime = null;
|
|
166
|
-
this._interactions = [];
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export { InteractionTimingSignal };
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects non-human keyboard input patterns.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Analyzes keystroke timing patterns.
|
|
9
|
-
* Bots often type with unnatural consistency or inhuman speeds.
|
|
10
|
-
*/
|
|
11
|
-
class KeyboardPatternSignal extends Signal {
|
|
12
|
-
static id = 'keyboard-pattern';
|
|
13
|
-
static category = 'behavior';
|
|
14
|
-
static weight = 0.8;
|
|
15
|
-
static description = 'Detects non-human keystroke patterns';
|
|
16
|
-
static requiresInteraction = true;
|
|
17
|
-
|
|
18
|
-
constructor(options = {}) {
|
|
19
|
-
super(options);
|
|
20
|
-
this._keystrokes = [];
|
|
21
|
-
this._isTracking = false;
|
|
22
|
-
this._trackingDuration = options.trackingDuration || 5000;
|
|
23
|
-
this._minKeystrokes = options.minKeystrokes || 10;
|
|
24
|
-
this._boundKeydownHandler = null;
|
|
25
|
-
this._boundKeyupHandler = null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Start tracking keyboard events.
|
|
30
|
-
*/
|
|
31
|
-
startTracking() {
|
|
32
|
-
if (this._isTracking) return;
|
|
33
|
-
|
|
34
|
-
this._keystrokes = [];
|
|
35
|
-
this._isTracking = true;
|
|
36
|
-
|
|
37
|
-
this._boundKeydownHandler = (e) => {
|
|
38
|
-
this._keystrokes.push({
|
|
39
|
-
type: 'down',
|
|
40
|
-
key: e.key,
|
|
41
|
-
code: e.code,
|
|
42
|
-
t: performance.now(),
|
|
43
|
-
});
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
this._boundKeyupHandler = (e) => {
|
|
47
|
-
this._keystrokes.push({
|
|
48
|
-
type: 'up',
|
|
49
|
-
key: e.key,
|
|
50
|
-
code: e.code,
|
|
51
|
-
t: performance.now(),
|
|
52
|
-
});
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
document.addEventListener('keydown', this._boundKeydownHandler, { passive: true });
|
|
56
|
-
document.addEventListener('keyup', this._boundKeyupHandler, { passive: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Stop tracking keyboard events.
|
|
61
|
-
*/
|
|
62
|
-
stopTracking() {
|
|
63
|
-
if (!this._isTracking) return;
|
|
64
|
-
|
|
65
|
-
this._isTracking = false;
|
|
66
|
-
if (this._boundKeydownHandler) {
|
|
67
|
-
document.removeEventListener('keydown', this._boundKeydownHandler);
|
|
68
|
-
this._boundKeydownHandler = null;
|
|
69
|
-
}
|
|
70
|
-
if (this._boundKeyupHandler) {
|
|
71
|
-
document.removeEventListener('keyup', this._boundKeyupHandler);
|
|
72
|
-
this._boundKeyupHandler = null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async detect() {
|
|
77
|
-
const anomalies = [];
|
|
78
|
-
let confidence = 0;
|
|
79
|
-
|
|
80
|
-
// If no tracking has occurred, start it
|
|
81
|
-
if (this._keystrokes.length === 0) {
|
|
82
|
-
this.startTracking();
|
|
83
|
-
await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
|
|
84
|
-
this.stopTracking();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const keystrokes = this._keystrokes;
|
|
88
|
-
const keydowns = keystrokes.filter(k => k.type === 'down');
|
|
89
|
-
|
|
90
|
-
// No keyboard activity
|
|
91
|
-
if (keydowns.length < this._minKeystrokes) {
|
|
92
|
-
// Not necessarily a bot - could just be no typing needed
|
|
93
|
-
return this.createResult(false, {
|
|
94
|
-
reason: 'insufficient-data',
|
|
95
|
-
keystrokes: keydowns.length
|
|
96
|
-
}, 0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const analysis = this._analyzeKeystrokes(keystrokes);
|
|
100
|
-
|
|
101
|
-
// Check for inhuman typing speed (> 20 chars/second sustained)
|
|
102
|
-
if (analysis.avgInterKeystrokeTime < 50 && keydowns.length > 20) {
|
|
103
|
-
anomalies.push('inhuman-speed');
|
|
104
|
-
confidence = Math.max(confidence, 0.9);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Check for too-consistent timing (robotic)
|
|
108
|
-
if (analysis.timingVariance < 5 && keydowns.length > 15) {
|
|
109
|
-
anomalies.push('robotic-timing');
|
|
110
|
-
confidence = Math.max(confidence, 0.8);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check for missing key-up events (programmatic input)
|
|
114
|
-
if (analysis.missingKeyups > keydowns.length * 0.5) {
|
|
115
|
-
anomalies.push('missing-keyups');
|
|
116
|
-
confidence = Math.max(confidence, 0.7);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Check for perfect key hold times
|
|
120
|
-
if (analysis.holdTimeVariance < 2 && analysis.holdTimes.length > 10) {
|
|
121
|
-
anomalies.push('constant-hold-time');
|
|
122
|
-
confidence = Math.max(confidence, 0.6);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Check for sequential key codes (batch input)
|
|
126
|
-
if (analysis.sequentialKeys > keydowns.length * 0.8 && keydowns.length > 10) {
|
|
127
|
-
anomalies.push('sequential-input');
|
|
128
|
-
confidence = Math.max(confidence, 0.5);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check for no typing rhythm variation
|
|
132
|
-
if (analysis.rhythmScore < 0.1 && keydowns.length > 20) {
|
|
133
|
-
anomalies.push('no-rhythm-variation');
|
|
134
|
-
confidence = Math.max(confidence, 0.6);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const triggered = anomalies.length > 0;
|
|
138
|
-
|
|
139
|
-
return this.createResult(triggered, {
|
|
140
|
-
anomalies,
|
|
141
|
-
keystrokeCount: keydowns.length,
|
|
142
|
-
analysis,
|
|
143
|
-
}, confidence);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Analyze keystroke patterns.
|
|
148
|
-
* @param {Array} keystrokes - Array of keystroke events
|
|
149
|
-
* @returns {Object} Analysis results
|
|
150
|
-
*/
|
|
151
|
-
_analyzeKeystrokes(keystrokes) {
|
|
152
|
-
const keydowns = keystrokes.filter(k => k.type === 'down');
|
|
153
|
-
const keyups = keystrokes.filter(k => k.type === 'up');
|
|
154
|
-
|
|
155
|
-
if (keydowns.length < 2) {
|
|
156
|
-
return {
|
|
157
|
-
avgInterKeystrokeTime: Infinity,
|
|
158
|
-
timingVariance: Infinity,
|
|
159
|
-
missingKeyups: 0,
|
|
160
|
-
holdTimeVariance: Infinity,
|
|
161
|
-
holdTimes: [],
|
|
162
|
-
sequentialKeys: 0,
|
|
163
|
-
rhythmScore: 1,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Calculate inter-keystroke times
|
|
168
|
-
const interTimes = [];
|
|
169
|
-
for (let i = 1; i < keydowns.length; i++) {
|
|
170
|
-
interTimes.push(keydowns[i].t - keydowns[i - 1].t);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const avgInterKeystrokeTime = interTimes.reduce((a, b) => a + b, 0) / interTimes.length;
|
|
174
|
-
const timingVariance = interTimes.reduce((acc, t) =>
|
|
175
|
-
acc + Math.pow(t - avgInterKeystrokeTime, 2), 0) / interTimes.length;
|
|
176
|
-
|
|
177
|
-
// Calculate hold times (time between keydown and keyup for same key)
|
|
178
|
-
const holdTimes = [];
|
|
179
|
-
for (const down of keydowns) {
|
|
180
|
-
const up = keyups.find(u => u.key === down.key && u.t > down.t);
|
|
181
|
-
if (up) {
|
|
182
|
-
holdTimes.push(up.t - down.t);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const avgHoldTime = holdTimes.length > 0
|
|
187
|
-
? holdTimes.reduce((a, b) => a + b, 0) / holdTimes.length
|
|
188
|
-
: 0;
|
|
189
|
-
const holdTimeVariance = holdTimes.length > 0
|
|
190
|
-
? holdTimes.reduce((acc, t) => acc + Math.pow(t - avgHoldTime, 2), 0) / holdTimes.length
|
|
191
|
-
: Infinity;
|
|
192
|
-
|
|
193
|
-
// Count missing keyups
|
|
194
|
-
const missingKeyups = keydowns.length - holdTimes.length;
|
|
195
|
-
|
|
196
|
-
// Count sequential keys (chars typed in order, like 'abc' or '123')
|
|
197
|
-
let sequentialKeys = 0;
|
|
198
|
-
for (let i = 1; i < keydowns.length; i++) {
|
|
199
|
-
const prevCode = keydowns[i - 1].key.charCodeAt(0);
|
|
200
|
-
const currCode = keydowns[i].key.charCodeAt(0);
|
|
201
|
-
if (Math.abs(currCode - prevCode) === 1) {
|
|
202
|
-
sequentialKeys++;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Calculate typing rhythm score (variation in timing patterns)
|
|
207
|
-
// Humans have natural rhythm variations (pause after words, faster for common patterns)
|
|
208
|
-
let rhythmScore = 0;
|
|
209
|
-
if (interTimes.length > 5) {
|
|
210
|
-
const sortedTimes = [...interTimes].sort((a, b) => a - b);
|
|
211
|
-
const median = sortedTimes[Math.floor(sortedTimes.length / 2)];
|
|
212
|
-
// Count how many timings deviate significantly from median
|
|
213
|
-
const deviations = interTimes.filter(t => Math.abs(t - median) > median * 0.3).length;
|
|
214
|
-
rhythmScore = deviations / interTimes.length;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
avgInterKeystrokeTime,
|
|
219
|
-
timingVariance,
|
|
220
|
-
missingKeyups,
|
|
221
|
-
holdTimeVariance,
|
|
222
|
-
holdTimes,
|
|
223
|
-
sequentialKeys,
|
|
224
|
-
rhythmScore,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
reset() {
|
|
229
|
-
super.reset();
|
|
230
|
-
this.stopTracking();
|
|
231
|
-
this._keystrokes = [];
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export { KeyboardPatternSignal };
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects non-human mouse movement patterns.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Tracks and analyzes mouse movement patterns.
|
|
9
|
-
* Bots often have perfectly linear paths, no movement, or teleportation.
|
|
10
|
-
*/
|
|
11
|
-
class MouseMovementSignal extends Signal {
|
|
12
|
-
static id = 'mouse-movement';
|
|
13
|
-
static category = 'behavior';
|
|
14
|
-
static weight = 0.9;
|
|
15
|
-
static description = 'Detects non-human mouse movement patterns';
|
|
16
|
-
static requiresInteraction = true;
|
|
17
|
-
|
|
18
|
-
constructor(options = {}) {
|
|
19
|
-
super(options);
|
|
20
|
-
this._movements = [];
|
|
21
|
-
this._isTracking = false;
|
|
22
|
-
this._trackingDuration = options.trackingDuration || 3000; // Default 3 seconds
|
|
23
|
-
this._minMovements = options.minMovements || 5;
|
|
24
|
-
this._boundHandler = null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Start tracking mouse movements.
|
|
29
|
-
* @returns {Promise<void>}
|
|
30
|
-
*/
|
|
31
|
-
startTracking() {
|
|
32
|
-
if (this._isTracking) return;
|
|
33
|
-
|
|
34
|
-
this._movements = [];
|
|
35
|
-
this._isTracking = true;
|
|
36
|
-
|
|
37
|
-
this._boundHandler = (e) => {
|
|
38
|
-
this._movements.push({
|
|
39
|
-
x: e.clientX,
|
|
40
|
-
y: e.clientY,
|
|
41
|
-
t: performance.now(),
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
document.addEventListener('mousemove', this._boundHandler, { passive: true });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Stop tracking mouse movements.
|
|
50
|
-
*/
|
|
51
|
-
stopTracking() {
|
|
52
|
-
if (!this._isTracking) return;
|
|
53
|
-
|
|
54
|
-
this._isTracking = false;
|
|
55
|
-
if (this._boundHandler) {
|
|
56
|
-
document.removeEventListener('mousemove', this._boundHandler);
|
|
57
|
-
this._boundHandler = null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async detect() {
|
|
62
|
-
const anomalies = [];
|
|
63
|
-
let confidence = 0;
|
|
64
|
-
|
|
65
|
-
// If no tracking has occurred, check if we have movement data
|
|
66
|
-
if (this._movements.length === 0) {
|
|
67
|
-
// Start tracking for a period, then analyze
|
|
68
|
-
this.startTracking();
|
|
69
|
-
|
|
70
|
-
await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
|
|
71
|
-
|
|
72
|
-
this.stopTracking();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const movements = this._movements;
|
|
76
|
-
|
|
77
|
-
// No mouse movements detected
|
|
78
|
-
if (movements.length < this._minMovements) {
|
|
79
|
-
anomalies.push('no-mouse-movement');
|
|
80
|
-
confidence = Math.max(confidence, 0.6);
|
|
81
|
-
return this.createResult(true, { anomalies, movements: movements.length }, confidence);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Analyze movement patterns
|
|
85
|
-
const analysis = this._analyzeMovements(movements);
|
|
86
|
-
|
|
87
|
-
// Check for teleportation (large instant jumps)
|
|
88
|
-
if (analysis.teleportCount > 0) {
|
|
89
|
-
anomalies.push('mouse-teleportation');
|
|
90
|
-
confidence = Math.max(confidence, 0.7);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Check for perfect linear paths
|
|
94
|
-
if (analysis.linearPathRatio > 0.9) {
|
|
95
|
-
anomalies.push('linear-path');
|
|
96
|
-
confidence = Math.max(confidence, 0.8);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Check for constant velocity (too perfect)
|
|
100
|
-
if (analysis.velocityVariance < 0.01 && movements.length > 10) {
|
|
101
|
-
anomalies.push('constant-velocity');
|
|
102
|
-
confidence = Math.max(confidence, 0.7);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check for zero acceleration changes
|
|
106
|
-
if (analysis.accelerationChanges === 0 && movements.length > 10) {
|
|
107
|
-
anomalies.push('no-acceleration-variance');
|
|
108
|
-
confidence = Math.max(confidence, 0.6);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Check for robotic timing (perfect intervals)
|
|
112
|
-
if (analysis.timingVariance < 1 && movements.length > 10) {
|
|
113
|
-
anomalies.push('robotic-timing');
|
|
114
|
-
confidence = Math.max(confidence, 0.8);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const triggered = anomalies.length > 0;
|
|
118
|
-
|
|
119
|
-
return this.createResult(triggered, {
|
|
120
|
-
anomalies,
|
|
121
|
-
movementCount: movements.length,
|
|
122
|
-
analysis,
|
|
123
|
-
}, confidence);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Analyze movement patterns for anomalies.
|
|
128
|
-
* @param {Array} movements - Array of movement points
|
|
129
|
-
* @returns {Object} Analysis results
|
|
130
|
-
*/
|
|
131
|
-
_analyzeMovements(movements) {
|
|
132
|
-
if (movements.length < 3) {
|
|
133
|
-
return {
|
|
134
|
-
teleportCount: 0,
|
|
135
|
-
linearPathRatio: 0,
|
|
136
|
-
velocityVariance: 0,
|
|
137
|
-
accelerationChanges: 0,
|
|
138
|
-
timingVariance: 0,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let teleportCount = 0;
|
|
143
|
-
const velocities = [];
|
|
144
|
-
const angles = [];
|
|
145
|
-
const timeIntervals = [];
|
|
146
|
-
|
|
147
|
-
for (let i = 1; i < movements.length; i++) {
|
|
148
|
-
const prev = movements[i - 1];
|
|
149
|
-
const curr = movements[i];
|
|
150
|
-
|
|
151
|
-
const dx = curr.x - prev.x;
|
|
152
|
-
const dy = curr.y - prev.y;
|
|
153
|
-
const dt = curr.t - prev.t;
|
|
154
|
-
|
|
155
|
-
if (dt === 0) continue;
|
|
156
|
-
|
|
157
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
158
|
-
const velocity = distance / dt;
|
|
159
|
-
|
|
160
|
-
velocities.push(velocity);
|
|
161
|
-
angles.push(Math.atan2(dy, dx));
|
|
162
|
-
timeIntervals.push(dt);
|
|
163
|
-
|
|
164
|
-
// Teleportation: large jump in very short time
|
|
165
|
-
if (distance > 300 && dt < 10) {
|
|
166
|
-
teleportCount++;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Calculate velocity variance
|
|
171
|
-
const avgVelocity = velocities.reduce((a, b) => a + b, 0) / velocities.length;
|
|
172
|
-
const velocityVariance = velocities.reduce((acc, v) =>
|
|
173
|
-
acc + Math.pow(v - avgVelocity, 2), 0) / velocities.length;
|
|
174
|
-
|
|
175
|
-
// Calculate angle consistency (linear path detection)
|
|
176
|
-
let angleConsistency = 0;
|
|
177
|
-
if (angles.length > 1) {
|
|
178
|
-
let consistentAngles = 0;
|
|
179
|
-
for (let i = 1; i < angles.length; i++) {
|
|
180
|
-
const angleDiff = Math.abs(angles[i] - angles[i - 1]);
|
|
181
|
-
if (angleDiff < 0.1) consistentAngles++;
|
|
182
|
-
}
|
|
183
|
-
angleConsistency = consistentAngles / (angles.length - 1);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Calculate timing variance
|
|
187
|
-
const avgInterval = timeIntervals.reduce((a, b) => a + b, 0) / timeIntervals.length;
|
|
188
|
-
const timingVariance = timeIntervals.reduce((acc, t) =>
|
|
189
|
-
acc + Math.pow(t - avgInterval, 2), 0) / timeIntervals.length;
|
|
190
|
-
|
|
191
|
-
// Count acceleration changes
|
|
192
|
-
let accelerationChanges = 0;
|
|
193
|
-
for (let i = 1; i < velocities.length; i++) {
|
|
194
|
-
if ((velocities[i] - velocities[i - 1]) * (velocities[i - 1] - (velocities[i - 2] || 0)) < 0) {
|
|
195
|
-
accelerationChanges++;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
teleportCount,
|
|
201
|
-
linearPathRatio: angleConsistency,
|
|
202
|
-
velocityVariance,
|
|
203
|
-
accelerationChanges,
|
|
204
|
-
timingVariance,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
reset() {
|
|
209
|
-
super.reset();
|
|
210
|
-
this.stopTracking();
|
|
211
|
-
this._movements = [];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export { MouseMovementSignal };
|