@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.
Files changed (38) hide show
  1. package/README.md +245 -0
  2. package/dist/bot-detector.cjs.js +2629 -0
  3. package/dist/bot-detector.cjs.js.map +7 -0
  4. package/dist/bot-detector.esm.js +2609 -0
  5. package/dist/bot-detector.esm.js.map +7 -0
  6. package/dist/bot-detector.iife.js +2631 -0
  7. package/dist/bot-detector.iife.js.map +7 -0
  8. package/dist/bot-detector.iife.min.js +1 -0
  9. package/package.json +57 -0
  10. package/src/core/BotDetector.js +284 -0
  11. package/src/core/ScoringEngine.js +134 -0
  12. package/src/core/Signal.js +181 -0
  13. package/src/core/VerdictEngine.js +132 -0
  14. package/src/index.js +273 -0
  15. package/src/signals/automation/PhantomJSSignal.js +137 -0
  16. package/src/signals/automation/PlaywrightSignal.js +129 -0
  17. package/src/signals/automation/PuppeteerSignal.js +117 -0
  18. package/src/signals/automation/SeleniumSignal.js +151 -0
  19. package/src/signals/automation/index.js +8 -0
  20. package/src/signals/behavior/InteractionTimingSignal.js +170 -0
  21. package/src/signals/behavior/KeyboardPatternSignal.js +235 -0
  22. package/src/signals/behavior/MouseMovementSignal.js +215 -0
  23. package/src/signals/behavior/ScrollBehaviorSignal.js +236 -0
  24. package/src/signals/behavior/index.js +8 -0
  25. package/src/signals/environment/HeadlessSignal.js +97 -0
  26. package/src/signals/environment/NavigatorAnomalySignal.js +117 -0
  27. package/src/signals/environment/PermissionsSignal.js +76 -0
  28. package/src/signals/environment/WebDriverSignal.js +58 -0
  29. package/src/signals/environment/index.js +8 -0
  30. package/src/signals/fingerprint/AudioContextSignal.js +158 -0
  31. package/src/signals/fingerprint/CanvasSignal.js +133 -0
  32. package/src/signals/fingerprint/PluginsSignal.js +106 -0
  33. package/src/signals/fingerprint/ScreenSignal.js +157 -0
  34. package/src/signals/fingerprint/WebGLSignal.js +146 -0
  35. package/src/signals/fingerprint/index.js +9 -0
  36. package/src/signals/timing/DOMContentTimingSignal.js +159 -0
  37. package/src/signals/timing/PageLoadSignal.js +165 -0
  38. package/src/signals/timing/index.js +6 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @fileoverview Detects WebGL rendering anomalies.
