@niksbanna/bot-detector 1.0.3 → 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 +3 -0
- package/dist/bot-detector.cjs.js +0 -1
- package/dist/bot-detector.esm.js +0 -1
- package/dist/bot-detector.iife.js +0 -1
- package/package.json +1 -2
- package/src/core/BotDetector.js +0 -295
- package/src/core/ScoringEngine.js +0 -135
- package/src/core/Signal.js +0 -181
- package/src/core/VerdictEngine.js +0 -132
- package/src/index.js +0 -281
- package/src/signals/automation/PhantomJSSignal.js +0 -135
- package/src/signals/automation/PlaywrightSignal.js +0 -123
- package/src/signals/automation/PuppeteerSignal.js +0 -123
- package/src/signals/automation/SeleniumSignal.js +0 -148
- 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 -118
- 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 -160
- package/src/signals/timing/PageLoadSignal.js +0 -183
- package/src/signals/timing/index.js +0 -6
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects Puppeteer-specific artifacts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects artifacts left by Puppeteer automation.
|
|
9
|
-
* Puppeteer leaves various fingerprints in the browser context.
|
|
10
|
-
*/
|
|
11
|
-
class PuppeteerSignal extends Signal {
|
|
12
|
-
static id = 'puppeteer';
|
|
13
|
-
static category = 'automation';
|
|
14
|
-
static weight = 1.0;
|
|
15
|
-
static description = 'Detects Puppeteer automation artifacts';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const indicators = [];
|
|
19
|
-
let confidence = 0;
|
|
20
|
-
|
|
21
|
-
// Check for Puppeteer evaluation script marker
|
|
22
|
-
if (window.__puppeteer_evaluation_script__) {
|
|
23
|
-
indicators.push('puppeteer-evaluation-script');
|
|
24
|
-
confidence = Math.max(confidence, 1.0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check for Puppeteer-injected functions
|
|
28
|
-
const puppeteerGlobals = [
|
|
29
|
-
'__puppeteer_evaluation_script__',
|
|
30
|
-
'__puppeteer',
|
|
31
|
-
'puppeteer',
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
for (const global of puppeteerGlobals) {
|
|
35
|
-
if (global in window) {
|
|
36
|
-
indicators.push(`global-${global}`);
|
|
37
|
-
confidence = Math.max(confidence, 1.0);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check for HeadlessChrome in user agent (common with Puppeteer)
|
|
42
|
-
const ua = navigator.userAgent || '';
|
|
43
|
-
if (ua.includes('HeadlessChrome')) {
|
|
44
|
-
indicators.push('headless-chrome-ua');
|
|
45
|
-
confidence = Math.max(confidence, 0.9);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Check for Puppeteer's typical Chrome DevTools Protocol artifacts
|
|
49
|
-
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array ||
|
|
50
|
-
window.cdc_adoQpoasnfa76pfcZLmcfl_Promise ||
|
|
51
|
-
window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
|
|
52
|
-
indicators.push('cdp-artifacts');
|
|
53
|
-
confidence = Math.max(confidence, 1.0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check for DevTools protocol detection
|
|
57
|
-
try {
|
|
58
|
-
// Puppeteer often leaves eval traces
|
|
59
|
-
const evalTest = window.eval.toString();
|
|
60
|
-
if (evalTest.includes('puppeteer')) {
|
|
61
|
-
indicators.push('eval-puppeteer');
|
|
62
|
-
confidence = Math.max(confidence, 0.9);
|
|
63
|
-
}
|
|
64
|
-
} catch (e) {
|
|
65
|
-
// Ignore errors
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check for typical Puppeteer page.evaluate patterns in stack traces
|
|
69
|
-
try {
|
|
70
|
-
throw new Error('stack trace test');
|
|
71
|
-
} catch (e) {
|
|
72
|
-
const stack = e.stack || '';
|
|
73
|
-
if (stack.includes('puppeteer') || stack.includes('pptr')) {
|
|
74
|
-
indicators.push('stack-trace-puppeteer');
|
|
75
|
-
confidence = Math.max(confidence, 0.8);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check for Puppeteer's default viewport (800x600)
|
|
80
|
-
if (window.innerWidth === 800 && window.innerHeight === 600) {
|
|
81
|
-
// Only weak indicator - could be coincidence
|
|
82
|
-
indicators.push('default-viewport');
|
|
83
|
-
confidence = Math.max(confidence, 0.3);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// navigator.webdriver is already exclusively checked by WebDriverSignal (environment).
|
|
87
|
-
// Duplicating it here causes triple-counting of the same property across signals.
|
|
88
|
-
|
|
89
|
-
// Check for binding injection pattern
|
|
90
|
-
// Puppeteer's exposeFunction creates window bindings
|
|
91
|
-
// (Next.js, webpack, React DevTools, Angular) push __* count past the
|
|
92
|
-
// old threshold of 5 on perfectly normal pages.
|
|
93
|
-
const FRAMEWORK_PREFIXES = [
|
|
94
|
-
'__zone_symbol__', // Angular / Zone.js
|
|
95
|
-
'__next', // Next.js
|
|
96
|
-
'__webpack', // webpack
|
|
97
|
-
'__react', // React DevTools
|
|
98
|
-
'__REACT',
|
|
99
|
-
'__vite', // Vite
|
|
100
|
-
'__nuxt', // Nuxt.js
|
|
101
|
-
];
|
|
102
|
-
const suspiciousBindings = Object.keys(window).filter(key => {
|
|
103
|
-
if (!key.startsWith('__')) return false;
|
|
104
|
-
if (typeof window[key] !== 'function') return false;
|
|
105
|
-
return !FRAMEWORK_PREFIXES.some(prefix => key.startsWith(prefix));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (suspiciousBindings.length > 10) {
|
|
109
|
-
indicators.push('suspicious-bindings');
|
|
110
|
-
confidence = Math.max(confidence, 0.5);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// window.chrome.runtime is ONLY populated inside Chrome extensions.
|
|
114
|
-
// Its absence is completely normal for all real Chrome users.
|
|
115
|
-
// This check was causing every non-extension Chrome user to be flagged as Puppeteer.
|
|
116
|
-
|
|
117
|
-
const triggered = indicators.length > 0;
|
|
118
|
-
|
|
119
|
-
return this.createResult(triggered, { indicators }, confidence);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export { PuppeteerSignal };
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects Selenium WebDriver artifacts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects artifacts left by Selenium WebDriver.
|
|
9
|
-
* Selenium leaves various fingerprints in the browser context.
|
|
10
|
-
*/
|
|
11
|
-
class SeleniumSignal extends Signal {
|
|
12
|
-
static id = 'selenium';
|
|
13
|
-
static category = 'automation';
|
|
14
|
-
static weight = 1.0;
|
|
15
|
-
static description = 'Detects Selenium WebDriver artifacts';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const indicators = [];
|
|
19
|
-
let confidence = 0;
|
|
20
|
-
|
|
21
|
-
// navigator.webdriver is already exclusively checked by WebDriverSignal (environment).
|
|
22
|
-
// Duplicating it here causes triple-counting of the same property across signals.
|
|
23
|
-
|
|
24
|
-
// Check for Selenium-specific globals
|
|
25
|
-
const seleniumGlobals = [
|
|
26
|
-
'_selenium',
|
|
27
|
-
'callSelenium',
|
|
28
|
-
'_Selenium_IDE_Recorder',
|
|
29
|
-
'__selenium_evaluate',
|
|
30
|
-
'__selenium_unwrap',
|
|
31
|
-
'__webdriver_evaluate',
|
|
32
|
-
'__webdriver_unwrap',
|
|
33
|
-
'__webdriver_script_function',
|
|
34
|
-
'__webdriver_script_func',
|
|
35
|
-
'__fxdriver_evaluate',
|
|
36
|
-
'__fxdriver_unwrap',
|
|
37
|
-
'webdriver',
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
for (const global of seleniumGlobals) {
|
|
41
|
-
if (global in window) {
|
|
42
|
-
indicators.push(`global-${global}`);
|
|
43
|
-
confidence = Math.max(confidence, 1.0);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check for Selenium document properties
|
|
48
|
-
const seleniumDocProps = [
|
|
49
|
-
'__webdriver_script_fn',
|
|
50
|
-
'__driver_evaluate',
|
|
51
|
-
'__webdriver_evaluate',
|
|
52
|
-
'__selenium_evaluate',
|
|
53
|
-
'__fxdriver_evaluate',
|
|
54
|
-
'__driver_unwrap',
|
|
55
|
-
'__webdriver_unwrap',
|
|
56
|
-
'__selenium_unwrap',
|
|
57
|
-
'__fxdriver_unwrap',
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
for (const prop of seleniumDocProps) {
|
|
61
|
-
if (prop in document) {
|
|
62
|
-
indicators.push(`document-${prop}`);
|
|
63
|
-
confidence = Math.max(confidence, 1.0);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Check for ChromeDriver artifacts ($cdc variables)
|
|
68
|
-
const windowKeys = Object.keys(window);
|
|
69
|
-
|
|
70
|
-
// ChromeDriver injects variables starting with $cdc_ or $wdc_
|
|
71
|
-
const cdcVars = windowKeys.filter(key =>
|
|
72
|
-
key.startsWith('$cdc_') ||
|
|
73
|
-
key.startsWith('$wdc_') ||
|
|
74
|
-
key.startsWith('$chrome_asyncScriptInfo')
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
if (cdcVars.length > 0) {
|
|
78
|
-
indicators.push('chromedriver-variables');
|
|
79
|
-
confidence = Math.max(confidence, 1.0);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Check for GeckoDriver (Firefox) artifacts
|
|
83
|
-
if (window.webdriverCallback || document.documentElement.getAttribute('webdriver')) {
|
|
84
|
-
indicators.push('geckodriver-artifacts');
|
|
85
|
-
confidence = Math.max(confidence, 1.0);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Check for webdriver in document element attributes
|
|
89
|
-
try {
|
|
90
|
-
const docElement = document.documentElement;
|
|
91
|
-
if (docElement.hasAttribute('webdriver') ||
|
|
92
|
-
docElement.getAttribute('selenium') ||
|
|
93
|
-
docElement.getAttribute('driver')) {
|
|
94
|
-
indicators.push('document-webdriver-attr');
|
|
95
|
-
confidence = Math.max(confidence, 1.0);
|
|
96
|
-
}
|
|
97
|
-
} catch (e) {
|
|
98
|
-
// Ignore errors
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Check for Selenium IDE artifacts
|
|
102
|
-
if (window.selenium || window.sideex) {
|
|
103
|
-
indicators.push('selenium-ide');
|
|
104
|
-
confidence = Math.max(confidence, 1.0);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Check for navigator.webdriver property descriptor anomalies
|
|
108
|
-
try {
|
|
109
|
-
const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
|
|
110
|
-
if (descriptor) {
|
|
111
|
-
// Check if the getter has been modified
|
|
112
|
-
if (descriptor.get) {
|
|
113
|
-
const getterStr = descriptor.get.toString();
|
|
114
|
-
if (!getterStr.includes('[native code]')) {
|
|
115
|
-
indicators.push('webdriver-getter-modified');
|
|
116
|
-
confidence = Math.max(confidence, 0.7);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} catch (e) {
|
|
121
|
-
// Ignore errors
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check for driver command executor
|
|
125
|
-
if (window.domAutomation || window.domAutomationController) {
|
|
126
|
-
indicators.push('dom-automation');
|
|
127
|
-
confidence = Math.max(confidence, 1.0);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Check for callPhantom alternative used by some Selenium setups
|
|
131
|
-
if (window.awesomium) {
|
|
132
|
-
indicators.push('awesomium');
|
|
133
|
-
confidence = Math.max(confidence, 0.9);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Check for external interface (used by some automation tools)
|
|
137
|
-
if (window.external && window.external.toString().includes('Selenium')) {
|
|
138
|
-
indicators.push('external-selenium');
|
|
139
|
-
confidence = Math.max(confidence, 1.0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const triggered = indicators.length > 0;
|
|
143
|
-
|
|
144
|
-
return this.createResult(triggered, { indicators }, confidence);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export { SeleniumSignal };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Automation signals index.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { PuppeteerSignal } from './PuppeteerSignal.js';
|
|
6
|
-
export { PlaywrightSignal } from './PlaywrightSignal.js';
|
|
7
|
-
export { SeleniumSignal } from './SeleniumSignal.js';
|
|
8
|
-
export { PhantomJSSignal } from './PhantomJSSignal.js';
|
|
@@ -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 = Math.min(options.trackingDuration || 2500, 2500);
|
|
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 };
|