@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,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Verdict engine that determines bot/human classification.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Possible verdicts from detection.
|
|
7
|
-
* @enum {string}
|
|
8
|
-
*/
|
|
9
|
-
const Verdict = {
|
|
10
|
-
HUMAN: 'human',
|
|
11
|
-
SUSPICIOUS: 'suspicious',
|
|
12
|
-
BOT: 'bot',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Determines the final verdict based on score and triggered signals.
|
|
17
|
-
*/
|
|
18
|
-
class VerdictEngine {
|
|
19
|
-
/**
|
|
20
|
-
* Default threshold configuration.
|
|
21
|
-
* @type {Object}
|
|
22
|
-
*/
|
|
23
|
-
static DEFAULT_THRESHOLDS = {
|
|
24
|
-
human: 20, // score < 20 = human
|
|
25
|
-
suspicious: 50, // 20 <= score < 50 = suspicious
|
|
26
|
-
// score >= 50 = bot
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Creates a new VerdictEngine instance.
|
|
31
|
-
* @param {Object} [options={}] - Configuration options
|
|
32
|
-
* @param {number} [options.humanThreshold=20] - Max score for human verdict
|
|
33
|
-
* @param {number} [options.suspiciousThreshold=50] - Max score for suspicious verdict
|
|
34
|
-
* @param {Array<string>} [options.instantBotSignals=[]] - Signal IDs that instantly flag as bot
|
|
35
|
-
*/
|
|
36
|
-
constructor(options = {}) {
|
|
37
|
-
this.humanThreshold = options.humanThreshold ?? VerdictEngine.DEFAULT_THRESHOLDS.human;
|
|
38
|
-
this.suspiciousThreshold = options.suspiciousThreshold ?? VerdictEngine.DEFAULT_THRESHOLDS.suspicious;
|
|
39
|
-
this.instantBotSignals = new Set(options.instantBotSignals || []);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get the verdict based on score and triggered signals.
|
|
44
|
-
* @param {number} score - Calculated score (0-100)
|
|
45
|
-
* @param {Array<string>} triggeredSignals - List of triggered signal IDs
|
|
46
|
-
* @returns {VerdictResult}
|
|
47
|
-
*/
|
|
48
|
-
getVerdict(score, triggeredSignals = []) {
|
|
49
|
-
// Check for instant bot signals first
|
|
50
|
-
for (const signalId of triggeredSignals) {
|
|
51
|
-
if (this.instantBotSignals.has(signalId)) {
|
|
52
|
-
return {
|
|
53
|
-
verdict: Verdict.BOT,
|
|
54
|
-
score,
|
|
55
|
-
confidence: 'high',
|
|
56
|
-
reason: `Instant bot signal triggered: ${signalId}`,
|
|
57
|
-
triggeredCount: triggeredSignals.length,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Score-based verdict
|
|
63
|
-
let verdict;
|
|
64
|
-
let confidence;
|
|
65
|
-
let reason;
|
|
66
|
-
|
|
67
|
-
if (score < this.humanThreshold) {
|
|
68
|
-
verdict = Verdict.HUMAN;
|
|
69
|
-
confidence = score < 10 ? 'high' : 'medium';
|
|
70
|
-
reason = 'Low bot score';
|
|
71
|
-
} else if (score < this.suspiciousThreshold) {
|
|
72
|
-
verdict = Verdict.SUSPICIOUS;
|
|
73
|
-
confidence = 'medium';
|
|
74
|
-
reason = 'Moderate bot indicators detected';
|
|
75
|
-
} else {
|
|
76
|
-
verdict = Verdict.BOT;
|
|
77
|
-
confidence = score >= 75 ? 'high' : 'medium';
|
|
78
|
-
reason = 'High accumulation of bot indicators';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
verdict,
|
|
83
|
-
score,
|
|
84
|
-
confidence,
|
|
85
|
-
reason,
|
|
86
|
-
triggeredCount: triggeredSignals.length,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Check if signal should instantly flag as bot.
|
|
92
|
-
* @param {string} signalId - Signal identifier
|
|
93
|
-
* @returns {boolean}
|
|
94
|
-
*/
|
|
95
|
-
isInstantBotSignal(signalId) {
|
|
96
|
-
return this.instantBotSignals.has(signalId);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Add a signal to the instant bot list.
|
|
101
|
-
* @param {string} signalId - Signal identifier
|
|
102
|
-
*/
|
|
103
|
-
addInstantBotSignal(signalId) {
|
|
104
|
-
this.instantBotSignals.add(signalId);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Update thresholds.
|
|
109
|
-
* @param {Object} thresholds - New threshold values
|
|
110
|
-
* @param {number} [thresholds.human] - New human threshold
|
|
111
|
-
* @param {number} [thresholds.suspicious] - New suspicious threshold
|
|
112
|
-
*/
|
|
113
|
-
setThresholds(thresholds) {
|
|
114
|
-
if (thresholds.human !== undefined) {
|
|
115
|
-
this.humanThreshold = thresholds.human;
|
|
116
|
-
}
|
|
117
|
-
if (thresholds.suspicious !== undefined) {
|
|
118
|
-
this.suspiciousThreshold = thresholds.suspicious;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @typedef {Object} VerdictResult
|
|
125
|
-
* @property {string} verdict - 'human', 'suspicious', or 'bot'
|
|
126
|
-
* @property {number} score - The calculated score
|
|
127
|
-
* @property {string} confidence - 'low', 'medium', or 'high'
|
|
128
|
-
* @property {string} reason - Human-readable reason for verdict
|
|
129
|
-
* @property {number} triggeredCount - Number of triggered signals
|
|
130
|
-
*/
|
|
131
|
-
|
|
132
|
-
export { VerdictEngine, Verdict };
|
package/src/index.js
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Main entry point for the bot detection library.
|
|
3
|
-
* @module @niksbanna/bot-detector
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Core classes
|
|
7
|
-
import { BotDetector, Verdict } from './core/BotDetector.js';
|
|
8
|
-
import { Signal } from './core/Signal.js';
|
|
9
|
-
import { ScoringEngine } from './core/ScoringEngine.js';
|
|
10
|
-
import { VerdictEngine } from './core/VerdictEngine.js';
|
|
11
|
-
|
|
12
|
-
// Environment signals
|
|
13
|
-
import {
|
|
14
|
-
WebDriverSignal,
|
|
15
|
-
HeadlessSignal,
|
|
16
|
-
NavigatorAnomalySignal,
|
|
17
|
-
PermissionsSignal,
|
|
18
|
-
} from './signals/environment/index.js';
|
|
19
|
-
|
|
20
|
-
// Behavioral signals
|
|
21
|
-
import {
|
|
22
|
-
MouseMovementSignal,
|
|
23
|
-
KeyboardPatternSignal,
|
|
24
|
-
InteractionTimingSignal,
|
|
25
|
-
ScrollBehaviorSignal,
|
|
26
|
-
} from './signals/behavior/index.js';
|
|
27
|
-
|
|
28
|
-
// Fingerprint signals
|
|
29
|
-
import {
|
|
30
|
-
PluginsSignal,
|
|
31
|
-
WebGLSignal,
|
|
32
|
-
CanvasSignal,
|
|
33
|
-
AudioContextSignal,
|
|
34
|
-
ScreenSignal,
|
|
35
|
-
} from './signals/fingerprint/index.js';
|
|
36
|
-
|
|
37
|
-
// Timing signals
|
|
38
|
-
import {
|
|
39
|
-
PageLoadSignal,
|
|
40
|
-
DOMContentTimingSignal,
|
|
41
|
-
} from './signals/timing/index.js';
|
|
42
|
-
|
|
43
|
-
// Automation framework signals
|
|
44
|
-
import {
|
|
45
|
-
PuppeteerSignal,
|
|
46
|
-
PlaywrightSignal,
|
|
47
|
-
SeleniumSignal,
|
|
48
|
-
PhantomJSSignal,
|
|
49
|
-
} from './signals/automation/index.js';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* All built-in signal classes organized by category.
|
|
53
|
-
*/
|
|
54
|
-
const Signals = {
|
|
55
|
-
// Environment signals
|
|
56
|
-
WebDriverSignal,
|
|
57
|
-
HeadlessSignal,
|
|
58
|
-
NavigatorAnomalySignal,
|
|
59
|
-
PermissionsSignal,
|
|
60
|
-
|
|
61
|
-
// Behavioral signals
|
|
62
|
-
MouseMovementSignal,
|
|
63
|
-
KeyboardPatternSignal,
|
|
64
|
-
InteractionTimingSignal,
|
|
65
|
-
ScrollBehaviorSignal,
|
|
66
|
-
|
|
67
|
-
// Fingerprint signals
|
|
68
|
-
PluginsSignal,
|
|
69
|
-
WebGLSignal,
|
|
70
|
-
CanvasSignal,
|
|
71
|
-
AudioContextSignal,
|
|
72
|
-
ScreenSignal,
|
|
73
|
-
|
|
74
|
-
// Timing signals
|
|
75
|
-
PageLoadSignal,
|
|
76
|
-
DOMContentTimingSignal,
|
|
77
|
-
|
|
78
|
-
// Automation framework signals
|
|
79
|
-
PuppeteerSignal,
|
|
80
|
-
PlaywrightSignal,
|
|
81
|
-
SeleniumSignal,
|
|
82
|
-
PhantomJSSignal,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Default signal instances that don't require user interaction.
|
|
87
|
-
* These are suitable for immediate detection on page load.
|
|
88
|
-
*/
|
|
89
|
-
const defaultInstantSignals = [
|
|
90
|
-
new WebDriverSignal(),
|
|
91
|
-
new HeadlessSignal(),
|
|
92
|
-
new NavigatorAnomalySignal(),
|
|
93
|
-
new PermissionsSignal(),
|
|
94
|
-
new PluginsSignal(),
|
|
95
|
-
new WebGLSignal(),
|
|
96
|
-
new CanvasSignal(),
|
|
97
|
-
new AudioContextSignal(),
|
|
98
|
-
new ScreenSignal(),
|
|
99
|
-
new PageLoadSignal(),
|
|
100
|
-
new DOMContentTimingSignal(),
|
|
101
|
-
new PuppeteerSignal(),
|
|
102
|
-
new PlaywrightSignal(),
|
|
103
|
-
new SeleniumSignal(),
|
|
104
|
-
new PhantomJSSignal(),
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Signal instances that require user interaction to be meaningful.
|
|
109
|
-
* These track mouse, keyboard, and scroll behavior over time.
|
|
110
|
-
*/
|
|
111
|
-
const defaultInteractionSignals = [
|
|
112
|
-
new MouseMovementSignal(),
|
|
113
|
-
new KeyboardPatternSignal(),
|
|
114
|
-
new InteractionTimingSignal(),
|
|
115
|
-
new ScrollBehaviorSignal(),
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* All default signal instances.
|
|
120
|
-
*/
|
|
121
|
-
const defaultSignals = [...defaultInstantSignals, ...defaultInteractionSignals];
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Create a BotDetector with all default signals registered.
|
|
125
|
-
* This is the recommended way to create a detector for most use cases.
|
|
126
|
-
*
|
|
127
|
-
* @param {Object} [options={}] - Configuration options
|
|
128
|
-
* @param {Object.<string, number>} [options.weightOverrides={}] - Override signal weights
|
|
129
|
-
* @param {number} [options.humanThreshold=20] - Score threshold for human verdict
|
|
130
|
-
* @param {number} [options.suspiciousThreshold=50] - Score threshold for suspicious verdict
|
|
131
|
-
* @param {Array<string>} [options.instantBotSignals=['webdriver', 'puppeteer', 'playwright', 'selenium', 'phantomjs']] - Signals that instantly flag as bot
|
|
132
|
-
* @param {boolean} [options.includeInteractionSignals=true] - Include signals requiring interaction
|
|
133
|
-
* @param {number} [options.detectionTimeout=5000] - Timeout for detection in ms
|
|
134
|
-
* @returns {BotDetector}
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* // Basic usage
|
|
138
|
-
* const detector = createDetector();
|
|
139
|
-
* const result = await detector.detect();
|
|
140
|
-
*
|
|
141
|
-
* @example
|
|
142
|
-
* // Custom thresholds
|
|
143
|
-
* const detector = createDetector({
|
|
144
|
-
* humanThreshold: 15,
|
|
145
|
-
* suspiciousThreshold: 40,
|
|
146
|
-
* });
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* // Skip interaction signals for instant detection
|
|
150
|
-
* const detector = createDetector({
|
|
151
|
-
* includeInteractionSignals: false,
|
|
152
|
-
* });
|
|
153
|
-
*/
|
|
154
|
-
function createDetector(options = {}) {
|
|
155
|
-
const {
|
|
156
|
-
includeInteractionSignals = true,
|
|
157
|
-
instantBotSignals = ['webdriver', 'puppeteer', 'playwright', 'selenium', 'phantomjs'],
|
|
158
|
-
...detectorOptions
|
|
159
|
-
} = options;
|
|
160
|
-
|
|
161
|
-
// singletons. If two detectors share the same signal instances, calling
|
|
162
|
-
// detector1.reset() would corrupt detector2's cached results.
|
|
163
|
-
const signalClasses = includeInteractionSignals
|
|
164
|
-
? [
|
|
165
|
-
WebDriverSignal, HeadlessSignal, NavigatorAnomalySignal, PermissionsSignal,
|
|
166
|
-
PluginsSignal, WebGLSignal, CanvasSignal, AudioContextSignal, ScreenSignal,
|
|
167
|
-
PageLoadSignal, DOMContentTimingSignal,
|
|
168
|
-
PuppeteerSignal, PlaywrightSignal, SeleniumSignal, PhantomJSSignal,
|
|
169
|
-
MouseMovementSignal, KeyboardPatternSignal, InteractionTimingSignal, ScrollBehaviorSignal,
|
|
170
|
-
]
|
|
171
|
-
: [
|
|
172
|
-
WebDriverSignal, HeadlessSignal, NavigatorAnomalySignal, PermissionsSignal,
|
|
173
|
-
PluginsSignal, WebGLSignal, CanvasSignal, AudioContextSignal, ScreenSignal,
|
|
174
|
-
PageLoadSignal, DOMContentTimingSignal,
|
|
175
|
-
PuppeteerSignal, PlaywrightSignal, SeleniumSignal, PhantomJSSignal,
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
const signals = signalClasses.map(Cls => new Cls());
|
|
179
|
-
|
|
180
|
-
return new BotDetector({
|
|
181
|
-
signals,
|
|
182
|
-
instantBotSignals,
|
|
183
|
-
...detectorOptions,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Quick detection function for simple use cases.
|
|
189
|
-
* Creates a detector, runs detection, and returns result.
|
|
190
|
-
*
|
|
191
|
-
* @param {Object} [options={}] - Detection options
|
|
192
|
-
* @param {boolean} [options.skipInteractionSignals=false] - Skip signals requiring interaction
|
|
193
|
-
* @returns {Promise<DetectionResult>}
|
|
194
|
-
*
|
|
195
|
-
* @example
|
|
196
|
-
* const result = await detect();
|
|
197
|
-
* if (result.verdict === 'bot') {
|
|
198
|
-
* console.log('Bot detected!', result.score);
|
|
199
|
-
* }
|
|
200
|
-
*/
|
|
201
|
-
async function detect(options = {}) {
|
|
202
|
-
const detector = createDetector({
|
|
203
|
-
includeInteractionSignals: !options.skipInteractionSignals,
|
|
204
|
-
});
|
|
205
|
-
return detector.detect(options);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Quick detection that only runs instant signals (no interaction required).
|
|
210
|
-
* Suitable for immediate detection on page load.
|
|
211
|
-
*
|
|
212
|
-
* @returns {Promise<DetectionResult>}
|
|
213
|
-
*
|
|
214
|
-
* @example
|
|
215
|
-
* document.addEventListener('DOMContentLoaded', async () => {
|
|
216
|
-
* const result = await detectInstant();
|
|
217
|
-
* console.log('Verdict:', result.verdict);
|
|
218
|
-
* });
|
|
219
|
-
*/
|
|
220
|
-
async function detectInstant() {
|
|
221
|
-
return detect({ skipInteractionSignals: true });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Export everything
|
|
225
|
-
export {
|
|
226
|
-
// Main class
|
|
227
|
-
BotDetector,
|
|
228
|
-
|
|
229
|
-
// Factory functions
|
|
230
|
-
createDetector,
|
|
231
|
-
detect,
|
|
232
|
-
detectInstant,
|
|
233
|
-
|
|
234
|
-
// Base classes
|
|
235
|
-
Signal,
|
|
236
|
-
ScoringEngine,
|
|
237
|
-
VerdictEngine,
|
|
238
|
-
|
|
239
|
-
// Enums
|
|
240
|
-
Verdict,
|
|
241
|
-
|
|
242
|
-
// Signal classes (for custom registration)
|
|
243
|
-
Signals,
|
|
244
|
-
|
|
245
|
-
// Individual signals
|
|
246
|
-
WebDriverSignal,
|
|
247
|
-
HeadlessSignal,
|
|
248
|
-
NavigatorAnomalySignal,
|
|
249
|
-
PermissionsSignal,
|
|
250
|
-
MouseMovementSignal,
|
|
251
|
-
KeyboardPatternSignal,
|
|
252
|
-
InteractionTimingSignal,
|
|
253
|
-
ScrollBehaviorSignal,
|
|
254
|
-
PluginsSignal,
|
|
255
|
-
WebGLSignal,
|
|
256
|
-
CanvasSignal,
|
|
257
|
-
AudioContextSignal,
|
|
258
|
-
ScreenSignal,
|
|
259
|
-
PageLoadSignal,
|
|
260
|
-
DOMContentTimingSignal,
|
|
261
|
-
PuppeteerSignal,
|
|
262
|
-
PlaywrightSignal,
|
|
263
|
-
SeleniumSignal,
|
|
264
|
-
PhantomJSSignal,
|
|
265
|
-
|
|
266
|
-
// Default signal instances
|
|
267
|
-
defaultSignals,
|
|
268
|
-
defaultInstantSignals,
|
|
269
|
-
defaultInteractionSignals,
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
// Default export for convenience
|
|
273
|
-
export default {
|
|
274
|
-
BotDetector,
|
|
275
|
-
createDetector,
|
|
276
|
-
detect,
|
|
277
|
-
detectInstant,
|
|
278
|
-
Signal,
|
|
279
|
-
Signals,
|
|
280
|
-
Verdict,
|
|
281
|
-
};
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects PhantomJS-specific artifacts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects artifacts left by PhantomJS.
|
|
9
|
-
* PhantomJS is an older headless browser that leaves specific traces.
|
|
10
|
-
*/
|
|
11
|
-
class PhantomJSSignal extends Signal {
|
|
12
|
-
static id = 'phantomjs';
|
|
13
|
-
static category = 'automation';
|
|
14
|
-
static weight = 1.0;
|
|
15
|
-
static description = 'Detects PhantomJS automation artifacts';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const indicators = [];
|
|
19
|
-
let confidence = 0;
|
|
20
|
-
|
|
21
|
-
// Check for PhantomJS globals
|
|
22
|
-
if (window.callPhantom) {
|
|
23
|
-
indicators.push('callPhantom');
|
|
24
|
-
confidence = Math.max(confidence, 1.0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (window._phantom) {
|
|
28
|
-
indicators.push('_phantom');
|
|
29
|
-
confidence = Math.max(confidence, 1.0);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (window.phantom) {
|
|
33
|
-
indicators.push('phantom');
|
|
34
|
-
confidence = Math.max(confidence, 1.0);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check for PhantomJS in user agent
|
|
38
|
-
const ua = navigator.userAgent || '';
|
|
39
|
-
if (ua.includes('PhantomJS')) {
|
|
40
|
-
indicators.push('phantomjs-ua');
|
|
41
|
-
confidence = Math.max(confidence, 1.0);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check for PhantomJS specific properties
|
|
45
|
-
if (window.__phantomas) {
|
|
46
|
-
indicators.push('phantomas');
|
|
47
|
-
confidence = Math.max(confidence, 1.0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Check for CasperJS (built on PhantomJS)
|
|
51
|
-
if (window.__casper) {
|
|
52
|
-
indicators.push('casperjs');
|
|
53
|
-
confidence = Math.max(confidence, 1.0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (window.casper) {
|
|
57
|
-
indicators.push('casper-global');
|
|
58
|
-
confidence = Math.max(confidence, 1.0);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check for SlimerJS (PhantomJS alternative)
|
|
62
|
-
if (window.slimer) {
|
|
63
|
-
indicators.push('slimerjs');
|
|
64
|
-
confidence = Math.max(confidence, 1.0);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Check for NightmareJS (Electron-based, similar patterns)
|
|
68
|
-
if (window.__nightmare) {
|
|
69
|
-
indicators.push('nightmare');
|
|
70
|
-
confidence = Math.max(confidence, 1.0);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (window.nightmare) {
|
|
74
|
-
indicators.push('nightmare-global');
|
|
75
|
-
confidence = Math.max(confidence, 1.0);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Check for function sources containing PhantomJS
|
|
79
|
-
try {
|
|
80
|
-
const funcString = Function.prototype.toString.call(Function);
|
|
81
|
-
if (funcString.includes('phantom') || funcString.includes('Phantom')) {
|
|
82
|
-
indicators.push('function-prototype-phantom');
|
|
83
|
-
confidence = Math.max(confidence, 0.8);
|
|
84
|
-
}
|
|
85
|
-
} catch (e) {
|
|
86
|
-
// Ignore errors
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Check for PhantomJS-specific behaviors
|
|
90
|
-
// PhantomJS has a specific way of handling errors
|
|
91
|
-
try {
|
|
92
|
-
throw new Error('test');
|
|
93
|
-
} catch (e) {
|
|
94
|
-
const stack = e.stack || '';
|
|
95
|
-
if (stack.includes('phantom')) {
|
|
96
|
-
indicators.push('stack-trace-phantom');
|
|
97
|
-
confidence = Math.max(confidence, 0.9);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Check for PhantomJS-specific plugin handling
|
|
102
|
-
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
103
|
-
// Combined with other PhantomJS indicators
|
|
104
|
-
if (indicators.length > 0) {
|
|
105
|
-
indicators.push('no-plugins-phantom');
|
|
106
|
-
confidence = Math.max(confidence, 0.5);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Check for specific PhantomJS window properties.
|
|
111
|
-
const phantomProps = [
|
|
112
|
-
'__PHANTOM__',
|
|
113
|
-
'PHANTOM',
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
for (const prop of phantomProps) {
|
|
117
|
-
if (prop in window) {
|
|
118
|
-
indicators.push(`phantom-prop-${prop.toLowerCase()}`);
|
|
119
|
-
confidence = Math.max(confidence, 0.9);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check for QtWebKit (PhantomJS engine)
|
|
124
|
-
if (ua.includes('QtWebKit')) {
|
|
125
|
-
indicators.push('qtwebkit');
|
|
126
|
-
confidence = Math.max(confidence, 0.7);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const triggered = indicators.length > 0;
|
|
130
|
-
|
|
131
|
-
return this.createResult(triggered, { indicators }, confidence);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export { PhantomJSSignal };
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Detects Playwright-specific artifacts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Signal } from '../../core/Signal.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detects artifacts left by Playwright automation.
|
|
9
|
-
* Playwright injects specific objects and leaves traces.
|
|
10
|
-
*/
|
|
11
|
-
class PlaywrightSignal extends Signal {
|
|
12
|
-
static id = 'playwright';
|
|
13
|
-
static category = 'automation';
|
|
14
|
-
static weight = 1.0;
|
|
15
|
-
static description = 'Detects Playwright automation artifacts';
|
|
16
|
-
|
|
17
|
-
async detect() {
|
|
18
|
-
const indicators = [];
|
|
19
|
-
let confidence = 0;
|
|
20
|
-
|
|
21
|
-
// Check for Playwright namespace
|
|
22
|
-
if (window.__playwright) {
|
|
23
|
-
indicators.push('playwright-namespace');
|
|
24
|
-
confidence = Math.max(confidence, 1.0);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check for Playwright-injected objects
|
|
28
|
-
const playwrightGlobals = [
|
|
29
|
-
'__playwright',
|
|
30
|
-
'__pw_manual',
|
|
31
|
-
'__pwInitScripts',
|
|
32
|
-
'playwright',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
for (const global of playwrightGlobals) {
|
|
36
|
-
if (global in window) {
|
|
37
|
-
indicators.push(`global-${global}`);
|
|
38
|
-
confidence = Math.max(confidence, 1.0);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Check for Playwright's binding pattern
|
|
43
|
-
if (window.__playwright__binding__) {
|
|
44
|
-
indicators.push('playwright-binding');
|
|
45
|
-
confidence = Math.max(confidence, 1.0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Check for Playwright-specific user agent markers
|
|
49
|
-
const ua = navigator.userAgent || '';
|
|
50
|
-
if (ua.includes('Playwright') || ua.includes('HeadlessChrome')) {
|
|
51
|
-
indicators.push('playwright-ua-marker');
|
|
52
|
-
confidence = Math.max(confidence, ua.includes('Playwright') ? 1.0 : 0.7);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check for Playwright's evaluate scope pattern
|
|
56
|
-
try {
|
|
57
|
-
// Playwright injects __pwBinding__ functions
|
|
58
|
-
const windowKeys = Object.keys(window);
|
|
59
|
-
const pwBindings = windowKeys.filter(k => k.startsWith('__pw'));
|
|
60
|
-
|
|
61
|
-
if (pwBindings.length > 0) {
|
|
62
|
-
indicators.push('pw-bindings');
|
|
63
|
-
confidence = Math.max(confidence, 1.0);
|
|
64
|
-
}
|
|
65
|
-
} catch (e) {
|
|
66
|
-
// Ignore errors
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check for Playwright's typical initialization patterns
|
|
70
|
-
if (typeof window.__pw_date_intercepted !== 'undefined') {
|
|
71
|
-
indicators.push('date-interception');
|
|
72
|
-
confidence = Math.max(confidence, 0.9);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check for Playwright's geolocation mock
|
|
76
|
-
if (window.__pw_geolocation__) {
|
|
77
|
-
indicators.push('geolocation-mock');
|
|
78
|
-
confidence = Math.max(confidence, 0.9);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check for Playwright's permission override
|
|
82
|
-
if (window.__pw_permissions__) {
|
|
83
|
-
indicators.push('permissions-override');
|
|
84
|
-
confidence = Math.max(confidence, 0.9);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check CDP session artifacts
|
|
88
|
-
if (window.__cdpSession__) {
|
|
89
|
-
indicators.push('cdp-session');
|
|
90
|
-
confidence = Math.max(confidence, 0.8);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Check for error stack traces containing Playwright
|
|
94
|
-
try {
|
|
95
|
-
throw new Error('stack trace test');
|
|
96
|
-
} catch (e) {
|
|
97
|
-
const stack = e.stack || '';
|
|
98
|
-
if (stack.includes('playwright') || stack.includes('__pw')) {
|
|
99
|
-
indicators.push('stack-trace-playwright');
|
|
100
|
-
confidence = Math.max(confidence, 0.8);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Check for Playwright's locale/timezone mocking
|
|
105
|
-
try {
|
|
106
|
-
const date = new Date();
|
|
107
|
-
const localeString = date.toLocaleString();
|
|
108
|
-
// Playwright often mocks timezone
|
|
109
|
-
if (window.__pwTimezone__) {
|
|
110
|
-
indicators.push('timezone-mock');
|
|
111
|
-
confidence = Math.max(confidence, 0.8);
|
|
112
|
-
}
|
|
113
|
-
} catch (e) {
|
|
114
|
-
// Ignore errors
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const triggered = indicators.length > 0;
|
|
118
|
-
|
|
119
|
-
return this.createResult(triggered, { indicators }, confidence);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export { PlaywrightSignal };
|