3
+ */
4
+
5
+ import { Signal } from '../../core/Signal.js';
6
+
7
+ /**
8
+ * Detects WebGL anomalies and spoofed renderer information.
9
+ * Bots often have missing, disabled, or fake WebGL contexts.
10
+ */
11
+ class WebGLSignal extends Signal {
12
+ static id = 'webgl';
13
+ static category = 'fingerprint';
14
+ static weight = 0.7;
15
+ static description = 'Detects WebGL rendering anomalies';
16
+
17
+ async detect() {
18
+ const anomalies = [];
19
+ let confidence = 0;
20
+
21
+ // Try to get WebGL context
22
+ const canvas = document.createElement('canvas');
23
+ let gl = null;
24
+
25
+ try {
26
+ gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
27
+ } catch (e) {
28
+ anomalies.push('webgl-error');
29
+ confidence = Math.max(confidence, 0.5);
30
+ }
31
+
32
+ if (!gl) {
33
+ // WebGL not available - could be disabled or blocked
34
+ anomalies.push('webgl-unavailable');
35
+ confidence = Math.max(confidence, 0.4);
36
+ return this.createResult(true, { anomalies }, confidence);
37
+ }
38
+
39
+ // Get renderer info
40
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
41
+ let vendor = '';
42
+ let renderer = '';
43
+
44
+ if (debugInfo) {
45
+ vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';
46
+ renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';
47
+ }
48
+
49
+ // Check for empty renderer info (common in headless)
50
+ if (!vendor && !renderer) {
51
+ anomalies.push('no-webgl-renderer-info');
52
+ confidence = Math.max(confidence, 0.6);
53
+ }
54
+
55
+ // Check for known headless/virtual renderer strings
56
+ const suspiciousRenderers = [
57
+ 'swiftshader',
58
+ 'llvmpipe',
59
+ 'software',
60
+ 'mesa',
61
+ 'google swiftshader',
62
+ 'vmware',
63
+ 'virtualbox',
64
+ ];
65
+
66
+ const rendererLower = renderer.toLowerCase();
67
+ for (const sus of suspiciousRenderers) {
68
+ if (rendererLower.includes(sus)) {
69
+ anomalies.push(`suspicious-renderer-${sus.replace(/\s+/g, '-')}`);
70
+ confidence = Math.max(confidence, 0.7);
71
+ break;
72
+ }
73
+ }
74
+
75
+ // Check for mismatched vendor/renderer
76
+ if (vendor && renderer) {
77
+ // NVIDIA renderer should have NVIDIA vendor
78
+ if (rendererLower.includes('nvidia') && !vendor.toLowerCase().includes('nvidia')) {
79
+ anomalies.push('vendor-renderer-mismatch');
80
+ confidence = Math.max(confidence, 0.6);
81
+ }
82
+ // AMD renderer should have AMD/ATI vendor
83
+ if ((rendererLower.includes('amd') || rendererLower.includes('radeon')) &&
84
+ !vendor.toLowerCase().includes('amd') && !vendor.toLowerCase().includes('ati')) {
85
+ anomalies.push('vendor-renderer-mismatch');
86
+ confidence = Math.max(confidence, 0.6);
87
+ }
88
+ }
89
+
90
+ // Check for supported extensions
91
+ const extensions = gl.getSupportedExtensions() || [];
92
+
93
+ // Suspiciously few extensions
94
+ if (extensions.length < 5) {
95
+ anomalies.push('few-webgl-extensions');
96
+ confidence = Math.max(confidence, 0.4);
97
+ }
98
+
99
+ // Check for WebGL parameter consistency
100
+ const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
101
+ const maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
102
+
103
+ // Unrealistic values
104
+ if (maxTextureSize < 1024 || maxTextureSize > 65536) {
105
+ anomalies.push('unrealistic-max-texture');
106
+ confidence = Math.max(confidence, 0.5);
107
+ }
108
+
109
+ // Check if WebGL rendering actually works
110
+ try {
111
+ // Simple render test
112
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
113
+ gl.clear(gl.COLOR_BUFFER_BIT);
114
+
115
+ const pixels = new Uint8Array(4);
116
+ gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
117
+
118
+ // If clear didn't work, something's wrong
119
+ if (pixels[3] !== 255) {
120
+ anomalies.push('webgl-render-failure');
121
+ confidence = Math.max(confidence, 0.6);
122
+ }
123
+ } catch (e) {
124
+ anomalies.push('webgl-render-error');
125
+ confidence = Math.max(confidence, 0.5);
126
+ }
127
+
128
+ // Clean up
129
+ const loseContext = gl.getExtension('WEBGL_lose_context');
130
+ if (loseContext) {
131
+ loseContext.loseContext();
132
+ }
133
+
134
+ const triggered = anomalies.length > 0;
135
+
136
+ return this.createResult(triggered, {
137
+ anomalies,
138
+ vendor,
139
+ renderer,
140
+ extensionCount: extensions.length,
141
+ maxTextureSize,
142
+ }, confidence);
143
+ }
144
+ }
145
+
146
+ export { WebGLSignal };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Fingerprint signals index.
3
+ */
4
+
5
+ export { PluginsSignal } from './PluginsSignal.js';
6
+ export { WebGLSignal } from './WebGLSignal.js';
7
+ export { CanvasSignal } from './CanvasSignal.js';
8
+ export { AudioContextSignal } from './AudioContextSignal.js';
9
+ export { ScreenSignal } from './ScreenSignal.js';
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @fileoverview Analyzes DOM content loaded event timing.
3
+ */
4
+
5
+ import { Signal } from '../../core/Signal.js';
6
+
7
+ /**
8
+ * Analyzes timing around DOMContentLoaded event.
9
+ * Bots may have unusual patterns in when/how DOM is processed.
10
+ */
11
+ class DOMContentTimingSignal extends Signal {
12
+ static id = 'dom-content-timing';
13
+ static category = 'timing';
14
+ static weight = 0.4;
15
+ static description = 'Analyzes DOM content loaded timing patterns';
16
+
17
+ constructor(options = {}) {
18
+ super(options);
19
+ this._domContentLoadedTime = null;
20
+ this._documentReadyState = document.readyState;
21
+ this._captureTime = performance.now();
22
+
23
+ // Capture DOMContentLoaded time if not already loaded
24
+ if (document.readyState === 'loading') {
25
+ document.addEventListener('DOMContentLoaded', () => {
26
+ this._domContentLoadedTime = performance.now();
27
+ });
28
+ }
29
+ }
30
+
31
+ async detect() {
32
+ const anomalies = [];
33
+ let confidence = 0;
34
+
35
+ // Get timing information
36
+ const now = performance.now();
37
+ const readyState = document.readyState;
38
+
39
+ // Check resource timing
40
+ let resourceCount = 0;
41
+ let totalResourceTime = 0;
42
+ let externalScriptCount = 0;
43
+
44
+ if (performance.getEntriesByType) {
45
+ const resources = performance.getEntriesByType('resource');
46
+ resourceCount = resources.length;
47
+
48
+ for (const resource of resources) {
49
+ totalResourceTime += resource.duration;
50
+ if (resource.initiatorType === 'script' &&
51
+ resource.name.startsWith('http')) {
52
+ externalScriptCount++;
53
+ }
54
+ }
55
+ }
56
+
57
+ // Check for very few resources (headless often loads minimal)
58
+ if (resourceCount === 0 && readyState === 'complete') {
59
+ anomalies.push('no-resources-loaded');
60
+ confidence = Math.max(confidence, 0.4);
61
+ }
62
+
63
+ // Check for suspiciously fast DOM ready without resources
64
+ if (this._domContentLoadedTime && this._domContentLoadedTime < 50 && resourceCount === 0) {
65
+ anomalies.push('instant-ready-no-resources');
66
+ confidence = Math.max(confidence, 0.6);
67
+ }
68
+
69
+ // Check document.hidden state at load
70
+ // Bots often run in hidden/background state
71
+ if (document.hidden && this._documentReadyState === 'loading') {
72
+ anomalies.push('hidden-at-load');
73
+ confidence = Math.max(confidence, 0.3);
74
+ }
75
+
76
+ // Check for visibility API
77
+ if (typeof document.visibilityState === 'undefined') {
78
+ anomalies.push('no-visibility-api');
79
+ confidence = Math.max(confidence, 0.4);
80
+ }
81
+
82
+ // Check DOM manipulation timing
83
+ try {
84
+ const startMutation = performance.now();
85
+ const testDiv = document.createElement('div');
86
+ testDiv.id = '__bot_detection_test__';
87
+ document.body.appendChild(testDiv);
88
+ const afterAppend = performance.now();
89
+ document.body.removeChild(testDiv);
90
+ const afterRemove = performance.now();
91
+
92
+ const appendTime = afterAppend - startMutation;
93
+ const removeTime = afterRemove - afterAppend;
94
+
95
+ // Instant DOM operations (< 0.01ms) may indicate mocked DOM
96
+ if (appendTime === 0 && removeTime === 0) {
97
+ anomalies.push('instant-dom-operations');
98
+ confidence = Math.max(confidence, 0.5);
99
+ }
100
+ } catch (e) {
101
+ // If body doesn't exist yet, that's unusual at detection time
102
+ if (!document.body) {
103
+ anomalies.push('no-document-body');
104
+ confidence = Math.max(confidence, 0.4);
105
+ }
106
+ }
107
+
108
+ // Check for MutationObserver availability (should exist in modern browsers)
109
+ if (typeof MutationObserver === 'undefined') {
110
+ anomalies.push('no-mutation-observer');
111
+ confidence = Math.max(confidence, 0.5);
112
+ }
113
+
114
+ // Check for requestAnimationFrame availability
115
+ if (typeof requestAnimationFrame === 'undefined') {
116
+ anomalies.push('no-request-animation-frame');
117
+ confidence = Math.max(confidence, 0.5);
118
+ }
119
+
120
+ // Check timing of first paint if available
121
+ if (performance.getEntriesByType) {
122
+ const paintEntries = performance.getEntriesByType('paint');
123
+ const firstPaint = paintEntries.find(e => e.name === 'first-paint');
124
+
125
+ if (!firstPaint && readyState === 'complete' && now > 1000) {
126
+ anomalies.push('no-first-paint');
127
+ confidence = Math.max(confidence, 0.4);
128
+ }
129
+
130
+ // Check for first contentful paint
131
+ const fcp = paintEntries.find(e => e.name === 'first-contentful-paint');
132
+ if (!fcp && readyState === 'complete' && now > 1000) {
133
+ anomalies.push('no-first-contentful-paint');
134
+ confidence = Math.max(confidence, 0.4);
135
+ }
136
+ }
137
+
138
+ // Check intersection observer
139
+ if (typeof IntersectionObserver === 'undefined') {
140
+ anomalies.push('no-intersection-observer');
141
+ confidence = Math.max(confidence, 0.4);
142
+ }
143
+
144
+ const triggered = anomalies.length > 0;
145
+
146
+ return this.createResult(triggered, {
147
+ anomalies,
148
+ metrics: {
149
+ readyState,
150
+ resourceCount,
151
+ externalScriptCount,
152
+ domContentLoadedTime: this._domContentLoadedTime,
153
+ documentHidden: document.hidden,
154
+ },
155
+ }, confidence);
156
+ }
157
+ }
158
+
159
+ export { DOMContentTimingSignal };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @fileoverview Detects suspicious page load timing patterns.
3
+ */
4
+
5
+ import { Signal } from '../../core/Signal.js';
6
+
7
+ /**
8
+ * Analyzes page load timing for automation indicators.
9
+ * Bots often have unusual or suspiciously fast load patterns.
10
+ */
11
+ class PageLoadSignal extends Signal {
12
+ static id = 'page-load';
13
+ static category = 'timing';
14
+ static weight = 0.5;
15
+ static description = 'Detects suspicious page load timing';
16
+
17
+ async detect() {
18
+ const anomalies = [];
19
+ let confidence = 0;
20
+
21
+ // Check if Performance API is available
22
+ if (!window.performance || !performance.timing) {
23
+ // Try Navigation Timing API Level 2
24
+ if (performance.getEntriesByType) {
25
+ const navEntries = performance.getEntriesByType('navigation');
26
+ if (navEntries.length > 0) {
27
+ return this._analyzeNavigationTiming(navEntries[0]);
28
+ }
29
+ }
30
+
31
+ anomalies.push('no-performance-api');
32
+ confidence = Math.max(confidence, 0.3);
33
+ return this.createResult(true, { anomalies }, confidence);
34
+ }
35
+
36
+ const timing = performance.timing;
37
+
38
+ // Calculate key timings
39
+ const navigationStart = timing.navigationStart;
40
+ const domContentLoaded = timing.domContentLoadedEventEnd - navigationStart;
41
+ const domComplete = timing.domComplete - navigationStart;
42
+ const loadComplete = timing.loadEventEnd - navigationStart;
43
+ const dnsLookup = timing.domainLookupEnd - timing.domainLookupStart;
44
+ const tcpConnection = timing.connectEnd - timing.connectStart;
45
+ const serverResponse = timing.responseEnd - timing.requestStart;
46
+ const domProcessing = timing.domComplete - timing.domLoading;
47
+
48
+ // Check for impossibly fast load times
49
+ if (domContentLoaded > 0 && domContentLoaded < 10) {
50
+ anomalies.push('instant-dom-content-loaded');
51
+ confidence = Math.max(confidence, 0.7);
52
+ }
53
+
54
+ // Check for zero DNS lookup (could indicate local file or caching, but suspicious in combination)
55
+ if (dnsLookup === 0 && tcpConnection === 0 && serverResponse < 5) {
56
+ anomalies.push('zero-network-timing');
57
+ confidence = Math.max(confidence, 0.4);
58
+ }
59
+
60
+ // Check for negative timings (timestamp manipulation)
61
+ if (domContentLoaded < 0 || domComplete < 0 || loadComplete < 0) {
62
+ anomalies.push('negative-timing');
63
+ confidence = Math.max(confidence, 0.8);
64
+ }
65
+
66
+ // Check for unrealistic timing order
67
+ if (timing.domContentLoadedEventEnd > 0 && timing.loadEventEnd > 0) {
68
+ if (timing.domContentLoadedEventEnd > timing.loadEventEnd) {
69
+ anomalies.push('timing-order-violation');
70
+ confidence = Math.max(confidence, 0.7);
71
+ }
72
+ }
73
+
74
+ // Check for very long processing times (could indicate headless waiting)
75
+ if (domProcessing > 30000) { // 30 seconds
76
+ anomalies.push('excessive-dom-processing');
77
+ confidence = Math.max(confidence, 0.3);
78
+ }
79
+
80
+ // Check for script injection timing pattern
81
+ // Bots often inject scripts immediately after load
82
+ const scriptsLoadedTime = timing.domContentLoadedEventStart - timing.responseEnd;
83
+ if (scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
84
+ anomalies.push('instant-script-execution');
85
+ confidence = Math.max(confidence, 0.4);
86
+ }
87
+
88
+ // Check for performance.now() manipulation
89
+ const perfNow1 = performance.now();
90
+ const perfNow2 = performance.now();
91
+
92
+ // If two consecutive calls return the same value (shouldn't happen)
93
+ if (perfNow1 === perfNow2 && perfNow1 > 0) {
94
+ anomalies.push('frozen-performance-now');
95
+ confidence = Math.max(confidence, 0.6);
96
+ }
97
+
98
+ // Check for Date.now() vs performance.now() consistency
99
+ const dateNow1 = Date.now();
100
+ const perfNow3 = performance.now();
101
+ const dateNow2 = Date.now();
102
+
103
+ // If they're wildly inconsistent
104
+ if (Math.abs((dateNow2 - dateNow1) - (performance.now() - perfNow3)) > 100) {
105
+ anomalies.push('timing-inconsistency');
106
+ confidence = Math.max(confidence, 0.5);
107
+ }
108
+
109
+ const triggered = anomalies.length > 0;
110
+
111
+ return this.createResult(triggered, {
112
+ anomalies,
113
+ timings: {
114
+ domContentLoaded,
115
+ domComplete,
116
+ loadComplete,
117
+ dnsLookup,
118
+ tcpConnection,
119
+ serverResponse,
120
+ domProcessing,
121
+ },
122
+ }, confidence);
123
+ }
124
+
125
+ /**
126
+ * Analyze Navigation Timing Level 2 API data.
127
+ * @param {PerformanceNavigationTiming} entry - Navigation timing entry
128
+ * @returns {SignalResult}
129
+ */
130
+ _analyzeNavigationTiming(entry) {
131
+ const anomalies = [];
132
+ let confidence = 0;
133
+
134
+ const domContentLoaded = entry.domContentLoadedEventEnd;
135
+ const loadComplete = entry.loadEventEnd;
136
+ const dnsLookup = entry.domainLookupEnd - entry.domainLookupStart;
137
+ const serverResponse = entry.responseEnd - entry.requestStart;
138
+
139
+ // Check for impossibly fast load
140
+ if (domContentLoaded > 0 && domContentLoaded < 10) {
141
+ anomalies.push('instant-dom-content-loaded');
142
+ confidence = Math.max(confidence, 0.7);
143
+ }
144
+
145
+ // Check for zero timings
146
+ if (dnsLookup === 0 && serverResponse === 0) {
147
+ anomalies.push('zero-network-timing');
148
+ confidence = Math.max(confidence, 0.4);
149
+ }
150
+
151
+ const triggered = anomalies.length > 0;
152
+
153
+ return this.createResult(triggered, {
154
+ anomalies,
155
+ timings: {
156
+ domContentLoaded,
157
+ loadComplete,
158
+ dnsLookup,
159
+ serverResponse,
160
+ },
161
+ }, confidence);
162
+ }
163
+ }
164
+
165
+ export { PageLoadSignal };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Timing signals index.
3
+ */
4
+
5
+ export { PageLoadSignal } from './PageLoadSignal.js';
6
+ export { DOMContentTimingSignal } from './DOMContentTimingSignal.js';