@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,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects non-human scroll patterns.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Tracks and analyzes scroll behavior patterns.
|
|
9
|
-
* Detects programmatic scrolling and non-human scroll patterns.
|
|
10
|
-
*/
|
|
11
|
-
class ScrollBehaviorSignal extends Signal {
|
|
12
|
-
static id = 'scroll-behavior';
|
|
13
|
-
static category = 'behavior';
|
|
14
|
-
static weight = 0.5;
|
|
15
|
-
static description = 'Detects programmatic scroll patterns';
|
|
16
|
-
static requiresInteraction = true;
|
|
17
|
-
|
|
18
|
-
constructor(options = {}) {
|
|
19
|
-
super(options);
|
|
20
|
-
this._scrollEvents = [];
|
|
21
|
-
this._isTracking = false;
|
|
22
|
-
this._trackingDuration = options.trackingDuration || 3000;
|
|
23
|
-
this._boundHandler = null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Start tracking scroll events.
|
|
28
|
-
*/
|
|
29
|
-
startTracking() {
|
|
30
|
-
if (this._isTracking) return;
|
|
31
|
-
|
|
32
|
-
this._scrollEvents = [];
|
|
33
|
-
this._isTracking = true;
|
|
34
|
-
|
|
35
|
-
this._boundHandler = () => {
|
|
36
|
-
this._scrollEvents.push({
|
|
37
|
-
scrollY: window.scrollY,
|
|
38
|
-
scrollX: window.scrollX,
|
|
39
|
-
t: performance.now(),
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
window.addEventListener('scroll', this._boundHandler, { passive: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Stop tracking scroll events.
|
|
48
|
-
*/
|
|
49
|
-
stopTracking() {
|
|
50
|
-
if (!this._isTracking) return;
|
|
51
|
-
|
|
52
|
-
this._isTracking = false;
|
|
53
|
-
if (this._boundHandler) {
|
|
54
|
-
window.removeEventListener('scroll', this._boundHandler);
|
|
55
|
-
this._boundHandler = null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async detect() {
|
|
60
|
-
const anomalies = [];
|
|
61
|
-
let confidence = 0;
|
|
62
|
-
|
|
63
|
-
// Start tracking if needed
|
|
64
|
-
if (this._scrollEvents.length === 0) {
|
|
65
|
-
this.startTracking();
|
|
66
|
-
await new Promise(resolve => setTimeout(resolve, this._trackingDuration));
|
|
67
|
-
this.stopTracking();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const events = this._scrollEvents;
|
|
71
|
-
|
|
72
|
-
// No scroll events - not necessarily suspicious
|
|
73
|
-
if (events.length < 3) {
|
|
74
|
-
return this.createResult(false, {
|
|
75
|
-
reason: 'insufficient-scroll-data',
|
|
76
|
-
scrollEvents: events.length,
|
|
77
|
-
}, 0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const analysis = this._analyzeScrollPatterns(events);
|
|
81
|
-
|
|
82
|
-
// Check for instant jumps (scrollTo without animation)
|
|
83
|
-
if (analysis.instantJumps > 0) {
|
|
84
|
-
anomalies.push('instant-scroll-jumps');
|
|
85
|
-
confidence = Math.max(confidence, 0.7);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Check for perfectly consistent scroll speed
|
|
89
|
-
if (analysis.velocityVariance < 0.1 && events.length > 10) {
|
|
90
|
-
anomalies.push('constant-scroll-velocity');
|
|
91
|
-
confidence = Math.max(confidence, 0.6);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Check for no momentum (instant stops)
|
|
95
|
-
if (analysis.momentumEvents === 0 && events.length > 5) {
|
|
96
|
-
anomalies.push('no-scroll-momentum');
|
|
97
|
-
confidence = Math.max(confidence, 0.5);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Check for perfectly even scroll intervals
|
|
101
|
-
if (analysis.intervalVariance < 5 && events.length > 10) {
|
|
102
|
-
anomalies.push('robotic-scroll-timing');
|
|
103
|
-
confidence = Math.max(confidence, 0.7);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check for exclusively vertical or horizontal scroll
|
|
107
|
-
if (analysis.scrollDirections === 1 && Math.abs(analysis.totalScrollY) > 1000) {
|
|
108
|
-
// Could be normal, but combined with other factors is suspicious
|
|
109
|
-
if (analysis.velocityVariance < 1) {
|
|
110
|
-
anomalies.push('one-dimensional-scroll');
|
|
111
|
-
confidence = Math.max(confidence, 0.4);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Check for scroll-to-element patterns (exact positions)
|
|
116
|
-
if (analysis.exactPositionScrolls > 2) {
|
|
117
|
-
anomalies.push('exact-position-scrolls');
|
|
118
|
-
confidence = Math.max(confidence, 0.6);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const triggered = anomalies.length > 0;
|
|
122
|
-
|
|
123
|
-
return this.createResult(triggered, {
|
|
124
|
-
anomalies,
|
|
125
|
-
scrollEventCount: events.length,
|
|
126
|
-
analysis,
|
|
127
|
-
}, confidence);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Analyze scroll patterns for anomalies.
|
|
132
|
-
* @param {Array} events - Array of scroll events
|
|
133
|
-
* @returns {Object} Analysis results
|
|
134
|
-
*/
|
|
135
|
-
_analyzeScrollPatterns(events) {
|
|
136
|
-
if (events.length < 2) {
|
|
137
|
-
return {
|
|
138
|
-
instantJumps: 0,
|
|
139
|
-
velocityVariance: 0,
|
|
140
|
-
momentumEvents: 0,
|
|
141
|
-
intervalVariance: 0,
|
|
142
|
-
scrollDirections: 0,
|
|
143
|
-
totalScrollY: 0,
|
|
144
|
-
exactPositionScrolls: 0,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let instantJumps = 0;
|
|
149
|
-
let momentumEvents = 0;
|
|
150
|
-
const velocities = [];
|
|
151
|
-
const intervals = [];
|
|
152
|
-
let hasVertical = false;
|
|
153
|
-
let hasHorizontal = false;
|
|
154
|
-
let exactPositionScrolls = 0;
|
|
155
|
-
|
|
156
|
-
// Common scroll targets (element heights, percentages)
|
|
157
|
-
const commonPositions = [0, 100, 200, 300, 400, 500, 600, 800, 1000];
|
|
158
|
-
|
|
159
|
-
for (let i = 1; i < events.length; i++) {
|
|
160
|
-
const prev = events[i - 1];
|
|
161
|
-
const curr = events[i];
|
|
162
|
-
|
|
163
|
-
const dy = curr.scrollY - prev.scrollY;
|
|
164
|
-
const dx = curr.scrollX - prev.scrollX;
|
|
165
|
-
const dt = curr.t - prev.t;
|
|
166
|
-
|
|
167
|
-
intervals.push(dt);
|
|
168
|
-
|
|
169
|
-
if (Math.abs(dy) > 0) hasVertical = true;
|
|
170
|
-
if (Math.abs(dx) > 0) hasHorizontal = true;
|
|
171
|
-
|
|
172
|
-
if (dt === 0) continue;
|
|
173
|
-
|
|
174
|
-
const velocity = Math.sqrt(dy * dy + dx * dx) / dt;
|
|
175
|
-
velocities.push(velocity);
|
|
176
|
-
|
|
177
|
-
// Instant jump: large distance in very short time (< 16ms, one frame)
|
|
178
|
-
const distance = Math.abs(dy) + Math.abs(dx);
|
|
179
|
-
if (distance > 200 && dt < 20) {
|
|
180
|
-
instantJumps++;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Momentum: decreasing velocity (natural scroll deceleration)
|
|
184
|
-
if (i > 1 && velocities.length > 1) {
|
|
185
|
-
const prevVelocity = velocities[velocities.length - 2];
|
|
186
|
-
if (velocity < prevVelocity * 0.9 && velocity > 0) {
|
|
187
|
-
momentumEvents++;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Check for exact positions
|
|
192
|
-
if (commonPositions.includes(Math.round(curr.scrollY))) {
|
|
193
|
-
exactPositionScrolls++;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Calculate velocity variance
|
|
198
|
-
const avgVelocity = velocities.length > 0
|
|
199
|
-
? velocities.reduce((a, b) => a + b, 0) / velocities.length
|
|
200
|
-
: 0;
|
|
201
|
-
const velocityVariance = velocities.length > 0
|
|
202
|
-
? velocities.reduce((acc, v) => acc + Math.pow(v - avgVelocity, 2), 0) / velocities.length
|
|
203
|
-
: 0;
|
|
204
|
-
|
|
205
|
-
// Calculate interval variance
|
|
206
|
-
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
207
|
-
const intervalVariance = intervals.reduce((acc, t) =>
|
|
208
|
-
acc + Math.pow(t - avgInterval, 2), 0) / intervals.length;
|
|
209
|
-
|
|
210
|
-
// Count scroll directions
|
|
211
|
-
let scrollDirections = 0;
|
|
212
|
-
if (hasVertical) scrollDirections++;
|
|
213
|
-
if (hasHorizontal) scrollDirections++;
|
|
214
|
-
|
|
215
|
-
// Total scroll distance
|
|
216
|
-
const totalScrollY = events[events.length - 1].scrollY - events[0].scrollY;
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
instantJumps,
|
|
220
|
-
velocityVariance,
|
|
221
|
-
momentumEvents,
|
|
222
|
-
intervalVariance,
|
|
223
|
-
scrollDirections,
|
|
224
|
-
totalScrollY,
|
|
225
|
-
exactPositionScrolls,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
reset() {
|
|
230
|
-
super.reset();
|
|
231
|
-
this.stopTracking();
|
|
232
|
-
this._scrollEvents = [];
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export { ScrollBehaviorSignal };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Behavioral signals index.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { MouseMovementSignal } from './MouseMovementSignal.js';
|
|
6
|
-
export { KeyboardPatternSignal } from './KeyboardPatternSignal.js';
|
|
7
|
-
export { InteractionTimingSignal } from './InteractionTimingSignal.js';
|
|
8
|
-
export { ScrollBehaviorSignal } from './ScrollBehaviorSignal.js';
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects headless browser indicators.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects indicators of headless browser execution.
|
|
9
|
-
* Headless browsers often have missing or inconsistent features.
|
|
10
|
-
*/
|
|
11
|
-
class HeadlessSignal extends Signal {
|
|
12
|
-
static id = 'headless';
|
|
13
|
-
static category = 'environment';
|
|
14
|
-
static weight = 0.8;
|
|
15
|
-
static description = 'Detects headless browser indicators';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const indicators = [];
|
|
19
|
-
let confidence = 0;
|
|
20
|
-
|
|
21
|
-
// Check for HeadlessChrome in user agent
|
|
22
|
-
const ua = navigator.userAgent || '';
|
|
23
|
-
if (ua.includes('HeadlessChrome')) {
|
|
24
|
-
indicators.push('headless-ua');
|
|
25
|
-
confidence = Math.max(confidence, 1.0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Check for missing chrome.runtime in Chrome
|
|
29
|
-
if (ua.includes('Chrome') && !ua.includes('Chromium')) {
|
|
30
|
-
if (typeof window.chrome === 'undefined') {
|
|
31
|
-
indicators.push('missing-chrome-object');
|
|
32
|
-
confidence = Math.max(confidence, 0.6);
|
|
33
|
-
} else if (!window.chrome.runtime) {
|
|
34
|
-
indicators.push('missing-chrome-runtime');
|
|
35
|
-
confidence = Math.max(confidence, 0.4);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Check for empty plugins (common in headless)
|
|
40
|
-
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
41
|
-
indicators.push('no-plugins');
|
|
42
|
-
confidence = Math.max(confidence, 0.5);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Check for missing languages
|
|
46
|
-
if (!navigator.languages || navigator.languages.length === 0) {
|
|
47
|
-
indicators.push('no-languages');
|
|
48
|
-
confidence = Math.max(confidence, 0.6);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check window dimensions anomalies
|
|
52
|
-
if (window.outerWidth === 0 && window.outerHeight === 0) {
|
|
53
|
-
indicators.push('zero-outer-dimensions');
|
|
54
|
-
confidence = Math.max(confidence, 0.7);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check for missing connection info
|
|
58
|
-
if (typeof navigator.connection === 'undefined' && ua.includes('Chrome')) {
|
|
59
|
-
indicators.push('missing-connection-api');
|
|
60
|
-
confidence = Math.max(confidence, 0.3);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Check for Notification API permission inconsistency
|
|
64
|
-
try {
|
|
65
|
-
if (typeof Notification !== 'undefined' && Notification.permission === 'denied' &&
|
|
66
|
-
window.outerWidth === 0) {
|
|
67
|
-
indicators.push('notification-headless-pattern');
|
|
68
|
-
confidence = Math.max(confidence, 0.5);
|
|
69
|
-
}
|
|
70
|
-
} catch (e) {
|
|
71
|
-
// Ignore errors
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Phantom.js specific check
|
|
75
|
-
if (window.callPhantom || window._phantom) {
|
|
76
|
-
indicators.push('phantomjs');
|
|
77
|
-
confidence = Math.max(confidence, 1.0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Nightmare.js check
|
|
81
|
-
if (window.__nightmare) {
|
|
82
|
-
indicators.push('nightmare');
|
|
83
|
-
confidence = Math.max(confidence, 1.0);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const triggered = indicators.length > 0;
|
|
87
|
-
|
|
88
|
-
// Increase confidence if multiple indicators are present
|
|
89
|
-
if (indicators.length >= 3) {
|
|
90
|
-
confidence = Math.min(1.0, confidence + 0.2);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return this.createResult(triggered, { indicators }, confidence);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export { HeadlessSignal };
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects navigator property anomalies and inconsistencies.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects inconsistencies in navigator properties.
|
|
9
|
-
* Bots often have mismatched or spoofed navigator values.
|
|
10
|
-
*/
|
|
11
|
-
class NavigatorAnomalySignal extends Signal {
|
|
12
|
-
static id = 'navigator-anomaly';
|
|
13
|
-
static category = 'environment';
|
|
14
|
-
static weight = 0.7;
|
|
15
|
-
static description = 'Detects navigator property inconsistencies';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const anomalies = [];
|
|
19
|
-
let totalScore = 0;
|
|
20
|
-
let checksPerformed = 0;
|
|
21
|
-
|
|
22
|
-
const ua = navigator.userAgent || '';
|
|
23
|
-
const platform = navigator.platform || '';
|
|
24
|
-
|
|
25
|
-
// Platform vs UserAgent consistency check
|
|
26
|
-
checksPerformed++;
|
|
27
|
-
if (platform.includes('Win') && !ua.includes('Windows')) {
|
|
28
|
-
anomalies.push('platform-ua-mismatch-windows');
|
|
29
|
-
totalScore += 1;
|
|
30
|
-
} else if (platform.includes('Mac') && !ua.includes('Mac')) {
|
|
31
|
-
anomalies.push('platform-ua-mismatch-mac');
|
|
32
|
-
totalScore += 1;
|
|
33
|
-
} else if (platform.includes('Linux') && !ua.includes('Linux') && !ua.includes('Android')) {
|
|
34
|
-
anomalies.push('platform-ua-mismatch-linux');
|
|
35
|
-
totalScore += 1;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Check for empty or suspicious platform
|
|
39
|
-
checksPerformed++;
|
|
40
|
-
if (!platform || platform === '' || platform === 'undefined') {
|
|
41
|
-
anomalies.push('empty-platform');
|
|
42
|
-
totalScore += 1;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Language consistency
|
|
46
|
-
checksPerformed++;
|
|
47
|
-
if (navigator.language && navigator.languages) {
|
|
48
|
-
if (!navigator.languages.includes(navigator.language)) {
|
|
49
|
-
anomalies.push('language-mismatch');
|
|
50
|
-
totalScore += 0.5;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check vendor consistency with browser
|
|
55
|
-
checksPerformed++;
|
|
56
|
-
if (ua.includes('Chrome') && navigator.vendor !== 'Google Inc.') {
|
|
57
|
-
anomalies.push('vendor-mismatch-chrome');
|
|
58
|
-
totalScore += 0.5;
|
|
59
|
-
} else if (ua.includes('Firefox') && navigator.vendor !== '') {
|
|
60
|
-
anomalies.push('vendor-mismatch-firefox');
|
|
61
|
-
totalScore += 0.5;
|
|
62
|
-
} else if (ua.includes('Safari') && !ua.includes('Chrome') &&
|
|
63
|
-
navigator.vendor !== 'Apple Computer, Inc.') {
|
|
64
|
-
anomalies.push('vendor-mismatch-safari');
|
|
65
|
-
totalScore += 0.5;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check for hardwareConcurrency anomaly
|
|
69
|
-
checksPerformed++;
|
|
70
|
-
if (typeof navigator.hardwareConcurrency !== 'undefined') {
|
|
71
|
-
if (navigator.hardwareConcurrency === 0 || navigator.hardwareConcurrency > 128) {
|
|
72
|
-
anomalies.push('suspicious-hardware-concurrency');
|
|
73
|
-
totalScore += 0.5;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check deviceMemory if available
|
|
78
|
-
checksPerformed++;
|
|
79
|
-
if (typeof navigator.deviceMemory !== 'undefined') {
|
|
80
|
-
if (navigator.deviceMemory === 0 || navigator.deviceMemory > 512) {
|
|
81
|
-
anomalies.push('suspicious-device-memory');
|
|
82
|
-
totalScore += 0.5;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Check maxTouchPoints consistency
|
|
87
|
-
checksPerformed++;
|
|
88
|
-
const isMobileUA = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
|
|
89
|
-
const hasTouchPoints = navigator.maxTouchPoints > 0;
|
|
90
|
-
|
|
91
|
-
// Desktop claiming touch or mobile with no touch
|
|
92
|
-
if (!isMobileUA && navigator.maxTouchPoints > 5) {
|
|
93
|
-
anomalies.push('desktop-high-touch-points');
|
|
94
|
-
totalScore += 0.3;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Check for overridden properties (common in spoofing)
|
|
98
|
-
checksPerformed++;
|
|
99
|
-
try {
|
|
100
|
-
const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent');
|
|
101
|
-
if (desc && desc.get && desc.get.toString().includes('native code') === false) {
|
|
102
|
-
anomalies.push('spoofed-user-agent');
|
|
103
|
-
totalScore += 1;
|
|
104
|
-
}
|
|
105
|
-
} catch (e) {
|
|
106
|
-
// Ignore errors
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Calculate confidence based on number and severity of anomalies
|
|
110
|
-
const triggered = anomalies.length > 0;
|
|
111
|
-
const confidence = Math.min(1, totalScore / Math.max(1, checksPerformed));
|
|
112
|
-
|
|
113
|
-
return this.createResult(triggered, { anomalies }, confidence);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export { NavigatorAnomalySignal };
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects permissions API anomalies.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects anomalies in the Permissions API.
|
|
9
|
-
* Automation tools often have inconsistent permission states.
|
|
10
|
-
*/
|
|
11
|
-
class PermissionsSignal extends Signal {
|
|
12
|
-
static id = 'permissions';
|
|
13
|
-
static category = 'environment';
|
|
14
|
-
static weight = 0.5;
|
|
15
|
-
static description = 'Detects Permissions API anomalies';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const anomalies = [];
|
|
19
|
-
|
|
20
|
-
// Check if Permissions API exists
|
|
21
|
-
if (!navigator.permissions) {
|
|
22
|
-
// Not an anomaly, just not supported
|
|
23
|
-
return this.createResult(false, { supported: false }, 0);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// Check notification permission consistency
|
|
28
|
-
const notificationStatus = await navigator.permissions.query({ name: 'notifications' });
|
|
29
|
-
|
|
30
|
-
if (typeof Notification !== 'undefined') {
|
|
31
|
-
const directPermission = Notification.permission;
|
|
32
|
-
|
|
33
|
-
// Check for mismatch
|
|
34
|
-
if ((directPermission === 'granted' && notificationStatus.state !== 'granted') ||
|
|
35
|
-
(directPermission === 'denied' && notificationStatus.state !== 'denied') ||
|
|
36
|
-
(directPermission === 'default' && notificationStatus.state !== 'prompt')) {
|
|
37
|
-
anomalies.push('notification-permission-mismatch');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check geolocation permission if available
|
|
42
|
-
try {
|
|
43
|
-
const geoStatus = await navigator.permissions.query({ name: 'geolocation' });
|
|
44
|
-
// In automation, geolocation is often pre-denied without user action
|
|
45
|
-
if (geoStatus.state === 'denied' && window.outerWidth === 0) {
|
|
46
|
-
anomalies.push('geo-denied-headless');
|
|
47
|
-
}
|
|
48
|
-
} catch (e) {
|
|
49
|
-
// Geolocation permission query may not be supported
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Check if permission.query throws on valid permission
|
|
53
|
-
try {
|
|
54
|
-
await navigator.permissions.query({ name: 'camera' });
|
|
55
|
-
} catch (e) {
|
|
56
|
-
// Some headless browsers don't support camera permission
|
|
57
|
-
if (e.name === 'TypeError') {
|
|
58
|
-
anomalies.push('camera-permission-error');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
} catch (e) {
|
|
63
|
-
// If permissions query throws unexpectedly, that's suspicious
|
|
64
|
-
if (e.name !== 'TypeError') {
|
|
65
|
-
anomalies.push('permissions-query-error');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const triggered = anomalies.length > 0;
|
|
70
|
-
const confidence = Math.min(1, anomalies.length * 0.4);
|
|
71
|
-
|
|
72
|
-
return this.createResult(triggered, { anomalies }, confidence);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export { PermissionsSignal };
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects navigator.webdriver property.
|
|
3
|
-
* This is the most reliable indicator of WebDriver-controlled browsers.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Signal } from '../../core/Signal.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Detects the presence of navigator.webdriver property.
|
|
10
|
-
* This property is set to true by automation frameworks like Selenium, Puppeteer, and Playwright.
|
|
11
|
-
*/
|
|
12
|
-
class WebDriverSignal extends Signal {
|
|
13
|
-
static id = 'webdriver';
|
|
14
|
-
static category = 'environment';
|
|
15
|
-
static weight = 1.0;
|
|
16
|
-
static description = 'Detects navigator.webdriver automation flag';
|
|
17
|
-
|
|
18
|
-
async detect() {
|
|
19
|
-
// Direct check
|
|
20
|
-
if (navigator.webdriver === true) {
|
|
21
|
-
return this.createResult(true, { webdriver: true }, 1.0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Check if property exists but is hidden/modified
|
|
25
|
-
const descriptor = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
|
|
26
|
-
if (descriptor) {
|
|
27
|
-
// Property exists - check if it's been tampered with
|
|
28
|
-
if (descriptor.get || !descriptor.configurable) {
|
|
29
|
-
return this.createResult(true, {
|
|
30
|
-
webdriver: 'modified',
|
|
31
|
-
descriptor: {
|
|
32
|
-
configurable: descriptor.configurable,
|
|
33
|
-
enumerable: descriptor.enumerable,
|
|
34
|
-
hasGetter: !!descriptor.get,
|
|
35
|
-
}
|
|
36
|
-
}, 0.8);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check prototype chain for webdriver
|
|
41
|
-
try {
|
|
42
|
-
const proto = Object.getPrototypeOf(navigator);
|
|
43
|
-
const protoDescriptor = Object.getOwnPropertyDescriptor(proto, 'webdriver');
|
|
44
|
-
if (protoDescriptor && protoDescriptor.get) {
|
|
45
|
-
const value = protoDescriptor.get.call(navigator);
|
|
46
|
-
if (value === true) {
|
|
47
|
-
return this.createResult(true, { webdriver: true, source: 'prototype' }, 1.0);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} catch (e) {
|
|
51
|
-
// Some environments may throw on prototype access
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return this.createResult(false);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export { WebDriverSignal };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Environment signals index.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { WebDriverSignal } from './WebDriverSignal.js';
|
|
6
|
-
export { HeadlessSignal } from './HeadlessSignal.js';
|
|
7
|
-
export { NavigatorAnomalySignal } from './NavigatorAnomalySignal.js';
|
|
8
|
-
export { PermissionsSignal } from './PermissionsSignal.js';
|