@riavzon/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.
package/dist/main.cjs ADDED
@@ -0,0 +1,3236 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_chunk = require('./chunk-C0xms8kb.cjs');
3
+ let _riavzon_utils = require("@riavzon/utils");
4
+ let zod = require("zod");
5
+ zod = require_chunk.__toESM(zod);
6
+ let maxmind = require("maxmind");
7
+ maxmind = require_chunk.__toESM(maxmind);
8
+ let lmdb = require("lmdb");
9
+ let node_fs = require("node:fs");
10
+ node_fs = require_chunk.__toESM(node_fs);
11
+ let node_path = require("node:path");
12
+ node_path = require_chunk.__toESM(node_path);
13
+ let node_url = require("node:url");
14
+ let pino = require("pino");
15
+ let fs = require("fs");
16
+ let path = require("path");
17
+ path = require_chunk.__toESM(path);
18
+ let unstorage = require("unstorage");
19
+ let unstorage_drivers_memory = require("unstorage/drivers/memory");
20
+ unstorage_drivers_memory = require_chunk.__toESM(unstorage_drivers_memory);
21
+ let db0 = require("db0");
22
+ let consola = require("consola");
23
+ consola = require_chunk.__toESM(consola);
24
+ let crypto = require("crypto");
25
+ let ua_parser_js = require("ua-parser-js");
26
+ let ua_parser_js_helpers = require("ua-parser-js/helpers");
27
+ let ua_parser_js_extensions = require("ua-parser-js/extensions");
28
+ let node_net = require("node:net");
29
+ let node_dns_promises = require("node:dns/promises");
30
+ node_dns_promises = require_chunk.__toESM(node_dns_promises);
31
+ let magic_regexp = require("magic-regexp");
32
+ let url = require("url");
33
+ let child_process = require("child_process");
34
+ let perf_hooks = require("perf_hooks");
35
+ let express = require("express");
36
+ let _riavzon_shield_base = require("@riavzon/shield-base");
37
+
38
+ //#region src/botDetector/types/configSchema.ts
39
+ const dbConfigStorage = zod.default.custom((val) => {
40
+ if (typeof val !== "object" || val === null) return false;
41
+ return typeof val.driver === "string";
42
+ }, { message: "Database config must be an object with a valid \"driver\" string" });
43
+ const store = zod.default.object({ main: dbConfigStorage }).required().strict();
44
+ const cache = zod.default.custom((val) => {
45
+ if (typeof val !== "object" || val === null) return false;
46
+ return typeof val.driver === "string";
47
+ }, { message: "Storage must be an object with a valid \"driver\" string" });
48
+ const configSchema = zod.default.object({
49
+ store,
50
+ banScore: zod.default.number().max(100).min(0).default(100),
51
+ maxScore: zod.default.number().max(100).min(0).default(100),
52
+ restoredReputationPoints: zod.default.number().default(10),
53
+ setNewComputedScore: zod.default.boolean().default(false),
54
+ whiteList: zod.default.array(zod.default.union([
55
+ zod.default.ipv4(),
56
+ zod.default.ipv6(),
57
+ zod.default.string()
58
+ ])).optional().default([]),
59
+ checksTimeRateControl: zod.default.object({
60
+ checkEveryRequest: zod.default.boolean().default(true),
61
+ checkEvery: zod.default.number().default(1e3 * 60 * 5)
62
+ }).prefault({}),
63
+ batchQueue: zod.default.object({
64
+ flushIntervalMs: zod.default.number().default(5e3),
65
+ maxBufferSize: zod.default.number().default(100),
66
+ maxRetries: zod.default.number().default(3)
67
+ }).prefault({}),
68
+ storage: cache.optional(),
69
+ checkers: zod.default.object({
70
+ localeMapsCheck: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
71
+ enable: zod.default.literal(true),
72
+ penalties: zod.default.object({
73
+ ipAndHeaderMismatch: zod.default.number().default(20),
74
+ missingHeader: zod.default.number().default(20),
75
+ missingGeoData: zod.default.number().default(20),
76
+ malformedHeader: zod.default.number().default(30)
77
+ }).prefault({})
78
+ })]).prefault({ enable: true }),
79
+ knownBadUserAgents: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
80
+ enable: zod.default.literal(true),
81
+ penalties: zod.default.object({
82
+ criticalSeverity: zod.default.number().default(100),
83
+ highSeverity: zod.default.number().default(80),
84
+ mediumSeverity: zod.default.number().default(30),
85
+ lowSeverity: zod.default.number().default(10)
86
+ }).prefault({})
87
+ })]).prefault({ enable: true }),
88
+ enableIpChecks: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
89
+ enable: zod.default.literal(true),
90
+ penalties: zod.default.number().default(10)
91
+ })]).prefault({ enable: true }),
92
+ enableGoodBotsChecks: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
93
+ enable: zod.default.literal(true),
94
+ banUnlistedBots: zod.default.boolean().default(true),
95
+ penalties: zod.default.number().default(100)
96
+ })]).prefault({ enable: true }),
97
+ enableBehaviorRateCheck: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
98
+ enable: zod.default.literal(true),
99
+ behavioral_window: zod.default.number().default(6e4),
100
+ behavioral_threshold: zod.default.number().default(30),
101
+ penalties: zod.default.number().default(60)
102
+ })]).prefault({ enable: true }),
103
+ enableProxyIspCookiesChecks: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
104
+ enable: zod.default.literal(true),
105
+ penalties: zod.default.object({
106
+ cookieMissing: zod.default.number().default(80),
107
+ proxyDetected: zod.default.number().default(40),
108
+ multiSourceBonus2to3: zod.default.number().default(10),
109
+ multiSourceBonus4plus: zod.default.number().default(20),
110
+ hostingDetected: zod.default.number().default(50),
111
+ ispUnknown: zod.default.number().default(10),
112
+ orgUnknown: zod.default.number().default(10)
113
+ }).prefault({})
114
+ })]).prefault({ enable: true }),
115
+ enableUaAndHeaderChecks: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
116
+ enable: zod.default.literal(true),
117
+ penalties: zod.default.object({
118
+ headlessBrowser: zod.default.number().default(100),
119
+ shortUserAgent: zod.default.number().default(80),
120
+ tlsCheckFailed: zod.default.number().default(60),
121
+ badUaChecker: zod.default.boolean().default(true)
122
+ }).prefault({})
123
+ })]).prefault({ enable: true }),
124
+ enableBrowserAndDeviceChecks: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
125
+ enable: zod.default.literal(true),
126
+ penalties: zod.default.object({
127
+ cliOrLibrary: zod.default.number().default(100),
128
+ internetExplorer: zod.default.number().default(100),
129
+ linuxOs: zod.default.number().default(10),
130
+ impossibleBrowserCombinations: zod.default.number().default(30),
131
+ browserTypeUnknown: zod.default.number().default(10),
132
+ browserNameUnknown: zod.default.number().default(10),
133
+ desktopWithoutOS: zod.default.number().default(10),
134
+ deviceVendorUnknown: zod.default.number().default(10),
135
+ browserVersionUnknown: zod.default.number().default(10),
136
+ deviceModelUnknown: zod.default.number().default(5)
137
+ }).prefault({})
138
+ })]).prefault({ enable: true }),
139
+ enableGeoChecks: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
140
+ enable: zod.default.literal(true),
141
+ bannedCountries: zod.default.array(zod.default.string()).optional().default([]),
142
+ penalties: zod.default.object({
143
+ countryUnknown: zod.default.number().default(10),
144
+ regionUnknown: zod.default.number().default(10),
145
+ latLonUnknown: zod.default.number().default(10),
146
+ districtUnknown: zod.default.number().default(10),
147
+ cityUnknown: zod.default.number().default(10),
148
+ timezoneUnknown: zod.default.number().default(10),
149
+ subregionUnknown: zod.default.number().default(10),
150
+ phoneUnknown: zod.default.number().default(10),
151
+ continentUnknown: zod.default.number().default(10)
152
+ }).prefault({})
153
+ })]).prefault({ enable: true }),
154
+ enableKnownThreatsDetections: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
155
+ enable: zod.default.literal(true),
156
+ penalties: zod.default.object({
157
+ anonymiseNetwork: zod.default.number().default(20),
158
+ threatLevels: zod.default.object({
159
+ criticalLevel1: zod.default.number().default(40),
160
+ currentAttacksLevel2: zod.default.number().default(30),
161
+ threatLevel3: zod.default.number().default(20),
162
+ threatLevel4: zod.default.number().default(10)
163
+ }).prefault({})
164
+ }).prefault({})
165
+ })]).prefault({ enable: true }),
166
+ enableAsnClassification: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
167
+ enable: zod.default.literal(true),
168
+ penalties: zod.default.object({
169
+ contentClassification: zod.default.number().default(20),
170
+ unknownClassification: zod.default.number().default(10),
171
+ lowVisibilityPenalty: zod.default.number().default(10),
172
+ lowVisibilityThreshold: zod.default.number().default(15),
173
+ comboHostingLowVisibility: zod.default.number().default(20)
174
+ }).prefault({})
175
+ })]).prefault({ enable: true }),
176
+ enableTorAnalysis: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
177
+ enable: zod.default.literal(true),
178
+ penalties: zod.default.object({
179
+ runningNode: zod.default.number().default(15),
180
+ exitNode: zod.default.number().default(20),
181
+ webExitCapable: zod.default.number().default(15),
182
+ guardNode: zod.default.number().default(10),
183
+ badExit: zod.default.number().default(40),
184
+ obsoleteVersion: zod.default.number().default(10)
185
+ }).prefault({})
186
+ })]).prefault({ enable: true }),
187
+ enableTimezoneConsistency: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
188
+ enable: zod.default.literal(true),
189
+ penalties: zod.default.number().default(20)
190
+ })]).prefault({ enable: true }),
191
+ honeypot: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
192
+ enable: zod.default.literal(true),
193
+ paths: zod.default.array(zod.default.string()).default([])
194
+ })]).prefault({ enable: true }),
195
+ enableSessionCoherence: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
196
+ enable: zod.default.literal(true),
197
+ penalties: zod.default.object({
198
+ pathMismatch: zod.default.number().default(10),
199
+ missingReferer: zod.default.number().default(20),
200
+ domainMismatch: zod.default.number().default(30)
201
+ }).prefault({})
202
+ })]).prefault({ enable: true }),
203
+ enableVelocityFingerprint: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
204
+ enable: zod.default.literal(true),
205
+ cvThreshold: zod.default.number().default(.1),
206
+ penalties: zod.default.number().default(40)
207
+ })]).prefault({ enable: true }),
208
+ enableKnownBadIpsCheck: zod.default.discriminatedUnion("enable", [zod.default.object({ enable: zod.default.literal(false) }), zod.default.object({
209
+ enable: zod.default.literal(true),
210
+ highRiskPenalty: zod.default.number().default(30)
211
+ })]).prefault({ enable: true })
212
+ }),
213
+ generator: zod.default.object({
214
+ scoreThreshold: zod.default.number().default(70),
215
+ generateTypes: zod.default.boolean().default(false),
216
+ deleteAfterBuild: zod.default.boolean().default(false),
217
+ mmdbctlPath: zod.default.string().default("mmdbctl")
218
+ }).prefault({}),
219
+ headerOptions: zod.default.object({
220
+ weightPerMustHeader: zod.default.number().default(20),
221
+ missingBrowserEngine: zod.default.number().default(30),
222
+ postManOrInsomiaHeaders: zod.default.number().default(50),
223
+ AJAXHeaderExists: zod.default.number().default(30),
224
+ connectionHeaderIsClose: zod.default.number().default(20),
225
+ originHeaderIsNULL: zod.default.number().default(10),
226
+ originHeaderMismatch: zod.default.number().default(30),
227
+ omittedAcceptHeader: zod.default.number().default(30),
228
+ clientHintsMissingForBlink: zod.default.number().default(30),
229
+ teHeaderUnexpectedForBlink: zod.default.number().default(10),
230
+ clientHintsUnexpectedForGecko: zod.default.number().default(30),
231
+ teHeaderMissingForGecko: zod.default.number().default(20),
232
+ aggressiveCacheControlOnGet: zod.default.number().default(15),
233
+ crossSiteRequestMissingReferer: zod.default.number().default(10),
234
+ inconsistentSecFetchMode: zod.default.number().default(20),
235
+ hostMismatchWeight: zod.default.number().default(40)
236
+ }).prefault({}),
237
+ pathTraveler: zod.default.object({
238
+ maxIterations: zod.default.number().default(3),
239
+ maxPathLength: zod.default.number().default(1500),
240
+ pathLengthToLong: zod.default.number().default(100),
241
+ longDecoding: zod.default.number().default(100),
242
+ traversalDetected: zod.default.number().default(60)
243
+ }).prefault({}),
244
+ punishmentType: zod.default.object({ enableFireWallBan: zod.default.boolean().default(false) }).prefault({}),
245
+ logLevel: zod.default.enum([
246
+ "debug",
247
+ "info",
248
+ "warn",
249
+ "error",
250
+ "fatal"
251
+ ]).default("info")
252
+ });
253
+
254
+ //#endregion
255
+ //#region src/botDetector/db/findDataPath.ts
256
+ const __moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
257
+ function getLibraryRoot(currentDir = __moduleDir) {
258
+ if (node_fs.default.existsSync(node_path.default.join(currentDir, "package.json"))) return currentDir;
259
+ const parentDir = node_path.default.resolve(currentDir, "..");
260
+ if (parentDir === currentDir) throw new Error("Could not find library root");
261
+ return getLibraryRoot(parentDir);
262
+ }
263
+ function resolveDataPath(fileName) {
264
+ const root = getLibraryRoot();
265
+ const possiblePaths = [node_path.default.resolve(root, "_data-sources", fileName), node_path.default.resolve(root, "dist", "_data-sources", fileName)];
266
+ for (const path of possiblePaths) if (node_fs.default.existsSync(path)) return path;
267
+ if (fileName !== "banned.mmdb" && fileName !== "highRisk.mmdb") throw new Error(`[Bot Detector] Data file "${fileName}" not found. Run 'bot-detector init' to download required data files.`);
268
+ return "";
269
+ }
270
+
271
+ //#endregion
272
+ //#region src/botDetector/helpers/mmdbDataReaders.ts
273
+ var DataSources = class DataSources {
274
+ constructor(readers) {
275
+ this.readers = readers;
276
+ }
277
+ static async initialize() {
278
+ const options = { watchForUpdates: process.env.NODE_ENV !== "test" };
279
+ const [asn, city, country, goodBots, tor, proxy, fireholAnon, fireholLvl1, fireholLvl2, fireholLvl3, fireholLvl4] = await Promise.all([
280
+ maxmind.default.open(resolveDataPath("asn.mmdb"), options),
281
+ maxmind.default.open(resolveDataPath("city.mmdb"), options),
282
+ maxmind.default.open(resolveDataPath("country.mmdb"), options),
283
+ maxmind.default.open(resolveDataPath("goodBots.mmdb"), options),
284
+ maxmind.default.open(resolveDataPath("tor.mmdb"), options),
285
+ maxmind.default.open(resolveDataPath("proxy.mmdb"), options),
286
+ maxmind.default.open(resolveDataPath("firehol_anonymous.mmdb"), options),
287
+ maxmind.default.open(resolveDataPath("firehol_l1.mmdb"), options),
288
+ maxmind.default.open(resolveDataPath("firehol_l2.mmdb"), options),
289
+ maxmind.default.open(resolveDataPath("firehol_l3.mmdb"), options),
290
+ maxmind.default.open(resolveDataPath("firehol_l4.mmdb"), options)
291
+ ]);
292
+ const bannedPath = resolveDataPath("banned.mmdb");
293
+ const highRiskPath = resolveDataPath("highRisk.mmdb");
294
+ const [banned, highRisk] = await Promise.all([(0, node_fs.existsSync)(bannedPath) ? maxmind.default.open(bannedPath, options) : Promise.resolve(void 0), (0, node_fs.existsSync)(highRiskPath) ? maxmind.default.open(highRiskPath, options) : Promise.resolve(void 0)]);
295
+ return new DataSources({
296
+ asn,
297
+ city,
298
+ country,
299
+ goodBots,
300
+ tor,
301
+ proxy,
302
+ fireholAnon,
303
+ fireholLvl1,
304
+ fireholLvl2,
305
+ fireholLvl3,
306
+ fireholLvl4,
307
+ banned,
308
+ highRisk,
309
+ userAgentLmdb: (0, lmdb.open)({
310
+ path: resolveDataPath("useragent-db/useragent.mdb"),
311
+ name: "useragent",
312
+ compression: true,
313
+ readOnly: true,
314
+ useVersions: true,
315
+ sharedStructuresKey: Symbol.for("structures"),
316
+ pageSize: 4096,
317
+ cache: { validated: true },
318
+ noReadAhead: true,
319
+ maxReaders: 2024
320
+ }),
321
+ ja4Lmdb: (0, lmdb.open)({
322
+ path: resolveDataPath("ja4-db/ja4.mdb"),
323
+ name: "ja4",
324
+ compression: true,
325
+ readOnly: true,
326
+ useVersions: true,
327
+ sharedStructuresKey: Symbol.for("structures"),
328
+ pageSize: 4096,
329
+ cache: { validated: true },
330
+ noReadAhead: true,
331
+ maxReaders: 2024
332
+ })
333
+ });
334
+ }
335
+ asnDataBase(ip) {
336
+ return this.readers.asn.get(ip);
337
+ }
338
+ cityDataBase(ip) {
339
+ return this.readers.city.get(ip);
340
+ }
341
+ countryDataBase(ip) {
342
+ return this.readers.country.get(ip);
343
+ }
344
+ goodBotsDataBase(ip) {
345
+ return this.readers.goodBots.get(ip);
346
+ }
347
+ torDataBase(ip) {
348
+ return this.readers.tor.get(ip);
349
+ }
350
+ proxyDataBase(ip) {
351
+ return this.readers.proxy.get(ip);
352
+ }
353
+ fireholAnonDataBase(ip) {
354
+ return this.readers.fireholAnon.get(ip);
355
+ }
356
+ fireholLvl1DataBase(ip) {
357
+ return this.readers.fireholLvl1.get(ip);
358
+ }
359
+ fireholLvl2DataBase(ip) {
360
+ return this.readers.fireholLvl2.get(ip);
361
+ }
362
+ fireholLvl3DataBase(ip) {
363
+ return this.readers.fireholLvl3.get(ip);
364
+ }
365
+ fireholLvl4DataBase(ip) {
366
+ return this.readers.fireholLvl4.get(ip);
367
+ }
368
+ bannedDataBase(ip) {
369
+ return this.readers.banned?.get(ip) ?? null;
370
+ }
371
+ highRiskDataBase(ip) {
372
+ return this.readers.highRisk?.get(ip) ?? null;
373
+ }
374
+ getUserAgentLmdb() {
375
+ return this.readers.userAgentLmdb;
376
+ }
377
+ getJa4Lmdb() {
378
+ return this.readers.ja4Lmdb;
379
+ }
380
+ };
381
+
382
+ //#endregion
383
+ //#region src/botDetector/utils/logger.ts
384
+ const LOG_DIR = path.default.resolve(process.cwd(), process.env.LOG_DIR || "bot-detector-logs");
385
+ if (!(0, fs.existsSync)(LOG_DIR)) (0, fs.mkdirSync)(LOG_DIR, { recursive: true });
386
+ const transport = pino.pino.transport({ targets: [
387
+ {
388
+ target: "pino/file",
389
+ level: "info",
390
+ options: {
391
+ destination: `${LOG_DIR}/info.log`,
392
+ mkdir: true
393
+ }
394
+ },
395
+ {
396
+ target: "pino/file",
397
+ level: "warn",
398
+ options: {
399
+ destination: `${LOG_DIR}/warn.log`,
400
+ mkdir: true
401
+ }
402
+ },
403
+ {
404
+ target: "pino/file",
405
+ level: "error",
406
+ options: {
407
+ destination: `${LOG_DIR}/errors.log`,
408
+ mkdir: true
409
+ }
410
+ }
411
+ ] });
412
+ let logger;
413
+ function getLogger() {
414
+ if (logger) return logger;
415
+ const { logLevel } = getConfiguration();
416
+ logger = (0, pino.pino)({
417
+ level: logLevel,
418
+ timestamp: pino.pino.stdTimeFunctions.isoTime,
419
+ mixin() {
420
+ return { uptime: process.uptime() };
421
+ },
422
+ redact: {
423
+ paths: [
424
+ "*.password",
425
+ "*.email",
426
+ "name",
427
+ "Name",
428
+ "*.cookies",
429
+ "*.cookie",
430
+ "cookies",
431
+ "cookie",
432
+ "*.accessToken",
433
+ "*.refresh_token",
434
+ "*.secret"
435
+ ],
436
+ censor: "[SECRET]"
437
+ }
438
+ }, transport);
439
+ return logger;
440
+ }
441
+
442
+ //#endregion
443
+ //#region src/botDetector/db/dialectUtils.ts
444
+ function isMySQL(db) {
445
+ return db.dialect === "mysql";
446
+ }
447
+ function isSQLite(db) {
448
+ return db.dialect === "sqlite";
449
+ }
450
+ /** Convert ? placeholders to $1, $2, for PostgreSQL. */
451
+ function prep(db, sql) {
452
+ if (db.dialect !== "postgresql") return db.prepare(sql);
453
+ let i = 0;
454
+ return db.prepare(sql.replace(/\?/g, () => `$${String(++i)}`));
455
+ }
456
+ /** Generate positional placeholders for a dynamic list of values. */
457
+ function placeholders(db, count, offset = 0) {
458
+ return Array.from({ length: count }, (_, i) => db.dialect === "postgresql" ? `$${String(offset + i + 1)}` : "?").join(", ");
459
+ }
460
+ /** NOW() equivalent per dialect. */
461
+ function now(db) {
462
+ return isSQLite(db) ? "datetime('now')" : "NOW()";
463
+ }
464
+ /**
465
+ * Upsert conflict clause.
466
+ * MySQL: ON DUPLICATE KEY UPDATE
467
+ * Others: ON CONFLICT(pk) DO UPDATE SET
468
+ */
469
+ function onUpsert(db, pk) {
470
+ return isMySQL(db) ? "ON DUPLICATE KEY UPDATE" : `ON CONFLICT(${pk}) DO UPDATE SET`;
471
+ }
472
+ /**
473
+ * Reference to the incoming row value in an upsert SET clause.
474
+ * MySQL: VALUES(col)
475
+ * Others: excluded.col
476
+ */
477
+ function excluded(db, col) {
478
+ return isMySQL(db) ? `VALUES(${col})` : `excluded.${col}`;
479
+ }
480
+
481
+ //#endregion
482
+ //#region src/botDetector/db/updateBanned.ts
483
+ async function updateBannedIP(cookie, ipAddress, country, user_agent, info) {
484
+ const db = getDb();
485
+ const log = getLogger().child({
486
+ service: "BOT DETECTOR",
487
+ branch: "db",
488
+ type: "updateBannedIP"
489
+ });
490
+ const params = [
491
+ cookie,
492
+ ipAddress,
493
+ country,
494
+ user_agent,
495
+ JSON.stringify(info.reasons),
496
+ info.score
497
+ ];
498
+ const ex = (col) => excluded(db, col);
499
+ const upsert = onUpsert(db, "canary_id");
500
+ try {
501
+ await prep(db, `INSERT INTO banned (canary_id, ip_address, country, user_agent, reason, score)
502
+ VALUES (?, ?, ?, ?, ?, ?)
503
+ ${upsert}
504
+ ip_address = ${ex("ip_address")},
505
+ country = ${ex("country")},
506
+ user_agent = ${ex("user_agent")},
507
+ score = ${ex("score")},
508
+ reason = ${ex("reason")}`).run(...params);
509
+ log.info(`Updated Database TABLE - banned. A user has been banned for IP ${ipAddress} (score ${String(info.score)})`);
510
+ } catch (err) {
511
+ log.error({ error: err }, "ERROR UPDATING \"banned\" TABLE");
512
+ throw err;
513
+ }
514
+ }
515
+
516
+ //#endregion
517
+ //#region src/botDetector/db/updateIsBot.ts
518
+ async function updateIsBot(isBot, cookie) {
519
+ const params = [isBot, cookie];
520
+ const db = getDb();
521
+ const log = getLogger().child({
522
+ service: "BOT DETECTOR",
523
+ branch: "db",
524
+ type: "updateIsBot"
525
+ });
526
+ try {
527
+ await prep(db, `UPDATE visitors SET is_bot = ? WHERE canary_id = ?`).run(...params);
528
+ } catch (err) {
529
+ log.error({ error: err }, "ERROR UPDATING IS_BOT");
530
+ throw err;
531
+ }
532
+ }
533
+
534
+ //#endregion
535
+ //#region src/botDetector/db/updateVisitors.ts
536
+ async function updateVisitor(u) {
537
+ const db = getDb();
538
+ const log = getLogger().child({
539
+ service: "BOT DETECTOR",
540
+ branch: "db",
541
+ type: "updateVisitors"
542
+ });
543
+ const { visitorId, cookie, ipAddress, userAgent, country, region, regionName, city, district, lat, lon, timezone, currency, isp, org, as: asOrg, device_type, browser, proxy, hosting, is_bot, first_seen, last_seen, request_count, deviceVendor, deviceModel, browserType, browserVersion, os, activity_score } = u;
544
+ const params = [
545
+ visitorId,
546
+ cookie,
547
+ ipAddress,
548
+ userAgent,
549
+ country,
550
+ region,
551
+ regionName,
552
+ city,
553
+ district,
554
+ lat,
555
+ lon,
556
+ timezone,
557
+ currency,
558
+ isp,
559
+ org,
560
+ asOrg,
561
+ device_type,
562
+ browser,
563
+ proxy,
564
+ hosting,
565
+ is_bot,
566
+ first_seen,
567
+ last_seen,
568
+ request_count,
569
+ deviceVendor,
570
+ deviceModel,
571
+ browserType,
572
+ browserVersion,
573
+ os,
574
+ Number(activity_score) || 0
575
+ ].map((value) => value === void 0 ? null : value);
576
+ const ex = (col) => excluded(db, col);
577
+ const upsert = onUpsert(db, "canary_id");
578
+ try {
579
+ await prep(db, `INSERT INTO visitors (
580
+ visitor_id,
581
+ canary_id,
582
+ ip_address,
583
+ user_agent,
584
+ country,
585
+ region,
586
+ region_name,
587
+ city,
588
+ district,
589
+ lat,
590
+ lon,
591
+ timezone,
592
+ currency,
593
+ isp,
594
+ org,
595
+ as_org,
596
+ device_type,
597
+ browser,
598
+ proxy,
599
+ hosting,
600
+ is_bot,
601
+ first_seen,
602
+ last_seen,
603
+ request_count,
604
+ deviceVendor,
605
+ deviceModel,
606
+ browserType,
607
+ browserVersion,
608
+ os,
609
+ suspicious_activity_score
610
+ ) VALUES (
611
+ ${params.map(() => "?").join(", ")}
612
+ )
613
+ ${upsert}
614
+ ip_address = ${ex("ip_address")},
615
+ user_agent = ${ex("user_agent")},
616
+ country = ${ex("country")},
617
+ region = ${ex("region")},
618
+ region_name = ${ex("region_name")},
619
+ city = ${ex("city")},
620
+ district = ${ex("district")},
621
+ lat = ${ex("lat")},
622
+ lon = ${ex("lon")},
623
+ timezone = ${ex("timezone")},
624
+ currency = ${ex("currency")},
625
+ isp = ${ex("isp")},
626
+ org = ${ex("org")},
627
+ as_org = ${ex("as_org")},
628
+ device_type = ${ex("device_type")},
629
+ browser = ${ex("browser")},
630
+ proxy = ${ex("proxy")},
631
+ hosting = ${ex("hosting")},
632
+ last_seen = ${now(db)},
633
+ request_count = request_count + 1,
634
+ deviceVendor = ${ex("deviceVendor")},
635
+ deviceModel = ${ex("deviceModel")},
636
+ browserType = ${ex("browserType")},
637
+ browserVersion = ${ex("browserVersion")},
638
+ os = ${ex("os")}`).run(...params);
639
+ log.info(`Updated visitors table, Visitor row for canary_id=${cookie ?? ""} inserted/updated successfully.`);
640
+ return;
641
+ } catch (err) {
642
+ log.error({ error: err }, `ERROR UPDATING visitors TABLE`);
643
+ }
644
+ }
645
+
646
+ //#endregion
647
+ //#region src/botDetector/db/updateVisitorScore.ts
648
+ async function updateScore(score, cookie) {
649
+ const params = [score, cookie];
650
+ const db = getDb();
651
+ const log = getLogger().child({
652
+ service: "BOT DETECTOR",
653
+ branch: "db",
654
+ type: "updateScore"
655
+ });
656
+ try {
657
+ await prep(db, `UPDATE visitors SET suspicious_activity_score = ? WHERE canary_id = ?`).run(...params);
658
+ } catch (err) {
659
+ log.error({ error: err }, "ERROR UPDATING SCORE");
660
+ throw err;
661
+ }
662
+ }
663
+
664
+ //#endregion
665
+ //#region src/botDetector/db/batchQueue.ts
666
+ var BatchQueue = class {
667
+ constructor() {
668
+ this.jobs = /* @__PURE__ */ new Map();
669
+ this.timer = null;
670
+ this.flushPromise = null;
671
+ }
672
+ get config() {
673
+ return getConfiguration().batchQueue;
674
+ }
675
+ get log() {
676
+ return getLogger().child({
677
+ service: "BOT DETECTOR",
678
+ branch: "BatchQueue"
679
+ });
680
+ }
681
+ async addQueue(canary, ipAddress, type, params, priority = "deferred") {
682
+ const key = `${type}:${canary}:${ipAddress}`;
683
+ this.jobs.set(key, {
684
+ id: key,
685
+ type,
686
+ priority,
687
+ params
688
+ });
689
+ if (priority === "immediate" || this.jobs.size >= this.config.maxBufferSize) await this.flush();
690
+ else this.timer ??= setTimeout(() => void this.flush(), this.config.flushIntervalMs);
691
+ }
692
+ async flush() {
693
+ while (this.flushPromise || this.jobs.size > 0) {
694
+ if (this.flushPromise) await this.flushPromise;
695
+ if (this.jobs.size > 0) {
696
+ if (this.timer) {
697
+ clearTimeout(this.timer);
698
+ this.timer = null;
699
+ }
700
+ const currentBatch = Array.from(this.jobs.values());
701
+ this.jobs.clear();
702
+ this.flushPromise = this.executeBatch(currentBatch, 0);
703
+ try {
704
+ await this.flushPromise;
705
+ } finally {
706
+ this.flushPromise = null;
707
+ }
708
+ }
709
+ }
710
+ }
711
+ runJob(job) {
712
+ switch (job.type) {
713
+ case "visitor_upsert": return updateVisitor(job.params.insert);
714
+ case "score_update": {
715
+ const { score, cookie } = job.params;
716
+ return updateScore(score, cookie);
717
+ }
718
+ case "is_bot_update": {
719
+ const { isBot, cookie } = job.params;
720
+ return updateIsBot(isBot, cookie);
721
+ }
722
+ case "update_banned_ip": {
723
+ const { cookie, ipAddress, country, user_agent, info } = job.params;
724
+ return updateBannedIP(cookie, ipAddress, country, user_agent, info);
725
+ }
726
+ }
727
+ }
728
+ async executeBatch(batch, retryCount) {
729
+ try {
730
+ const visitors = batch.filter((j) => j.type === "visitor_upsert");
731
+ const others = batch.filter((j) => j.type !== "visitor_upsert");
732
+ if (visitors.length > 0) await Promise.all(visitors.map((j) => this.runJob(j)));
733
+ await Promise.all(others.map((j) => this.runJob(j)));
734
+ } catch (err) {
735
+ this.log.error({ err }, `Batch flush failed (Attempt ${String(retryCount + 1)})`);
736
+ if (retryCount < this.config.maxRetries) {
737
+ await new Promise((res) => setTimeout(res, 1e3));
738
+ return this.executeBatch(batch, retryCount + 1);
739
+ }
740
+ this.log.error("Max retries reached. Discarding batch.");
741
+ }
742
+ }
743
+ async shutdown() {
744
+ this.log.info("Shutting down BatchQueue: Draining remaining jobs...");
745
+ await this.flush();
746
+ }
747
+ };
748
+
749
+ //#endregion
750
+ //#region src/botDetector/config/storageAdapter.ts
751
+ async function initStorage(config) {
752
+ if (!config) return (0, unstorage.createStorage)({ driver: (0, unstorage_drivers_memory.default)() });
753
+ const { driver, ...opts } = config;
754
+ let mod;
755
+ switch (driver) {
756
+ case "redis":
757
+ mod = await import("unstorage/drivers/redis");
758
+ break;
759
+ case "upstash":
760
+ mod = await import("unstorage/drivers/upstash");
761
+ break;
762
+ case "lru":
763
+ mod = await import("unstorage/drivers/lru-cache");
764
+ break;
765
+ case "fs":
766
+ mod = await import("unstorage/drivers/fs-lite");
767
+ break;
768
+ case "cloudflare-kv-binding":
769
+ mod = await import("unstorage/drivers/cloudflare-kv-binding");
770
+ break;
771
+ case "cloudflare-kv-http":
772
+ mod = await import("unstorage/drivers/cloudflare-kv-http");
773
+ break;
774
+ case "cloudflare-r2-binding":
775
+ mod = await import("unstorage/drivers/cloudflare-r2-binding");
776
+ break;
777
+ case "vercel":
778
+ mod = await import("unstorage/drivers/vercel-runtime-cache");
779
+ break;
780
+ default: throw new Error(`Unsupported storage driver: ${driver}`);
781
+ }
782
+ return (0, unstorage.createStorage)({ driver: mod.default(opts) });
783
+ }
784
+
785
+ //#endregion
786
+ //#region src/botDetector/config/dbAdapter.ts
787
+ async function initDb(config) {
788
+ const { driver, ...opts } = config;
789
+ let mod;
790
+ switch (driver) {
791
+ case "mysql-pool":
792
+ mod = await Promise.resolve().then(() => require("./mysqlPoolConnector-D1eVEhRK.cjs"));
793
+ break;
794
+ case "postgresql":
795
+ mod = await import("db0/connectors/postgresql");
796
+ break;
797
+ case "sqlite":
798
+ mod = await import("db0/connectors/better-sqlite3");
799
+ break;
800
+ case "cloudflare-d1":
801
+ mod = await import("db0/connectors/cloudflare-d1");
802
+ break;
803
+ case "planetscale":
804
+ mod = await import("db0/connectors/planetscale");
805
+ break;
806
+ default: throw new Error(`Unsupported database driver: ${driver}`);
807
+ }
808
+ return (0, db0.createDatabase)(mod.default(opts));
809
+ }
810
+
811
+ //#endregion
812
+ //#region src/botDetector/checkers/CheckerRegistry.ts
813
+ let registeredCheckers = [];
814
+ const CheckerRegistry = {
815
+ register(checker) {
816
+ consola.default.log(`Loaded plugin: ${checker.name}`);
817
+ registeredCheckers.push(checker);
818
+ },
819
+ getEnabled(phase, config) {
820
+ return registeredCheckers.filter((checker) => checker.phase === phase && checker.isEnabled(config));
821
+ },
822
+ clear() {
823
+ registeredCheckers = [];
824
+ }
825
+ };
826
+
827
+ //#endregion
828
+ //#region src/botDetector/checkers/badUaChecker.ts
829
+ const SEVERITY_ORDER = [
830
+ "critical",
831
+ "high",
832
+ "medium",
833
+ "low"
834
+ ];
835
+ let patterns = [];
836
+ function loadUaPatterns() {
837
+ const db = getDataSources().getUserAgentLmdb();
838
+ const bucketStrings = new Map(SEVERITY_ORDER.map((sev) => [sev, []]));
839
+ for (const { value } of db.getRange({ limit: 1e4 })) {
840
+ const sev = value.metadata_severity;
841
+ bucketStrings.get(sev)?.push(value.useragent_rx);
842
+ }
843
+ patterns = SEVERITY_ORDER.map((severity) => {
844
+ const patterns = bucketStrings.get(severity) ?? [];
845
+ if (patterns.length === 0) return null;
846
+ const combined = patterns.map((p) => `(?:${p})`).join("|");
847
+ return {
848
+ rx: new RegExp(combined, "i"),
849
+ severity
850
+ };
851
+ }).filter((p) => p !== null);
852
+ }
853
+ var BadUaChecker = class {
854
+ constructor() {
855
+ this.name = "Bad User Agent list";
856
+ this.phase = "heavy";
857
+ }
858
+ isEnabled(config) {
859
+ return config.checkers.knownBadUserAgents.enable;
860
+ }
861
+ run(ctx, config) {
862
+ const { knownBadUserAgents } = config.checkers;
863
+ const reasons = [];
864
+ let score = 0;
865
+ if (!knownBadUserAgents.enable) return {
866
+ score,
867
+ reasons
868
+ };
869
+ if (patterns.length === 0) loadUaPatterns();
870
+ const rawUa = ctx.req.get("User-Agent") ?? "";
871
+ for (const { rx, severity } of patterns) if (rx.test(rawUa)) {
872
+ reasons.push("BAD_UA_DETECTED");
873
+ switch (severity) {
874
+ case "critical":
875
+ score += knownBadUserAgents.penalties.criticalSeverity;
876
+ break;
877
+ case "high":
878
+ score += knownBadUserAgents.penalties.highSeverity;
879
+ break;
880
+ case "medium":
881
+ score += knownBadUserAgents.penalties.mediumSeverity;
882
+ break;
883
+ case "low":
884
+ score += knownBadUserAgents.penalties.lowSeverity;
885
+ break;
886
+ }
887
+ break;
888
+ }
889
+ return {
890
+ score,
891
+ reasons
892
+ };
893
+ }
894
+ };
895
+ CheckerRegistry.register(new BadUaChecker());
896
+
897
+ //#endregion
898
+ //#region src/botDetector/config/config.ts
899
+ const { defineConfiguration, getConfiguration } = (0, _riavzon_utils.createConfigManager)(configSchema, "Bot Detector");
900
+ let globalDataSources;
901
+ let globalBatchQueue;
902
+ let globalStorage;
903
+ let globalDb;
904
+ /**
905
+ * @description
906
+ * The bot detector library's configuration object.
907
+ * Contains the core configuration to make the library usable client side.
908
+ * @module jwtAuth/config
909
+ * @see {@link ./jwtAuth/types/configSchema.js}
910
+ */
911
+ async function configuration(config) {
912
+ const initDataSourcesTask = async () => {
913
+ globalDataSources ??= await DataSources.initialize();
914
+ loadUaPatterns();
915
+ };
916
+ const initBatchQueueTask = () => {
917
+ if (!globalBatchQueue) {
918
+ globalBatchQueue = new BatchQueue();
919
+ process.on("SIGTERM", () => {
920
+ globalBatchQueue?.shutdown();
921
+ });
922
+ process.on("SIGINT", () => {
923
+ globalBatchQueue?.shutdown();
924
+ });
925
+ }
926
+ };
927
+ const initStorageTask = async () => {
928
+ globalStorage ??= await initStorage(config.storage);
929
+ };
930
+ const initDbTask = async () => {
931
+ globalDb ??= await initDb(config.store.main);
932
+ };
933
+ await defineConfiguration(config, [
934
+ initDataSourcesTask,
935
+ initBatchQueueTask,
936
+ initStorageTask,
937
+ initDbTask
938
+ ]);
939
+ }
940
+ function getBatchQueue() {
941
+ if (!globalBatchQueue) {
942
+ consola.default.trace("Premature getBatchQueue() call");
943
+ throw new Error("BatchQueue not ready. Call configuration() first.");
944
+ }
945
+ return globalBatchQueue;
946
+ }
947
+ function getStorage() {
948
+ if (!globalStorage) {
949
+ consola.default.trace("Premature getStorage() call");
950
+ throw new Error("Storage not ready. Call configuration() first.");
951
+ }
952
+ return globalStorage;
953
+ }
954
+ function getDb() {
955
+ if (!globalDb) {
956
+ consola.default.trace("Premature getDb() call");
957
+ throw new Error("DB not ready. Call configuration() first.");
958
+ }
959
+ return globalDb;
960
+ }
961
+ function getDataSources() {
962
+ if (!globalDataSources) {
963
+ consola.default.trace("Premature getDataSources() call");
964
+ throw new Error(`##### Must be initialized globally #####
965
+ Bot Detector: DataSources not ready. Call configuration() first.`);
966
+ }
967
+ return globalDataSources;
968
+ }
969
+
970
+ //#endregion
971
+ //#region src/botDetector/helpers/getIPInformation.ts
972
+ const norm = (string) => string?.trim().toLowerCase();
973
+ function getData(ip) {
974
+ const dataSource = getDataSources();
975
+ const countryLvl = dataSource.countryDataBase(ip);
976
+ const cityLvl = dataSource.cityDataBase(ip);
977
+ const asn = dataSource.asnDataBase(ip);
978
+ const proxy = dataSource.proxyDataBase(ip);
979
+ const tor = dataSource.torDataBase(ip);
980
+ return {
981
+ country: norm(countryLvl?.name ?? cityLvl?.name),
982
+ countryCode: norm(countryLvl?.country_code ?? cityLvl?.country_code),
983
+ region: norm(cityLvl?.region ?? countryLvl?.region),
984
+ regionName: norm(cityLvl?.continent ?? cityLvl?.subregion ?? countryLvl?.subregion),
985
+ subregion: norm(cityLvl?.subregion ?? countryLvl?.subregion),
986
+ state: norm(cityLvl?.state),
987
+ zipCode: norm(cityLvl?.zip_code),
988
+ city: norm(cityLvl?.city ?? cityLvl?.capital ?? countryLvl?.capital),
989
+ phone: norm(cityLvl?.phone ?? countryLvl?.phone),
990
+ numericCode: norm(cityLvl?.numericCode ?? countryLvl?.numericCode),
991
+ native: norm(cityLvl?.native ?? countryLvl?.native),
992
+ continent: norm(cityLvl?.continent),
993
+ capital: norm(cityLvl?.capital ?? countryLvl?.capital),
994
+ district: norm(cityLvl?.state),
995
+ lat: norm(cityLvl?.latitude),
996
+ lon: norm(cityLvl?.longitude),
997
+ timezone: norm(cityLvl?.timezone ?? countryLvl?.timezone),
998
+ timeZoneName: norm(cityLvl?.timeZoneName ?? countryLvl?.timeZoneName),
999
+ utc_offset: norm(cityLvl?.utc_offset ?? countryLvl?.utc_offset),
1000
+ tld: norm(cityLvl?.tld ?? countryLvl?.tld),
1001
+ nationality: norm(cityLvl?.nationality ?? countryLvl?.nationality),
1002
+ currency: norm(cityLvl?.currency ?? countryLvl?.currency),
1003
+ iso639: norm(cityLvl?.iso639 ?? countryLvl?.iso639),
1004
+ languages: norm(cityLvl?.languages ?? countryLvl?.languages),
1005
+ isp: norm(asn?.asn_name),
1006
+ org: norm(asn?.asn_id),
1007
+ as_org: norm(asn?.asn_name),
1008
+ proxy: proxy ? true : false,
1009
+ hosting: asn?.classification === "Content" || Boolean(tor?.exit_addresses)
1010
+ };
1011
+ }
1012
+
1013
+ //#endregion
1014
+ //#region src/botDetector/utils/cookieGenerator.ts
1015
+ function makeCookie(res, name, value, options) {
1016
+ if (name.startsWith("__Host-")) {
1017
+ options.secure = true;
1018
+ options.path = "/";
1019
+ delete options.domain;
1020
+ }
1021
+ if (name.startsWith("__Secure-")) options.secure = true;
1022
+ res.cookie(name, value, {
1023
+ httpOnly: options.httpOnly,
1024
+ sameSite: options.sameSite,
1025
+ maxAge: options.maxAge,
1026
+ secure: options.secure,
1027
+ expires: options.expires,
1028
+ domain: options.domain,
1029
+ path: options.path
1030
+ });
1031
+ }
1032
+
1033
+ //#endregion
1034
+ //#region src/botDetector/helpers/UAparser.ts
1035
+ function parseUA(userAgent) {
1036
+ const botParser = new ua_parser_js.UAParser([
1037
+ ua_parser_js_extensions.Crawlers,
1038
+ ua_parser_js_extensions.CLIs,
1039
+ ua_parser_js_extensions.Fetchers,
1040
+ ua_parser_js_extensions.Libraries
1041
+ ]);
1042
+ const uaString = typeof userAgent === "string" ? userAgent : String(userAgent);
1043
+ const result = botParser.setUA(uaString).getResult();
1044
+ return {
1045
+ device: result.device.type ?? "desktop",
1046
+ deviceVendor: result.device.vendor,
1047
+ deviceModel: result.device.model,
1048
+ browser: result.browser.name,
1049
+ browserType: result.browser.type,
1050
+ browserVersion: result.browser.version,
1051
+ os: result.os.name,
1052
+ botAI: (0, ua_parser_js_helpers.isAIBot)(result),
1053
+ bot: (0, ua_parser_js_helpers.isBot)(result),
1054
+ allResults: result
1055
+ };
1056
+ }
1057
+
1058
+ //#endregion
1059
+ //#region src/botDetector/checkers/ipValidation.ts
1060
+ var IpChecker = class {
1061
+ constructor() {
1062
+ this.name = "IP Validation";
1063
+ this.phase = "cheap";
1064
+ }
1065
+ isEnabled(config) {
1066
+ return config.checkers.enableIpChecks.enable;
1067
+ }
1068
+ run(ctx, config) {
1069
+ const isValid = (0, node_net.isIP)(ctx.ipAddress) !== 0;
1070
+ return {
1071
+ score: isValid ? 0 : config.banScore,
1072
+ reasons: isValid ? [] : ["IP_INVALID"]
1073
+ };
1074
+ }
1075
+ };
1076
+ CheckerRegistry.register(new IpChecker());
1077
+
1078
+ //#endregion
1079
+ //#region src/botDetector/helpers/cache/dnsLookupCache.ts
1080
+ const DNS_TTL_SECONDS = 3600 * 2;
1081
+ const PREFIX$5 = "dns:";
1082
+ const dnsCache = {
1083
+ async get(ip) {
1084
+ return getStorage().getItem(`${PREFIX$5}${ip}`);
1085
+ },
1086
+ async set(ip, entry) {
1087
+ await getStorage().setItem(`${PREFIX$5}${ip}`, entry, { ttl: DNS_TTL_SECONDS });
1088
+ },
1089
+ async delete(ip) {
1090
+ await getStorage().removeItem(`${PREFIX$5}${ip}`);
1091
+ },
1092
+ async clear() {
1093
+ await getStorage().clear();
1094
+ }
1095
+ };
1096
+
1097
+ //#endregion
1098
+ //#region src/botDetector/checkers/goodBots/base.ts
1099
+ var GoodBotsBase = class {
1100
+ getDomains(suffix) {
1101
+ const allDomains = [];
1102
+ for (const key in suffix) {
1103
+ const entrySuffix = suffix[key].suffix;
1104
+ if (Array.isArray(entrySuffix)) allDomains.push(...entrySuffix);
1105
+ else if (typeof entrySuffix === "string") allDomains.push(entrySuffix);
1106
+ }
1107
+ return allDomains;
1108
+ }
1109
+ get logger() {
1110
+ this._logger ??= getLogger().child({
1111
+ service: "botDetector",
1112
+ branch: "checker",
1113
+ type: "GoodBotsBase"
1114
+ });
1115
+ return this._logger;
1116
+ }
1117
+ constructor(suffixes) {
1118
+ this.suffixes = suffixes;
1119
+ this.domains = this.getDomains(this.suffixes).map((d) => `.${d.toLowerCase()}`);
1120
+ }
1121
+ async isBotFromTrustedDomain(ip) {
1122
+ const cached = await dnsCache.get(ip);
1123
+ if (cached) return cached.trustedBot;
1124
+ try {
1125
+ const matchingHosts = (await node_dns_promises.default.reverse(ip)).filter((host) => this.domains.some((domain) => host.endsWith(domain)));
1126
+ if (matchingHosts.length === 0) {
1127
+ dnsCache.set(ip, {
1128
+ ip,
1129
+ trustedBot: false
1130
+ }).catch((err) => {
1131
+ this.logger.error({ err }, "Failed to save dnsCache in storage");
1132
+ });
1133
+ return false;
1134
+ }
1135
+ for (const host of matchingHosts) if ((await node_dns_promises.default.lookup(host, { all: true })).some((a) => a.address === ip)) {
1136
+ dnsCache.set(ip, {
1137
+ ip,
1138
+ trustedBot: true
1139
+ }).catch((err) => {
1140
+ this.logger.error({ err }, "Failed to save dnsCache in storage");
1141
+ });
1142
+ return true;
1143
+ }
1144
+ } catch (err) {
1145
+ this.logger.error({ err }, "DNS reverse lookup failed");
1146
+ }
1147
+ dnsCache.set(ip, {
1148
+ ip,
1149
+ trustedBot: false
1150
+ }).catch((err) => {
1151
+ this.logger.error({ err }, "Failed to save dnsCache in storage");
1152
+ });
1153
+ return false;
1154
+ }
1155
+ isBotIPTrusted(ipAddress) {
1156
+ return getDataSources().goodBotsDataBase(ipAddress) !== null;
1157
+ }
1158
+ };
1159
+
1160
+ //#endregion
1161
+ //#region src/botDetector/checkers/goodBots/goodBots.ts
1162
+ const suffixPath = resolveDataPath("suffix.json");
1163
+ const suffixes = JSON.parse(node_fs.default.readFileSync(suffixPath, "utf-8"));
1164
+ const userAgents = Object.values(suffixes).flatMap((e) => Array.isArray(e.useragent) ? e.useragent : [e.useragent]).map((u) => u.toLowerCase());
1165
+ var GoodBotsChecker = class extends GoodBotsBase {
1166
+ constructor() {
1167
+ super(suffixes);
1168
+ this.name = "Good/Bad Bot Verification";
1169
+ this.phase = "cheap";
1170
+ }
1171
+ isEnabled(config) {
1172
+ return config.checkers.enableGoodBotsChecks.enable;
1173
+ }
1174
+ async run(ctx, config) {
1175
+ const browserType = (ctx.parsedUA.browserType ?? "").toLowerCase();
1176
+ const browserName = (ctx.parsedUA.browser ?? "").toLowerCase();
1177
+ const ipAddress = ctx.ipAddress;
1178
+ const score = 0;
1179
+ const reasons = [];
1180
+ const checkersConfig = config.checkers.enableGoodBotsChecks;
1181
+ if (!checkersConfig.enable) return {
1182
+ score,
1183
+ reasons
1184
+ };
1185
+ if (browserType !== "crawler" && browserType !== "fetcher") return {
1186
+ score: 0,
1187
+ reasons: []
1188
+ };
1189
+ const name = browserName;
1190
+ const botsWithoutSuffix = [
1191
+ "duckduckbot",
1192
+ "gptbot",
1193
+ "oai-searchbot",
1194
+ "chatgpt-user"
1195
+ ].includes(name);
1196
+ const botsWithSuffix = userAgents.some((suf) => name.includes(suf));
1197
+ if (checkersConfig.banUnlistedBots && !botsWithoutSuffix && !botsWithSuffix) {
1198
+ reasons.push("BAD_BOT_DETECTED");
1199
+ return {
1200
+ score: 0,
1201
+ reasons
1202
+ };
1203
+ }
1204
+ let trusted;
1205
+ if (botsWithSuffix) trusted = await this.isBotFromTrustedDomain(ipAddress);
1206
+ else trusted = this.isBotIPTrusted(ipAddress);
1207
+ if (!trusted) {
1208
+ reasons.push("BAD_BOT_DETECTED");
1209
+ return {
1210
+ score: checkersConfig.penalties,
1211
+ reasons
1212
+ };
1213
+ }
1214
+ reasons.push("GOOD_BOT_IDENTIFIED");
1215
+ return {
1216
+ score,
1217
+ reasons
1218
+ };
1219
+ }
1220
+ };
1221
+ CheckerRegistry.register(new GoodBotsChecker());
1222
+
1223
+ //#endregion
1224
+ //#region src/botDetector/checkers/browserTypesAneDevicesCalc.ts
1225
+ var BrowserDetailsAndDeviceChecker = class {
1226
+ constructor() {
1227
+ this.name = "Browser and Device Verification";
1228
+ this.phase = "cheap";
1229
+ }
1230
+ isEnabled(config) {
1231
+ return config.checkers.enableBrowserAndDeviceChecks.enable;
1232
+ }
1233
+ run(ctx, config) {
1234
+ const checkConfig = config.checkers.enableBrowserAndDeviceChecks;
1235
+ const reasons = [];
1236
+ let score = 0;
1237
+ if (!checkConfig.enable) return {
1238
+ score,
1239
+ reasons
1240
+ };
1241
+ const penalties = checkConfig.penalties;
1242
+ const bType = ctx.parsedUA.browserType;
1243
+ const bName = ctx.parsedUA.browser ?? "";
1244
+ const bOS = ctx.parsedUA.os ?? "";
1245
+ const dType = ctx.parsedUA.device ?? "desktop";
1246
+ if (bType === "cli" || bType === "library") {
1247
+ score += penalties.cliOrLibrary;
1248
+ reasons.push("CLI_OR_LIBRARY");
1249
+ }
1250
+ if ([
1251
+ "ie",
1252
+ "iemobile",
1253
+ "internet explorer"
1254
+ ].includes(bName.toLowerCase())) {
1255
+ score += penalties.internetExplorer;
1256
+ reasons.push("INTERNET_EXPLORER");
1257
+ }
1258
+ if (bOS.toLowerCase().includes("kali")) {
1259
+ score += penalties.linuxOs;
1260
+ reasons.push("KALI_LINUX_OS");
1261
+ }
1262
+ if (bOS === "Mac OS" && dType === "mobile") {
1263
+ score += penalties.impossibleBrowserCombinations;
1264
+ reasons.push("IMPOSSIBLE_BROWSER_COMBINATION");
1265
+ }
1266
+ if (bName === "Safari" && bOS === "Windows") {
1267
+ score += penalties.impossibleBrowserCombinations;
1268
+ reasons.push("IMPOSSIBLE_BROWSER_COMBINATION");
1269
+ }
1270
+ if (dType === "desktop" && ctx.parsedUA.deviceVendor) {
1271
+ score += penalties.impossibleBrowserCombinations;
1272
+ reasons.push("IMPOSSIBLE_BROWSER_COMBINATION");
1273
+ }
1274
+ if (!bType && (!bName || dType !== "desktop")) {
1275
+ score += penalties.browserTypeUnknown;
1276
+ reasons.push("BROWSER_TYPE_UNKNOWN");
1277
+ }
1278
+ if (!bName) {
1279
+ score += penalties.browserNameUnknown;
1280
+ reasons.push("BROWSER_NAME_UNKNOWN");
1281
+ }
1282
+ if (dType === "desktop" && !bOS) {
1283
+ score += penalties.desktopWithoutOS;
1284
+ reasons.push("DESKTOP_WITHOUT_OS");
1285
+ }
1286
+ if (dType !== "desktop" && !ctx.parsedUA.deviceVendor) {
1287
+ score += penalties.deviceVendorUnknown;
1288
+ reasons.push("DEVICE_VENDOR_UNKNOWN");
1289
+ }
1290
+ if (!ctx.parsedUA.browserVersion) {
1291
+ score += penalties.browserVersionUnknown;
1292
+ reasons.push("BROWSER_VERSION_UNKNOWN");
1293
+ }
1294
+ if (!ctx.parsedUA.deviceModel) {
1295
+ score += penalties.deviceModelUnknown;
1296
+ reasons.push("NO_MODEL");
1297
+ }
1298
+ return {
1299
+ score,
1300
+ reasons
1301
+ };
1302
+ }
1303
+ };
1304
+ CheckerRegistry.register(new BrowserDetailsAndDeviceChecker());
1305
+
1306
+ //#endregion
1307
+ //#region src/botDetector/utils/regex/acceptLangRegex.ts
1308
+ const space = (0, magic_regexp.anyOf)(" ", " ").times.any();
1309
+ const quality = (0, magic_regexp.maybe)((0, magic_regexp.anyOf)((0, magic_regexp.exactly)(space, ";", space, "q", space, "=", space, (0, magic_regexp.anyOf)((0, magic_regexp.exactly)("0", (0, magic_regexp.maybe)((0, magic_regexp.anyOf)((0, magic_regexp.exactly)(".", magic_regexp.digit.times.between(1, 3))))), (0, magic_regexp.exactly)("1", (0, magic_regexp.maybe)((0, magic_regexp.anyOf)((0, magic_regexp.exactly)(".", (0, magic_regexp.exactly)("0").times.between(1, 3)))))))));
1310
+ const primaryTag = magic_regexp.letter.times.between(1, 8);
1311
+ const subTag = (0, magic_regexp.anyOf)((0, magic_regexp.exactly)("-", (0, magic_regexp.anyOf)(magic_regexp.letter, magic_regexp.digit).times.between(1, 8))).times.any();
1312
+ const language = (0, magic_regexp.exactly)((0, magic_regexp.anyOf)((0, magic_regexp.exactly)("*"), (0, magic_regexp.exactly)(primaryTag, subTag)), quality);
1313
+ const repeatingLanguages = (0, magic_regexp.anyOf)((0, magic_regexp.exactly)(space, ",", space, language)).times.any();
1314
+ const acceptLanguageValidator = (0, magic_regexp.createRegExp)((0, magic_regexp.exactly)(language, repeatingLanguages).at.lineStart().at.lineEnd(), ["i"]);
1315
+
1316
+ //#endregion
1317
+ //#region src/botDetector/checkers/acceptLangMap.ts
1318
+ var LocaleMapChecker = class {
1319
+ constructor() {
1320
+ this.name = "Locale and Country Verification";
1321
+ this.phase = "cheap";
1322
+ }
1323
+ isEnabled(config) {
1324
+ return config.checkers.localeMapsCheck.enable;
1325
+ }
1326
+ run(ctx, config) {
1327
+ const settings = config.checkers.localeMapsCheck;
1328
+ const reasons = [];
1329
+ let score = 0;
1330
+ if (!settings.enable) return {
1331
+ score,
1332
+ reasons
1333
+ };
1334
+ const AccHeader = ctx.req.get("Accept-Language") ?? "";
1335
+ if (!AccHeader) {
1336
+ score += settings.penalties.missingHeader;
1337
+ if (score > 0) reasons.push("LOCALE_MISMATCH");
1338
+ return {
1339
+ score,
1340
+ reasons
1341
+ };
1342
+ }
1343
+ if (!acceptLanguageValidator.test(AccHeader.trim().toLowerCase())) {
1344
+ score += settings.penalties.malformedHeader;
1345
+ if (score > 0) reasons.push("LOCALE_MISMATCH");
1346
+ return {
1347
+ score,
1348
+ reasons
1349
+ };
1350
+ }
1351
+ const langs = AccHeader.split(",").map((entry) => {
1352
+ const [tag, q] = entry.trim().split(/\s*;\s*q\s*=\s*/);
1353
+ return {
1354
+ tag: tag.toLowerCase(),
1355
+ weight: q ? parseFloat(q) : 1
1356
+ };
1357
+ }).sort((a, b) => b.weight - a.weight);
1358
+ const country = ctx.geoData.country;
1359
+ const countryCode = ctx.geoData.countryCode;
1360
+ const iso6 = ctx.geoData.iso639;
1361
+ if (!country || !countryCode || !iso6) {
1362
+ score += settings.penalties.missingGeoData;
1363
+ if (score > 0) reasons.push("LOCALE_MISMATCH");
1364
+ return {
1365
+ score,
1366
+ reasons
1367
+ };
1368
+ }
1369
+ const expectedCountryCode = countryCode.toLowerCase();
1370
+ const expectedLang = iso6.toLowerCase();
1371
+ const combined = `${expectedLang}-${expectedCountryCode}`;
1372
+ let localeMatchesGeo = false;
1373
+ for (const { tag, weight } of langs) {
1374
+ if (weight === 0) continue;
1375
+ const parts = tag.split(/[-_]/);
1376
+ const langPart = parts[0];
1377
+ const regionPart = parts.find((p) => p.length === 2 && p === expectedCountryCode);
1378
+ if (regionPart) {
1379
+ localeMatchesGeo = true;
1380
+ break;
1381
+ }
1382
+ if (langPart === expectedLang) {
1383
+ localeMatchesGeo = true;
1384
+ break;
1385
+ }
1386
+ if (langPart && regionPart && tag === combined) {
1387
+ localeMatchesGeo = true;
1388
+ break;
1389
+ }
1390
+ }
1391
+ if (!localeMatchesGeo) {
1392
+ score += settings.penalties.ipAndHeaderMismatch;
1393
+ if (score > 0) reasons.push("LOCALE_MISMATCH");
1394
+ }
1395
+ return {
1396
+ score,
1397
+ reasons
1398
+ };
1399
+ }
1400
+ };
1401
+ CheckerRegistry.register(new LocaleMapChecker());
1402
+
1403
+ //#endregion
1404
+ //#region src/botDetector/helpers/cache/rateLimitarCache.ts
1405
+ const RATE_TTL_SECONDS_FALLBACK = 120;
1406
+ const PREFIX$4 = "rate:";
1407
+ const rateCache = {
1408
+ async get(cookie) {
1409
+ return getStorage().getItem(`${PREFIX$4}${cookie}`);
1410
+ },
1411
+ async set(cookie, entry, ttlSeconds) {
1412
+ await getStorage().setItem(`${PREFIX$4}${cookie}`, entry, { ttl: ttlSeconds ?? RATE_TTL_SECONDS_FALLBACK });
1413
+ },
1414
+ async delete(cookie) {
1415
+ await getStorage().removeItem(`${PREFIX$4}${cookie}`);
1416
+ },
1417
+ async clear() {
1418
+ await getStorage().clear();
1419
+ }
1420
+ };
1421
+
1422
+ //#endregion
1423
+ //#region src/botDetector/checkers/rateTracker.ts
1424
+ var BehavioralDbChecker = class {
1425
+ constructor() {
1426
+ this.name = "Behavior Rate Verification";
1427
+ this.phase = "heavy";
1428
+ }
1429
+ get logger() {
1430
+ this._logger ??= getLogger().child({
1431
+ service: "botDetector",
1432
+ branch: "checker",
1433
+ type: "BehavioralDbChecker"
1434
+ });
1435
+ return this._logger;
1436
+ }
1437
+ isEnabled(config) {
1438
+ return config.checkers.enableBehaviorRateCheck.enable;
1439
+ }
1440
+ async run(ctx, config) {
1441
+ const cookie = ctx.cookie ?? "";
1442
+ const checkConfig = config.checkers.enableBehaviorRateCheck;
1443
+ if (!checkConfig.enable) return {
1444
+ score: 0,
1445
+ reasons: []
1446
+ };
1447
+ const BEHAVIORAL_THRESHOLD = checkConfig.behavioral_threshold;
1448
+ const BEHAVIORAL_WINDOW = checkConfig.behavioral_window;
1449
+ const BEHAVIORAL_PENALTY = checkConfig.penalties;
1450
+ const ttlSeconds = Math.ceil(BEHAVIORAL_WINDOW / 1e3);
1451
+ const cached = await rateCache.get(cookie);
1452
+ if (cached) if (Date.now() - cached.timestamp <= BEHAVIORAL_WINDOW) {
1453
+ const newCount = cached.request_count + 1;
1454
+ const score = newCount > BEHAVIORAL_THRESHOLD ? BEHAVIORAL_PENALTY : 0;
1455
+ rateCache.set(cookie, {
1456
+ ...cached,
1457
+ request_count: newCount,
1458
+ score
1459
+ }, ttlSeconds).catch((err) => {
1460
+ this.logger.error({ err }, "Failed to save rateCache in storage");
1461
+ });
1462
+ return {
1463
+ score,
1464
+ reasons: score ? ["BEHAVIOR_TOO_FAST"] : []
1465
+ };
1466
+ } else {
1467
+ rateCache.set(cookie, {
1468
+ request_count: 1,
1469
+ timestamp: Date.now(),
1470
+ score: 0
1471
+ }, ttlSeconds).catch((err) => {
1472
+ this.logger.error({ err }, "Failed to reset rateCache in storage");
1473
+ });
1474
+ return {
1475
+ score: 0,
1476
+ reasons: []
1477
+ };
1478
+ }
1479
+ return {
1480
+ score: 0,
1481
+ reasons: []
1482
+ };
1483
+ }
1484
+ };
1485
+ CheckerRegistry.register(new BehavioralDbChecker());
1486
+
1487
+ //#endregion
1488
+ //#region src/botDetector/checkers/proxyISPAndCookieCalc.ts
1489
+ var ProxyIspAndCookieChecker = class {
1490
+ constructor() {
1491
+ this.name = "Proxy, ISP and Cookie Verification";
1492
+ this.phase = "heavy";
1493
+ }
1494
+ isEnabled(config) {
1495
+ return config.checkers.enableProxyIspCookiesChecks.enable;
1496
+ }
1497
+ run(ctx, config) {
1498
+ const checkConfig = config.checkers.enableProxyIspCookiesChecks;
1499
+ const reasons = [];
1500
+ let score = 0;
1501
+ if (!checkConfig.enable) return {
1502
+ score,
1503
+ reasons
1504
+ };
1505
+ const { penalties } = checkConfig;
1506
+ const cookie = ctx.cookie ?? "";
1507
+ const proxy = ctx.proxy.isProxy;
1508
+ const proxyType = ctx.proxy.proxyType ?? "";
1509
+ const hosting = ctx.geoData.hosting ?? false;
1510
+ const isp = ctx.geoData.isp ?? "";
1511
+ const org = ctx.geoData.org ?? "";
1512
+ if (!cookie) {
1513
+ score += penalties.cookieMissing;
1514
+ reasons.push("COOKIE_MISSING");
1515
+ }
1516
+ if (proxy) {
1517
+ score += penalties.proxyDetected;
1518
+ reasons.push("PROXY_DETECTED");
1519
+ if (proxyType) {
1520
+ const sourceCount = proxyType.split(",").length;
1521
+ if (sourceCount >= 4) score += penalties.multiSourceBonus4plus;
1522
+ else if (sourceCount >= 2) score += penalties.multiSourceBonus2to3;
1523
+ }
1524
+ }
1525
+ if (hosting) {
1526
+ score += penalties.hostingDetected;
1527
+ reasons.push("HOSTING_DETECTED");
1528
+ }
1529
+ if (!isp) {
1530
+ score += penalties.ispUnknown;
1531
+ reasons.push("ISP_UNKNOWN");
1532
+ }
1533
+ if (!org) {
1534
+ score += penalties.orgUnknown;
1535
+ reasons.push("ORG_UNKNOWN");
1536
+ }
1537
+ return {
1538
+ score,
1539
+ reasons
1540
+ };
1541
+ }
1542
+ };
1543
+ CheckerRegistry.register(new ProxyIspAndCookieChecker());
1544
+
1545
+ //#endregion
1546
+ //#region src/botDetector/checkers/headers/headersBase.ts
1547
+ var HeadersBase = class {
1548
+ constructor() {
1549
+ this.config = getConfiguration().headerOptions;
1550
+ }
1551
+ mustHaveHeadersChecker(req) {
1552
+ let score = 0;
1553
+ if (req.httpVersion === "1.0") return score += 40;
1554
+ if (!req.get("User-Agent")) score += this.config.weightPerMustHeader;
1555
+ if (!req.accepts) score += this.config.weightPerMustHeader;
1556
+ if (!req.acceptsEncodings) score += this.config.weightPerMustHeader;
1557
+ if (!req.acceptsLanguages) score += this.config.weightPerMustHeader;
1558
+ if (!req.host) score += this.config.weightPerMustHeader;
1559
+ if (!req.get("Upgrade-Insecure-Requests")) score += this.config.weightPerMustHeader;
1560
+ if (!req.get("x-client-id")) score += this.config.weightPerMustHeader;
1561
+ if (req.httpVersion === "1.1" || req.get("Connection")) {
1562
+ if (!req.get("Connection") || req.get("Connection") !== "keep-alive") score += this.config.connectionHeaderIsClose;
1563
+ }
1564
+ if (!req.get("sec-fetch-mode") || !req.get("sec-fetch-dest") || !req.get("sec-fetch-site")) score += this.config.weightPerMustHeader;
1565
+ return score;
1566
+ }
1567
+ async engineHeaders(req) {
1568
+ let score = 0;
1569
+ const ua = req.get("User-Agent");
1570
+ const hints = req.headers;
1571
+ const { name } = await new ua_parser_js.UAParser(ua, hints).getEngine().withClientHints();
1572
+ if (!name) return score += this.config.missingBrowserEngine;
1573
+ const headers = Object.keys(hints);
1574
+ const containsCh = headers.some((sec) => sec.toLowerCase().startsWith("sec-ch-ua"));
1575
+ const containsTe = headers.some((te) => te.toLowerCase() === "te");
1576
+ if (name === "Blink") {
1577
+ if (!containsCh) score += this.config.clientHintsMissingForBlink;
1578
+ if (containsTe) score += this.config.teHeaderUnexpectedForBlink;
1579
+ } else if (name === "Gecko") {
1580
+ if (containsCh) score += this.config.clientHintsUnexpectedForGecko;
1581
+ if (!containsTe) score += this.config.teHeaderMissingForGecko;
1582
+ } else if (name === "WebKit") {
1583
+ if (containsCh) score += this.config.clientHintsUnexpectedForGecko;
1584
+ if (containsTe) score += this.config.teHeaderUnexpectedForBlink;
1585
+ }
1586
+ return score;
1587
+ }
1588
+ weirdHeaders(req) {
1589
+ let score = 0;
1590
+ if (req.get("accept") === "*/*") score += this.config.omittedAcceptHeader;
1591
+ if (req.get("x-requested-with") && req.method === "GET") score += this.config.AJAXHeaderExists;
1592
+ if (req.get("postman-token") || req.get("insomnia")) score += this.config.postManOrInsomiaHeaders;
1593
+ const hostHeader = req.get("X-Forwarded-Host");
1594
+ if (hostHeader && req.hostname && hostHeader !== req.hostname) score += this.config.hostMismatchWeight;
1595
+ if (req.method === "GET" && req.get("Cache-Control") === "no-cache" && req.get("Pragma") === "no-cache") score += this.config.aggressiveCacheControlOnGet;
1596
+ if (req.get("sec-fetch-site") === "cross-site" && !req.get("referer")) score += this.config.crossSiteRequestMissingReferer;
1597
+ const isBrowserRequest = !req.get("x-client-id");
1598
+ const isTopNavigation = req.method === "GET" && req.get("sec-fetch-dest") === "document";
1599
+ if (isBrowserRequest && !isTopNavigation && req.method !== "GET") {
1600
+ const origin = req.get("origin");
1601
+ if (!origin) score += this.config.originHeaderIsNULL;
1602
+ else if (origin !== `${req.protocol}://${req.hostname}`) score += this.config.originHeaderMismatch;
1603
+ }
1604
+ const mode = req.get("sec-fetch-mode");
1605
+ if (mode !== "same-origin" && mode !== "navigate") score += this.config.inconsistentSecFetchMode;
1606
+ return score;
1607
+ }
1608
+ };
1609
+
1610
+ //#endregion
1611
+ //#region src/botDetector/checkers/headers/headers.ts
1612
+ var HeaderAnalysis = class extends HeadersBase {
1613
+ constructor(req) {
1614
+ super();
1615
+ this.req = req;
1616
+ }
1617
+ async scoreHeaders() {
1618
+ const missing = this.mustHaveHeadersChecker(this.req);
1619
+ const engines = await this.engineHeaders(this.req);
1620
+ const weird = this.weirdHeaders(this.req);
1621
+ return missing + engines + weird;
1622
+ }
1623
+ };
1624
+
1625
+ //#endregion
1626
+ //#region src/botDetector/utils/regex/pathTravelersRegex.ts
1627
+ const startOrSlash = (0, magic_regexp.anyOf)((0, magic_regexp.exactly)("").at.lineStart(), "/").grouped();
1628
+ const slashOrEnd = (0, magic_regexp.anyOf)("/", (0, magic_regexp.exactly)("").at.lineEnd());
1629
+ const pathRules = [
1630
+ {
1631
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".git", slashOrEnd, ["i"]),
1632
+ weight: 10
1633
+ },
1634
+ {
1635
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".git/config", slashOrEnd, ["i"]),
1636
+ weight: 10
1637
+ },
1638
+ {
1639
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".env", (0, magic_regexp.maybe)((0, magic_regexp.anyOf)(".local", ".example")), slashOrEnd, ["i"]),
1640
+ weight: 10
1641
+ },
1642
+ {
1643
+ re: (0, magic_regexp.createRegExp)(startOrSlash, "wp-admin", slashOrEnd, ["i"]),
1644
+ weight: 8
1645
+ },
1646
+ {
1647
+ re: (0, magic_regexp.createRegExp)("/wp-json/wp/v2", slashOrEnd, ["i"]),
1648
+ weight: 7
1649
+ },
1650
+ {
1651
+ re: (0, magic_regexp.createRegExp)("/", (0, magic_regexp.anyOf)("jenkins", "hudson").grouped(), slashOrEnd, ["i"]),
1652
+ weight: 9
1653
+ },
1654
+ {
1655
+ re: (0, magic_regexp.createRegExp)("/", (0, magic_regexp.anyOf)("script", "login").grouped(), ".groovy", slashOrEnd, ["i"]),
1656
+ weight: 8
1657
+ },
1658
+ {
1659
+ re: (0, magic_regexp.createRegExp)("/actuator/", (0, magic_regexp.anyOf)("env", "health", "metrics"), slashOrEnd, ["i"]),
1660
+ weight: 8
1661
+ },
1662
+ {
1663
+ re: (0, magic_regexp.createRegExp)("/web.config", slashOrEnd, ["i"]),
1664
+ weight: 7
1665
+ },
1666
+ {
1667
+ re: (0, magic_regexp.createRegExp)("/.DS_Store", slashOrEnd, ["i"]),
1668
+ weight: 4
1669
+ },
1670
+ {
1671
+ re: (0, magic_regexp.createRegExp)("/latest/meta-data/iam", slashOrEnd, ["i"]),
1672
+ weight: 10
1673
+ },
1674
+ {
1675
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".aws/credentials", slashOrEnd, ["i"]),
1676
+ weight: 10
1677
+ },
1678
+ {
1679
+ re: (0, magic_regexp.createRegExp)("/Dockerfile", slashOrEnd, ["i"]),
1680
+ weight: 7
1681
+ },
1682
+ {
1683
+ re: (0, magic_regexp.createRegExp)("/composer.lock", slashOrEnd, ["i"]),
1684
+ weight: 7
1685
+ },
1686
+ {
1687
+ re: (0, magic_regexp.createRegExp)("/jnlpJars/jenkins-cli.jar", slashOrEnd, ["i"]),
1688
+ weight: 9
1689
+ },
1690
+ {
1691
+ re: (0, magic_regexp.createRegExp)("/manager/html", slashOrEnd, ["i"]),
1692
+ weight: 10
1693
+ },
1694
+ {
1695
+ re: (0, magic_regexp.createRegExp)("/shell", (0, magic_regexp.maybe)(".php"), slashOrEnd, ["i"]),
1696
+ weight: 10
1697
+ },
1698
+ {
1699
+ re: (0, magic_regexp.createRegExp)("/", (0, magic_regexp.anyOf)("grafana", "kibana", "prometheus").grouped(), slashOrEnd, ["i"]),
1700
+ weight: 7
1701
+ },
1702
+ {
1703
+ re: (0, magic_regexp.createRegExp)("/", (0, magic_regexp.anyOf)("owa", "ecp").grouped(), slashOrEnd, ["i"]),
1704
+ weight: 8
1705
+ },
1706
+ {
1707
+ re: (0, magic_regexp.createRegExp)("wp-login.php", slashOrEnd, ["i"]),
1708
+ weight: 8
1709
+ },
1710
+ {
1711
+ re: (0, magic_regexp.createRegExp)("/swagger-ui", (0, magic_regexp.anyOf)("/", ".html"), slashOrEnd, ["i"]),
1712
+ weight: 6
1713
+ },
1714
+ {
1715
+ re: (0, magic_regexp.createRegExp)("/v2/api-docs", slashOrEnd, ["i"]),
1716
+ weight: 5
1717
+ },
1718
+ {
1719
+ re: (0, magic_regexp.createRegExp)("/api-docs", (0, magic_regexp.anyOf)("/", ".json"), slashOrEnd, ["i"]),
1720
+ weight: 5
1721
+ },
1722
+ {
1723
+ re: (0, magic_regexp.createRegExp)("phpmyadmin", slashOrEnd, ["i"]),
1724
+ weight: 8
1725
+ },
1726
+ {
1727
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".well-known", slashOrEnd, ["i"]),
1728
+ weight: 5
1729
+ },
1730
+ {
1731
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".htaccess", slashOrEnd, ["i"]),
1732
+ weight: 7
1733
+ },
1734
+ {
1735
+ re: (0, magic_regexp.createRegExp)("composer.json", slashOrEnd, ["i"]),
1736
+ weight: 7
1737
+ },
1738
+ {
1739
+ re: (0, magic_regexp.createRegExp)("docker-compose.y", (0, magic_regexp.maybe)("a"), "ml", slashOrEnd, ["i"]),
1740
+ weight: 7
1741
+ },
1742
+ {
1743
+ re: (0, magic_regexp.createRegExp)(".", (0, magic_regexp.anyOf)("sql", "bak", "old", "save", "log", "ini", "conf", "zip", (0, magic_regexp.exactly)("tar", (0, magic_regexp.maybe)(".gz"))), slashOrEnd, ["i"]),
1744
+ weight: 7
1745
+ },
1746
+ {
1747
+ re: (0, magic_regexp.createRegExp)((0, magic_regexp.anyOf)("..", "%2e%2e").grouped(), (0, magic_regexp.anyOf)("/", "\\"), ["i"]),
1748
+ weight: 5
1749
+ },
1750
+ {
1751
+ re: (0, magic_regexp.createRegExp)((0, magic_regexp.anyOf)("package-lock.json", "yarn.lock", ".gitignore").grouped(), slashOrEnd, ["i"]),
1752
+ weight: 6
1753
+ },
1754
+ {
1755
+ re: (0, magic_regexp.createRegExp)(startOrSlash, "admin", slashOrEnd, ["i"]),
1756
+ weight: 6
1757
+ },
1758
+ {
1759
+ re: (0, magic_regexp.createRegExp)("phpinfo", (0, magic_regexp.maybe)(".php"), slashOrEnd, ["i"]),
1760
+ weight: 8
1761
+ },
1762
+ {
1763
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".ssh", slashOrEnd, ["i"]),
1764
+ weight: 8
1765
+ },
1766
+ {
1767
+ re: (0, magic_regexp.createRegExp)((0, magic_regexp.exactly)("").at.lineStart(), "/xmlrpc.php", slashOrEnd, ["i"]),
1768
+ weight: 8
1769
+ },
1770
+ {
1771
+ re: (0, magic_regexp.createRegExp)(startOrSlash, "wp-config.php", slashOrEnd, ["i"]),
1772
+ weight: 10
1773
+ },
1774
+ {
1775
+ re: (0, magic_regexp.createRegExp)(startOrSlash, (0, magic_regexp.anyOf)("install", "setup").grouped(), (0, magic_regexp.maybe)(".php"), slashOrEnd, ["i"]),
1776
+ weight: 8
1777
+ },
1778
+ {
1779
+ re: (0, magic_regexp.createRegExp)(startOrSlash, "backup", (0, magic_regexp.maybe)("s"), (0, magic_regexp.anyOf)(".zip", ".tar.gz", ".sql"), slashOrEnd, ["i"]),
1780
+ weight: 8
1781
+ },
1782
+ {
1783
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".gitlab-ci.yml", slashOrEnd, ["i"]),
1784
+ weight: 8
1785
+ },
1786
+ {
1787
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".svn", slashOrEnd, ["i"]),
1788
+ weight: 6
1789
+ },
1790
+ {
1791
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".hg", slashOrEnd, ["i"]),
1792
+ weight: 6
1793
+ },
1794
+ {
1795
+ re: (0, magic_regexp.createRegExp)(startOrSlash, "CVS", slashOrEnd, ["i"]),
1796
+ weight: 6
1797
+ },
1798
+ {
1799
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".vscode", slashOrEnd, ["i"]),
1800
+ weight: 4
1801
+ },
1802
+ {
1803
+ re: (0, magic_regexp.createRegExp)(startOrSlash, ".htpasswd", slashOrEnd, ["i"]),
1804
+ weight: 8
1805
+ },
1806
+ {
1807
+ re: (0, magic_regexp.createRegExp)("phpunit", (0, magic_regexp.maybe)(".phar"), slashOrEnd, ["i"]),
1808
+ weight: 9
1809
+ },
1810
+ {
1811
+ re: (0, magic_regexp.createRegExp)(startOrSlash, "config", (0, magic_regexp.maybe)("uration"), ".", (0, magic_regexp.anyOf)("php", "yml", "json", "xml", "ini", "conf", "cfg"), slashOrEnd, ["i"]),
1812
+ weight: 7
1813
+ }
1814
+ ];
1815
+ const whiteList = [
1816
+ (0, magic_regexp.createRegExp)((0, magic_regexp.exactly)("").at.lineStart(), (0, magic_regexp.maybe)("/"), (0, magic_regexp.exactly)("").at.lineEnd()),
1817
+ (0, magic_regexp.createRegExp)((0, magic_regexp.exactly)("").at.lineStart(), "/", (0, magic_regexp.anyOf)("css", "js", "images", "assets", "static").grouped(), "/", ["i"]),
1818
+ (0, magic_regexp.createRegExp)(".", (0, magic_regexp.anyOf)("html", "css", "js", "png", (0, magic_regexp.exactly)("jp", (0, magic_regexp.maybe)("e"), "g"), "svg", "map", (0, magic_regexp.exactly)("woff", (0, magic_regexp.maybe)("2")), "ttf", "webp"), (0, magic_regexp.exactly)("").at.lineEnd(), ["i"]),
1819
+ (0, magic_regexp.createRegExp)((0, magic_regexp.exactly)("").at.lineStart(), "/robots.txt", (0, magic_regexp.exactly)("").at.lineEnd(), ["i"]),
1820
+ (0, magic_regexp.createRegExp)((0, magic_regexp.exactly)("").at.lineStart(), "/sitemap.xml", (0, magic_regexp.exactly)("").at.lineEnd(), ["i"])
1821
+ ];
1822
+
1823
+ //#endregion
1824
+ //#region src/botDetector/checkers/UaAndHeaderChecker/base.ts
1825
+ var UaAndHeaderCheckerBase = class {
1826
+ pathScore(req, config) {
1827
+ const settings = config.pathTraveler;
1828
+ const MAX_DECODE_ITERATIONS = settings.maxIterations;
1829
+ const MAX_PATH_LENGTH = settings.maxPathLength;
1830
+ let score = 0;
1831
+ const hostHeader = req.get("x-forwarded-host");
1832
+ const base = `${req.protocol}://${hostHeader ?? req.get("host") ?? ""}`;
1833
+ const rawPath = req.get("x-original-path") || req.originalUrl;
1834
+ if (/(?:\.\.[\\/]|\.\.%2f|\.\.%5c|%2e%2e[\\/]|%2e%2e%2f|%2e%2e%5c)/i.test(rawPath)) return settings.traversalDetected;
1835
+ let pathname;
1836
+ try {
1837
+ pathname = new url.URL(rawPath, base).pathname;
1838
+ } catch {
1839
+ pathname = rawPath.split("?")[0];
1840
+ }
1841
+ if (pathname.length > MAX_PATH_LENGTH) return settings.pathLengthToLong;
1842
+ if (whiteList.some((rx) => rx.test(pathname))) return 0;
1843
+ let decoded = pathname;
1844
+ let totalDecodedLength = 0;
1845
+ for (let i = 0; i < MAX_DECODE_ITERATIONS; i++) try {
1846
+ const tmp = decodeURIComponent(decoded);
1847
+ if (tmp === decoded) break;
1848
+ totalDecodedLength += tmp.length;
1849
+ if (totalDecodedLength > MAX_PATH_LENGTH * 2) return settings.longDecoding;
1850
+ decoded = tmp;
1851
+ } catch {
1852
+ break;
1853
+ }
1854
+ const normalized = path.default.normalize(decoded);
1855
+ for (const { re, weight } of pathRules) if (re.test(normalized)) {
1856
+ score += weight;
1857
+ if (score >= 30) break;
1858
+ }
1859
+ return score;
1860
+ }
1861
+ tlsBotScore(req, config) {
1862
+ const settings = config.checkers.enableUaAndHeaderChecks;
1863
+ if (!settings.enable) return 0;
1864
+ const { penalties } = settings;
1865
+ let score = 0;
1866
+ const proto = req.httpVersion === "2.0" ? "h2" : req.httpVersion === "1.1" ? "http/1.1" : "";
1867
+ if (!proto || proto !== "h2" && proto !== "http/1.1") score += penalties.tlsCheckFailed;
1868
+ const cipher = req.get("x-client-cipher") ?? "";
1869
+ if (!new Set([
1870
+ "TLS_AES_128_GCM_SHA256",
1871
+ "TLS_AES_256_GCM_SHA384",
1872
+ "TLS_CHACHA20_POLY1305_SHA256",
1873
+ "ECDHE-ECDSA-AES128-GCM-SHA256",
1874
+ "ECDHE-RSA-AES128-GCM-SHA256",
1875
+ "ECDHE-ECDSA-CHACHA20-POLY1305",
1876
+ "ECDHE-RSA-CHACHA20-POLY1305",
1877
+ "ECDHE-ECDSA-AES256-GCM-SHA384",
1878
+ "ECDHE-RSA-AES256-GCM-SHA384",
1879
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
1880
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
1881
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
1882
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
1883
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
1884
+ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
1885
+ ]).has(cipher)) score += penalties.tlsCheckFailed;
1886
+ const tlsVersion = (req.get("x-client-tls-version") ?? "").toLowerCase();
1887
+ if (tlsVersion && !tlsVersion.startsWith("tls1.3") && !tlsVersion.startsWith("tls1.2")) score += penalties.tlsCheckFailed;
1888
+ return score;
1889
+ }
1890
+ };
1891
+
1892
+ //#endregion
1893
+ //#region src/botDetector/checkers/UaAndHeaderChecker/headersAndUACalc.ts
1894
+ var UaAndHeaderChecker = class extends UaAndHeaderCheckerBase {
1895
+ constructor(..._args) {
1896
+ super(..._args);
1897
+ this.name = "User agent and Header Verification";
1898
+ this.phase = "heavy";
1899
+ }
1900
+ isEnabled(config) {
1901
+ return config.checkers.enableUaAndHeaderChecks.enable;
1902
+ }
1903
+ async run(ctx, config) {
1904
+ const checkConfig = config.checkers.enableUaAndHeaderChecks;
1905
+ const reasons = [];
1906
+ let score = 0;
1907
+ if (!checkConfig.enable) return {
1908
+ score,
1909
+ reasons
1910
+ };
1911
+ const { penalties } = checkConfig;
1912
+ const req = ctx.req;
1913
+ const uaString = req.get("User-Agent") ?? "";
1914
+ const uaLower = uaString.toLowerCase();
1915
+ if ((0, magic_regexp.createRegExp)((0, magic_regexp.anyOf)("headless", "puppeteer", "selenium", "playwright", "phantomjs")).test(uaLower)) {
1916
+ score += penalties.headlessBrowser;
1917
+ reasons.push("HEADLESS_BROWSER_DETECTED");
1918
+ }
1919
+ if (!uaString || uaString.length < 10) {
1920
+ score += penalties.shortUserAgent;
1921
+ reasons.push("SHORT_USER_AGENT");
1922
+ }
1923
+ const tlsCheckScore = this.tlsBotScore(req, config);
1924
+ if (tlsCheckScore > 0) {
1925
+ score += tlsCheckScore;
1926
+ reasons.push("TLS_CHECK_FAILED");
1927
+ }
1928
+ const headerChecker = await new HeaderAnalysis(req).scoreHeaders();
1929
+ if (headerChecker > 0) {
1930
+ score += headerChecker;
1931
+ reasons.push("HEADER_SCORE_TOO_HIGH");
1932
+ }
1933
+ const pathChecker = this.pathScore(req, config);
1934
+ if (pathChecker > 0) {
1935
+ score += pathChecker;
1936
+ reasons.push("PATH_TRAVELER_FOUND");
1937
+ }
1938
+ return {
1939
+ score,
1940
+ reasons
1941
+ };
1942
+ }
1943
+ };
1944
+ CheckerRegistry.register(new UaAndHeaderChecker());
1945
+
1946
+ //#endregion
1947
+ //#region src/botDetector/checkers/geoLocationCalc.ts
1948
+ var GeoLocationChecker = class {
1949
+ constructor() {
1950
+ this.name = "Geo-Location Verification";
1951
+ this.phase = "heavy";
1952
+ }
1953
+ isEnabled(config) {
1954
+ return config.checkers.enableGeoChecks.enable;
1955
+ }
1956
+ isAllowedCountry(country, bannedCountries) {
1957
+ return !bannedCountries.includes(country.trim().toLowerCase());
1958
+ }
1959
+ run(ctx, config) {
1960
+ const checkConfig = config.checkers.enableGeoChecks;
1961
+ const reasons = [];
1962
+ let score = 0;
1963
+ if (!checkConfig.enable) return {
1964
+ score,
1965
+ reasons
1966
+ };
1967
+ const penalties = checkConfig.penalties;
1968
+ const banScore = config.banScore;
1969
+ const details = ctx.geoData;
1970
+ const country = details.country;
1971
+ const countryCode = details.countryCode;
1972
+ if (countryCode || country) {
1973
+ const banned = checkConfig.bannedCountries;
1974
+ const codeMatch = !!countryCode && !this.isAllowedCountry(countryCode, banned);
1975
+ const nameMatch = !!country && !this.isAllowedCountry(country, banned);
1976
+ if (codeMatch || nameMatch) {
1977
+ score += banScore;
1978
+ reasons.push("BANNED_COUNTRY");
1979
+ }
1980
+ } else {
1981
+ score += penalties.countryUnknown;
1982
+ reasons.push("COUNTRY_UNKNOWN");
1983
+ }
1984
+ const region = details.region;
1985
+ const regionName = details.regionName;
1986
+ if (!region || !regionName) {
1987
+ score += penalties.regionUnknown;
1988
+ reasons.push("REGION_UNKNOWN");
1989
+ }
1990
+ if (!details.lat || !details.lon) {
1991
+ score += penalties.latLonUnknown;
1992
+ reasons.push("LAT_LON_UNKNOWN");
1993
+ }
1994
+ if (!details.district) {
1995
+ score += penalties.districtUnknown;
1996
+ reasons.push("DISTRICT_UNKNOWN");
1997
+ }
1998
+ if (!details.city) {
1999
+ score += penalties.cityUnknown;
2000
+ reasons.push("CITY_UNKNOWN");
2001
+ }
2002
+ if (!details.timezone) {
2003
+ score += penalties.timezoneUnknown;
2004
+ reasons.push("TIMEZONE_UNKNOWN");
2005
+ }
2006
+ if (!details.subregion) {
2007
+ score += penalties.subregionUnknown;
2008
+ reasons.push("SUBREGION_UNKNOWN");
2009
+ }
2010
+ if (!details.phone) {
2011
+ score += penalties.phoneUnknown;
2012
+ reasons.push("PHONE_UNKNOWN");
2013
+ }
2014
+ if (!details.continent) {
2015
+ score += penalties.continentUnknown;
2016
+ reasons.push("CONTINENT_UNKNOWN");
2017
+ }
2018
+ return {
2019
+ score,
2020
+ reasons
2021
+ };
2022
+ }
2023
+ };
2024
+ CheckerRegistry.register(new GeoLocationChecker());
2025
+
2026
+ //#endregion
2027
+ //#region src/botDetector/checkers/fireholEscalation.ts
2028
+ var ThreatLevels = class {
2029
+ constructor() {
2030
+ this.name = "Known ThreatLevels";
2031
+ this.phase = "cheap";
2032
+ }
2033
+ isEnabled(config) {
2034
+ return config.checkers.enableKnownThreatsDetections.enable;
2035
+ }
2036
+ run(ctx, config) {
2037
+ const { enableKnownThreatsDetections } = config.checkers;
2038
+ const reasons = [];
2039
+ let score = 0;
2040
+ if (!enableKnownThreatsDetections.enable) return {
2041
+ score,
2042
+ reasons
2043
+ };
2044
+ const { anonymiseNetwork, threatLevels } = enableKnownThreatsDetections.penalties;
2045
+ if (ctx.anon) {
2046
+ score += anonymiseNetwork;
2047
+ reasons.push("ANONYMITY_NETWORK");
2048
+ }
2049
+ switch (ctx.threatLevel) {
2050
+ case 1:
2051
+ score += threatLevels.criticalLevel1;
2052
+ reasons.push("FIREHOL_L1_THREAT");
2053
+ break;
2054
+ case 2:
2055
+ score += threatLevels.currentAttacksLevel2;
2056
+ reasons.push("FIREHOL_L2_THREAT");
2057
+ break;
2058
+ case 3:
2059
+ score += threatLevels.threatLevel3;
2060
+ reasons.push("FIREHOL_L3_THREAT");
2061
+ break;
2062
+ case 4:
2063
+ score += threatLevels.threatLevel4;
2064
+ reasons.push("FIREHOL_L4_THREAT");
2065
+ break;
2066
+ }
2067
+ return {
2068
+ score,
2069
+ reasons
2070
+ };
2071
+ }
2072
+ };
2073
+ CheckerRegistry.register(new ThreatLevels());
2074
+
2075
+ //#endregion
2076
+ //#region src/botDetector/checkers/asnClassification.ts
2077
+ var AsnClassificationChecker = class {
2078
+ constructor() {
2079
+ this.name = "ASN Classification";
2080
+ this.phase = "cheap";
2081
+ }
2082
+ isEnabled(config) {
2083
+ return config.checkers.enableAsnClassification.enable;
2084
+ }
2085
+ run(ctx, config) {
2086
+ const checkConfig = config.checkers.enableAsnClassification;
2087
+ const reasons = [];
2088
+ let score = 0;
2089
+ if (!checkConfig.enable) return {
2090
+ score,
2091
+ reasons
2092
+ };
2093
+ const { penalties } = checkConfig;
2094
+ const { classification, hits } = ctx.bgp;
2095
+ if (!classification) {
2096
+ score += penalties.unknownClassification;
2097
+ reasons.push("ASN_CLASSIFICATION_UNKNOWN");
2098
+ return {
2099
+ score,
2100
+ reasons
2101
+ };
2102
+ }
2103
+ if (classification === "Content") {
2104
+ score += penalties.contentClassification;
2105
+ reasons.push("ASN_HOSTING_CLASSIFIED");
2106
+ }
2107
+ const hitsNum = parseInt(hits ?? "", 10);
2108
+ const isLowVisibility = Number.isFinite(hitsNum) && hitsNum >= 0 && hitsNum < penalties.lowVisibilityThreshold;
2109
+ if (isLowVisibility) {
2110
+ score += penalties.lowVisibilityPenalty;
2111
+ reasons.push("ASN_LOW_VISIBILITY");
2112
+ }
2113
+ if (classification === "Content" && isLowVisibility) {
2114
+ score += penalties.comboHostingLowVisibility;
2115
+ reasons.push("ASN_HOSTING_LOW_VISIBILITY_COMBO");
2116
+ }
2117
+ return {
2118
+ score,
2119
+ reasons
2120
+ };
2121
+ }
2122
+ };
2123
+ CheckerRegistry.register(new AsnClassificationChecker());
2124
+
2125
+ //#endregion
2126
+ //#region src/botDetector/checkers/torAnalysis.ts
2127
+ var TorAnalysisChecker = class {
2128
+ constructor() {
2129
+ this.name = "Tor Node Analysis";
2130
+ this.phase = "cheap";
2131
+ }
2132
+ isEnabled(config) {
2133
+ return config.checkers.enableTorAnalysis.enable;
2134
+ }
2135
+ parseFlags(flags) {
2136
+ if (!flags) return /* @__PURE__ */ new Set();
2137
+ return new Set(flags.split(",").map((f) => f.trim()));
2138
+ }
2139
+ canExitWebTraffic(summary) {
2140
+ if (!summary) return false;
2141
+ try {
2142
+ const policy = JSON.parse(summary);
2143
+ const coversWebPort = (entry) => {
2144
+ if (entry === "80" || entry === "443") return true;
2145
+ if (entry.includes("-")) {
2146
+ const [lo, hi] = entry.split("-").map(Number);
2147
+ return lo <= 80 && 80 <= hi || lo <= 443 && 443 <= hi;
2148
+ }
2149
+ return false;
2150
+ };
2151
+ if (policy.accept) return policy.accept.some(coversWebPort);
2152
+ if (policy.reject) return !policy.reject.some(coversWebPort);
2153
+ } catch {}
2154
+ return false;
2155
+ }
2156
+ run(ctx, config) {
2157
+ const checkConfig = config.checkers.enableTorAnalysis;
2158
+ const reasons = [];
2159
+ let score = 0;
2160
+ if (!checkConfig.enable) return {
2161
+ score,
2162
+ reasons
2163
+ };
2164
+ if (!ctx.tor || Object.keys(ctx.tor).length === 0) return {
2165
+ score,
2166
+ reasons
2167
+ };
2168
+ const { penalties } = checkConfig;
2169
+ const { running, exit_addresses, flags, recommended_version, version_status, exit_probability, exit_policy_summary, guard_probability } = ctx.tor;
2170
+ const flagSet = this.parseFlags(flags);
2171
+ if (running) {
2172
+ score += penalties.runningNode;
2173
+ reasons.push("TOR_ACTIVE_NODE");
2174
+ }
2175
+ if ((exit_addresses && exit_addresses.length > 0) ?? flagSet.has("Exit")) {
2176
+ score += penalties.exitNode + Math.ceil((exit_probability ?? 0) * 30);
2177
+ reasons.push("TOR_EXIT_NODE");
2178
+ if (this.canExitWebTraffic(exit_policy_summary)) {
2179
+ score += penalties.webExitCapable;
2180
+ reasons.push("TOR_WEB_EXIT_CAPABLE");
2181
+ }
2182
+ }
2183
+ if (flagSet.has("BadExit")) {
2184
+ score += penalties.badExit;
2185
+ reasons.push("TOR_BAD_EXIT");
2186
+ }
2187
+ if (flagSet.has("Guard") || (guard_probability ?? 0) > 0) {
2188
+ score += penalties.guardNode;
2189
+ reasons.push("TOR_GUARD_NODE");
2190
+ }
2191
+ if (recommended_version === false || version_status === "obsolete") {
2192
+ score += penalties.obsoleteVersion;
2193
+ reasons.push("TOR_OBSOLETE_VERSION");
2194
+ }
2195
+ return {
2196
+ score,
2197
+ reasons
2198
+ };
2199
+ }
2200
+ };
2201
+ CheckerRegistry.register(new TorAnalysisChecker());
2202
+
2203
+ //#endregion
2204
+ //#region src/botDetector/checkers/timezoneConsistency.ts
2205
+ var TimezoneConsistencyChecker = class {
2206
+ constructor() {
2207
+ this.name = "Timezone Consistency";
2208
+ this.phase = "cheap";
2209
+ }
2210
+ isEnabled(config) {
2211
+ return config.checkers.enableTimezoneConsistency.enable;
2212
+ }
2213
+ run(ctx, config) {
2214
+ const checkConfig = config.checkers.enableTimezoneConsistency;
2215
+ const reasons = [];
2216
+ let score = 0;
2217
+ if (!checkConfig.enable) return {
2218
+ score,
2219
+ reasons
2220
+ };
2221
+ const geoTimezone = ctx.geoData.timezone?.toLowerCase();
2222
+ if (!geoTimezone) return {
2223
+ score,
2224
+ reasons
2225
+ };
2226
+ const tzHeader = ctx.req.get("Sec-CH-UA-Timezone") ?? ctx.req.get("X-Timezone");
2227
+ if (tzHeader && tzHeader.toLowerCase() !== geoTimezone) {
2228
+ score += checkConfig.penalties;
2229
+ reasons.push("TZ_HEADER_GEO_MISMATCH");
2230
+ }
2231
+ return {
2232
+ score,
2233
+ reasons
2234
+ };
2235
+ }
2236
+ };
2237
+ CheckerRegistry.register(new TimezoneConsistencyChecker());
2238
+
2239
+ //#endregion
2240
+ //#region src/botDetector/checkers/honeypot.ts
2241
+ var HoneypotChecker = class {
2242
+ constructor() {
2243
+ this.name = "Honeypot Path";
2244
+ this.phase = "cheap";
2245
+ }
2246
+ isEnabled(config) {
2247
+ return config.checkers.honeypot.enable;
2248
+ }
2249
+ run(ctx, config) {
2250
+ const checkConfig = config.checkers.honeypot;
2251
+ const reasons = [];
2252
+ if (!checkConfig.enable || checkConfig.paths.length === 0) return {
2253
+ score: 0,
2254
+ reasons
2255
+ };
2256
+ const requestPath = ctx.req.path.toLowerCase();
2257
+ if (checkConfig.paths.some((p) => requestPath === p.toLowerCase())) {
2258
+ reasons.push("HONEYPOT_PATH_HIT");
2259
+ reasons.push("BAD_BOT_DETECTED");
2260
+ }
2261
+ return {
2262
+ score: 0,
2263
+ reasons
2264
+ };
2265
+ }
2266
+ };
2267
+ CheckerRegistry.register(new HoneypotChecker());
2268
+
2269
+ //#endregion
2270
+ //#region src/botDetector/helpers/cache/sessionCache.ts
2271
+ const SESSION_TTL_SECONDS = 600;
2272
+ const PREFIX$3 = "session:";
2273
+ const sessionCache = {
2274
+ async get(sessionId) {
2275
+ const key = `${PREFIX$3}${sessionId}`;
2276
+ const storage = getStorage();
2277
+ const data = await storage.getItem(key);
2278
+ if (data) storage.setItem(key, data, { ttl: SESSION_TTL_SECONDS }).catch((err) => {
2279
+ consola.default.warn(`Failed to update session TTL for ${key}`, err);
2280
+ });
2281
+ return data;
2282
+ },
2283
+ async set(sessionId, entry) {
2284
+ const key = `${PREFIX$3}${sessionId}`;
2285
+ await getStorage().setItem(key, entry, { ttl: SESSION_TTL_SECONDS });
2286
+ },
2287
+ async delete(sessionId) {
2288
+ const key = `${PREFIX$3}${sessionId}`;
2289
+ await getStorage().removeItem(key);
2290
+ },
2291
+ async clear() {
2292
+ await getStorage().clear();
2293
+ }
2294
+ };
2295
+
2296
+ //#endregion
2297
+ //#region src/botDetector/checkers/sessionCoherence.ts
2298
+ var SessionCoherenceChecker = class {
2299
+ constructor() {
2300
+ this.name = "Session Coherence";
2301
+ this.phase = "heavy";
2302
+ }
2303
+ get logger() {
2304
+ this._logger ??= getLogger().child({
2305
+ service: "botDetector",
2306
+ branch: "checker",
2307
+ type: "SessionCoherenceChecker"
2308
+ });
2309
+ return this._logger;
2310
+ }
2311
+ isEnabled(config) {
2312
+ return config.checkers.enableSessionCoherence.enable;
2313
+ }
2314
+ async run(ctx, config) {
2315
+ const checkConfig = config.checkers.enableSessionCoherence;
2316
+ const reasons = [];
2317
+ let score = 0;
2318
+ if (!checkConfig.enable) return {
2319
+ score,
2320
+ reasons
2321
+ };
2322
+ if (!ctx.cookie) return {
2323
+ score,
2324
+ reasons
2325
+ };
2326
+ const currentPath = ctx.req.path;
2327
+ const refererHeader = ctx.req.get("Referer");
2328
+ const secFetchSite = ctx.req.get("Sec-Fetch-Site");
2329
+ const currentHostname = ctx.req.hostname;
2330
+ const cached = await sessionCache.get(ctx.cookie);
2331
+ if (secFetchSite === "same-origin" && !refererHeader || cached && !refererHeader) {
2332
+ score += checkConfig.penalties.missingReferer;
2333
+ reasons.push("SESSION_COHERENCE_MISSING_REFERER");
2334
+ } else if (refererHeader) try {
2335
+ const refererUrl = new URL(refererHeader);
2336
+ if (refererUrl.hostname !== currentHostname) {
2337
+ score += checkConfig.penalties.domainMismatch;
2338
+ reasons.push("SESSION_COHERENCE_DOMAIN_MISMATCH");
2339
+ } else if (cached && refererUrl.pathname !== cached.lastPath) {
2340
+ score += checkConfig.penalties.pathMismatch;
2341
+ reasons.push("SESSION_COHERENCE_PATH_MISMATCH");
2342
+ }
2343
+ } catch {
2344
+ score += checkConfig.penalties.missingReferer;
2345
+ reasons.push("SESSION_COHERENCE_INVALID_REFERER");
2346
+ }
2347
+ sessionCache.set(ctx.cookie, { lastPath: currentPath }).catch((err) => {
2348
+ this.logger.error({ err }, "Failed to save session in storage.");
2349
+ });
2350
+ return {
2351
+ score,
2352
+ reasons
2353
+ };
2354
+ }
2355
+ };
2356
+ CheckerRegistry.register(new SessionCoherenceChecker());
2357
+
2358
+ //#endregion
2359
+ //#region src/botDetector/helpers/cache/timingCache.ts
2360
+ const TIMING_TTL_SECONDS = 900;
2361
+ const PREFIX$2 = "timing:";
2362
+ const timingCache = {
2363
+ async get(visitorId) {
2364
+ const key = `${PREFIX$2}${visitorId}`;
2365
+ return await getStorage().getItem(key);
2366
+ },
2367
+ async set(visitorId, entry) {
2368
+ const key = `${PREFIX$2}${visitorId}`;
2369
+ await getStorage().setItem(key, entry, { ttl: TIMING_TTL_SECONDS });
2370
+ },
2371
+ async delete(visitorId) {
2372
+ const key = `${PREFIX$2}${visitorId}`;
2373
+ await getStorage().removeItem(key);
2374
+ },
2375
+ async clear() {
2376
+ await getStorage().clear();
2377
+ }
2378
+ };
2379
+
2380
+ //#endregion
2381
+ //#region src/botDetector/checkers/velocityFingerprint.ts
2382
+ const MAX_SAMPLES = 10;
2383
+ const MIN_SAMPLES_TO_EVALUATE = 5;
2384
+ var VelocityFingerprintChecker = class {
2385
+ constructor() {
2386
+ this.name = "Velocity Fingerprinting";
2387
+ this.phase = "heavy";
2388
+ }
2389
+ get logger() {
2390
+ this._logger ??= getLogger().child({
2391
+ service: "botDetector",
2392
+ branch: "checker",
2393
+ type: "VelocityFingerprintChecker"
2394
+ });
2395
+ return this._logger;
2396
+ }
2397
+ isEnabled(config) {
2398
+ return config.checkers.enableVelocityFingerprint.enable;
2399
+ }
2400
+ async run(ctx, config) {
2401
+ const checkConfig = config.checkers.enableVelocityFingerprint;
2402
+ const reasons = [];
2403
+ let score = 0;
2404
+ if (!checkConfig.enable || !ctx.cookie) return {
2405
+ score,
2406
+ reasons
2407
+ };
2408
+ const now = Date.now();
2409
+ const timestamps = [...await timingCache.get(ctx.cookie) ?? [], now].slice(-MAX_SAMPLES);
2410
+ timingCache.set(ctx.cookie, timestamps).catch((err) => {
2411
+ this.logger.error({ err }, "Failed to save timingCache in storage");
2412
+ });
2413
+ if (timestamps.length < MIN_SAMPLES_TO_EVALUATE) return {
2414
+ score,
2415
+ reasons
2416
+ };
2417
+ const intervals = [];
2418
+ for (let i = 1; i < timestamps.length; i++) intervals.push(timestamps[i] - timestamps[i - 1]);
2419
+ const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
2420
+ if (mean === 0) return {
2421
+ score,
2422
+ reasons
2423
+ };
2424
+ const variance = intervals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / intervals.length;
2425
+ if (Math.sqrt(variance) / mean < checkConfig.cvThreshold) {
2426
+ score += checkConfig.penalties;
2427
+ reasons.push("TIMING_TOO_REGULAR");
2428
+ }
2429
+ return {
2430
+ score,
2431
+ reasons
2432
+ };
2433
+ }
2434
+ };
2435
+ CheckerRegistry.register(new VelocityFingerprintChecker());
2436
+
2437
+ //#endregion
2438
+ //#region src/botDetector/checkers/knownBadIps.ts
2439
+ var KnownBadIps = class {
2440
+ constructor() {
2441
+ this.name = "KnownBadIps";
2442
+ this.phase = "cheap";
2443
+ }
2444
+ isEnabled(config) {
2445
+ return config.checkers.enableKnownBadIpsCheck.enable;
2446
+ }
2447
+ run(ctx, config) {
2448
+ const { enableKnownBadIpsCheck } = config.checkers;
2449
+ const reasons = [];
2450
+ let score = 0;
2451
+ if (!enableKnownBadIpsCheck.enable) return {
2452
+ score,
2453
+ reasons
2454
+ };
2455
+ const ds = getDataSources();
2456
+ const ip = ctx.ipAddress;
2457
+ if (ds.bannedDataBase(ip)) {
2458
+ reasons.push("PREVIOUSLY_BANNED_IP", "BAD_BOT_DETECTED");
2459
+ return {
2460
+ score,
2461
+ reasons
2462
+ };
2463
+ }
2464
+ const highRisk = ds.highRiskDataBase(ip);
2465
+ if (highRisk) {
2466
+ const ratio = Math.min(highRisk.score / config.banScore, 1);
2467
+ score += Math.round(enableKnownBadIpsCheck.highRiskPenalty * ratio);
2468
+ reasons.push("PREVIOUSLY_HIGH_RISK_IP");
2469
+ }
2470
+ return {
2471
+ score,
2472
+ reasons
2473
+ };
2474
+ }
2475
+ };
2476
+ CheckerRegistry.register(new KnownBadIps());
2477
+
2478
+ //#endregion
2479
+ //#region src/botDetector/helpers/exceptions.ts
2480
+ var BadBotDetected = class extends Error {
2481
+ constructor(message = "Bad bot detected") {
2482
+ super(message);
2483
+ this.name = "BadBotDetected";
2484
+ }
2485
+ };
2486
+ var GoodBotDetected = class extends Error {
2487
+ constructor(message = "Good bot detected") {
2488
+ super(message);
2489
+ this.name = "GoodBotDetected";
2490
+ }
2491
+ };
2492
+
2493
+ //#endregion
2494
+ //#region src/botDetector/penalties/banIP.ts
2495
+ const UFW = "/usr/sbin/ufw";
2496
+ function banIp(ip, info) {
2497
+ const log = getLogger().child({
2498
+ service: "BOT DETECTOR",
2499
+ branch: `banIp`,
2500
+ ipAddress: ip,
2501
+ details: info
2502
+ });
2503
+ const { punishmentType } = getConfiguration();
2504
+ if (!punishmentType.enableFireWallBan) {
2505
+ log.info(`Firewall punishment ban is disabled, skipping`);
2506
+ return;
2507
+ }
2508
+ log.info("about to ban an IP");
2509
+ return new Promise((resolve, reject) => {
2510
+ const child = (0, child_process.spawn)("sudo", [
2511
+ "-n",
2512
+ UFW,
2513
+ "insert",
2514
+ "1",
2515
+ "deny",
2516
+ "from",
2517
+ ip
2518
+ ], {
2519
+ stdio: [
2520
+ "ignore",
2521
+ "ignore",
2522
+ "pipe"
2523
+ ],
2524
+ detached: true
2525
+ });
2526
+ child.unref();
2527
+ let stderr = "";
2528
+ const timer = setTimeout(() => {
2529
+ child.kill("SIGKILL");
2530
+ log.warn(`ufw hang timeout on ${ip}`);
2531
+ reject(/* @__PURE__ */ new Error(`ufw hang timeout on ${ip}`));
2532
+ }, 5e3);
2533
+ child.stderr.on("data", (d) => stderr += String(d));
2534
+ child.on("error", (err) => {
2535
+ clearTimeout(timer);
2536
+ log.fatal({ err }, `- CRITICAL - UFW spawn failed`);
2537
+ reject(err);
2538
+ });
2539
+ child.on("close", (code) => {
2540
+ clearTimeout(timer);
2541
+ if (code !== 0) {
2542
+ log.fatal({ code }, `- CRITICAL - UFW ban failed`);
2543
+ reject(/* @__PURE__ */ new Error(`ufw exited ${String(code)}`));
2544
+ return;
2545
+ }
2546
+ log.info(`Banning Detected Bot/Malicious User. IP ${ip} banned (score ${String(info.score)})\nReasons:\n- ${info.reasons.join("\n- ")}`);
2547
+ resolve();
2548
+ });
2549
+ });
2550
+ }
2551
+
2552
+ //#endregion
2553
+ //#region src/botDetector/helpers/processChecks.ts
2554
+ async function processChecks(checkers, ctx, config, botScore, reasons, phaseLabel = "phase") {
2555
+ const { banScore } = getConfiguration();
2556
+ const log = getLogger().child({
2557
+ service: `BOT DETECTOR`,
2558
+ branch: "checks"
2559
+ });
2560
+ const reqId = Date.now();
2561
+ const phaseStart = perf_hooks.performance.now();
2562
+ log.info({
2563
+ phase: phaseLabel,
2564
+ reqId,
2565
+ event: "start"
2566
+ });
2567
+ const banLimit = banScore;
2568
+ for (const checker of checkers) {
2569
+ const label = checker.name;
2570
+ const checkStart = perf_hooks.performance.now();
2571
+ log.info({
2572
+ reqId,
2573
+ check: label,
2574
+ event: "start"
2575
+ });
2576
+ const { score, reasons: rs } = await checker.run(ctx, config);
2577
+ const checkEnd = perf_hooks.performance.now();
2578
+ log.info({
2579
+ reqId,
2580
+ check: label,
2581
+ event: "end",
2582
+ durationMs: +(checkEnd - checkStart).toFixed(3),
2583
+ score,
2584
+ reasons: rs
2585
+ });
2586
+ botScore += score;
2587
+ rs.forEach((r) => reasons.push(r));
2588
+ if (rs.includes("GOOD_BOT_IDENTIFIED")) throw new GoodBotDetected();
2589
+ if (rs.includes("BAD_BOT_DETECTED")) throw new BadBotDetected();
2590
+ if (botScore >= banLimit) {
2591
+ log.warn({
2592
+ reqId,
2593
+ botScore
2594
+ }, "Bot detected — aborting checks");
2595
+ break;
2596
+ }
2597
+ }
2598
+ const phaseEnd = perf_hooks.performance.now();
2599
+ log.info({
2600
+ reqId,
2601
+ phase: phaseLabel,
2602
+ event: "end",
2603
+ durationMs: +(phaseEnd - phaseStart).toFixed(3),
2604
+ Score: botScore
2605
+ });
2606
+ return botScore;
2607
+ }
2608
+
2609
+ //#endregion
2610
+ //#region src/botDetector/helpers/cache/reputationCache.ts
2611
+ const PREFIX$1 = "reputation:";
2612
+ const reputationCache = {
2613
+ async get(cookie) {
2614
+ return getStorage().getItem(`${PREFIX$1}${cookie}`);
2615
+ },
2616
+ async set(cookie, entry) {
2617
+ const { checksTimeRateControl } = getConfiguration();
2618
+ await getStorage().setItem(`${PREFIX$1}${cookie}`, entry, { ttl: Math.floor(checksTimeRateControl.checkEvery / 1e3) });
2619
+ },
2620
+ async delete(cookie) {
2621
+ await getStorage().removeItem(`${PREFIX$1}${cookie}`);
2622
+ },
2623
+ async clear() {
2624
+ await getStorage().clear();
2625
+ }
2626
+ };
2627
+
2628
+ //#endregion
2629
+ //#region src/botDetector.ts
2630
+ async function uaAndGeoBotDetector(req, ipAddress, userAgent, geo, parsedUA, buildCustomContext) {
2631
+ const log = getLogger().child({
2632
+ service: "BOT DETECTOR",
2633
+ branch: "main"
2634
+ });
2635
+ const { banScore, maxScore, setNewComputedScore } = getConfiguration();
2636
+ const BAN_THRESHOLD = banScore;
2637
+ const MAX_SCORE = maxScore;
2638
+ const reasons = [];
2639
+ let botScore = 0;
2640
+ const cookie = req.cookies.canary_id;
2641
+ const uaString = req.get("User-Agent") ?? "";
2642
+ log.info(`BotDetection called for ${req.method} ${req.get("X-Forwarded-Host") ?? ""}`);
2643
+ const threatsLevels = getDataSources().fireholLvl1DataBase(ipAddress) ? 1 : getDataSources().fireholLvl2DataBase(ipAddress) ? 2 : getDataSources().fireholLvl3DataBase(ipAddress) ? 3 : getDataSources().fireholLvl4DataBase(ipAddress) ? 4 : null;
2644
+ const proxy = () => {
2645
+ const isProxy = getDataSources().proxyDataBase(ipAddress);
2646
+ if (isProxy) return {
2647
+ proxy: true,
2648
+ proxyType: isProxy.comment
2649
+ };
2650
+ return { proxy: false };
2651
+ };
2652
+ const { tor, asn, threatLevel, anon } = {
2653
+ tor: getDataSources().torDataBase(ipAddress),
2654
+ asn: getDataSources().asnDataBase(ipAddress),
2655
+ threatLevel: threatsLevels,
2656
+ anon: getDataSources().fireholAnonDataBase(ipAddress) ? true : false
2657
+ };
2658
+ const proxyResult = proxy();
2659
+ const ctx = {
2660
+ req,
2661
+ ipAddress,
2662
+ parsedUA,
2663
+ geoData: geo,
2664
+ cookie,
2665
+ proxy: {
2666
+ isProxy: proxyResult.proxy,
2667
+ proxyType: proxyResult.proxyType
2668
+ },
2669
+ anon,
2670
+ bgp: asn ?? {},
2671
+ tor: tor ?? {},
2672
+ threatLevel,
2673
+ custom: buildCustomContext ? buildCustomContext(req) : {}
2674
+ };
2675
+ const cheapChecks = CheckerRegistry.getEnabled("cheap", getConfiguration());
2676
+ const checks = CheckerRegistry.getEnabled("heavy", getConfiguration());
2677
+ try {
2678
+ botScore = await processChecks(cheapChecks, ctx, getConfiguration(), botScore, reasons, "cheapPhase");
2679
+ if (botScore < BAN_THRESHOLD) botScore = await processChecks(checks, ctx, getConfiguration(), botScore, reasons, "heavyPhase");
2680
+ } catch (error) {
2681
+ if (error instanceof BadBotDetected) {
2682
+ const bannedInfo = {
2683
+ score: BAN_THRESHOLD,
2684
+ reasons: Array.from(reasons)
2685
+ };
2686
+ banIp(ipAddress, bannedInfo);
2687
+ getBatchQueue().addQueue(cookie ?? "", ipAddress, "update_banned_ip", {
2688
+ cookie: cookie ?? "",
2689
+ ipAddress,
2690
+ country: ctx.geoData.country ?? "",
2691
+ user_agent: uaString,
2692
+ info: bannedInfo
2693
+ }, "deferred");
2694
+ getBatchQueue().addQueue(cookie ?? "", ipAddress, "is_bot_update", {
2695
+ isBot: true,
2696
+ cookie: cookie ?? ""
2697
+ }, "deferred");
2698
+ return true;
2699
+ }
2700
+ if (error instanceof GoodBotDetected) return false;
2701
+ log.error({ error }, "unexpected error");
2702
+ }
2703
+ botScore = Math.min(botScore, MAX_SCORE);
2704
+ if (botScore >= BAN_THRESHOLD) {
2705
+ log.info(`Starting Ban for ${ipAddress} ${userAgent}`);
2706
+ const bannedInfo = {
2707
+ score: botScore,
2708
+ reasons: Array.from(reasons)
2709
+ };
2710
+ banIp(ipAddress, bannedInfo);
2711
+ getBatchQueue().addQueue(cookie ?? "", ipAddress, "update_banned_ip", {
2712
+ cookie: cookie ?? "",
2713
+ ipAddress,
2714
+ country: ctx.geoData.country ?? "",
2715
+ user_agent: uaString,
2716
+ info: bannedInfo
2717
+ }, "deferred");
2718
+ getBatchQueue().addQueue(cookie ?? "", ipAddress, "is_bot_update", {
2719
+ isBot: true,
2720
+ cookie: cookie ?? ""
2721
+ }, "deferred");
2722
+ return true;
2723
+ }
2724
+ if (setNewComputedScore) {
2725
+ getBatchQueue().addQueue(cookie ?? "", ipAddress, "score_update", {
2726
+ score: botScore,
2727
+ cookie: cookie ?? ""
2728
+ }, "deferred");
2729
+ reputationCache.set(cookie ?? "", {
2730
+ isBot: false,
2731
+ score: botScore
2732
+ }).catch((err) => {
2733
+ log.error({ err }, "Failed to set reputationCache in storage");
2734
+ });
2735
+ } else {
2736
+ const cached = await reputationCache.get(cookie ?? "");
2737
+ if (!cached || cached.score === 0) {
2738
+ getBatchQueue().addQueue(cookie ?? "", ipAddress, "score_update", {
2739
+ score: botScore,
2740
+ cookie: cookie ?? ""
2741
+ }, "deferred");
2742
+ reputationCache.set(cookie ?? "", {
2743
+ isBot: false,
2744
+ score: botScore
2745
+ }).catch((err) => {
2746
+ log.error({ err }, "Failed to set reputationCache in storage");
2747
+ });
2748
+ }
2749
+ }
2750
+ return false;
2751
+ }
2752
+
2753
+ //#endregion
2754
+ //#region src/botDetector/helpers/cache/cannaryCache.ts
2755
+ const PREFIX = "visitor:";
2756
+ const visitorCache = {
2757
+ async get(cookie) {
2758
+ return getStorage().getItem(`${PREFIX}${cookie}`);
2759
+ },
2760
+ async set(cookie, entry) {
2761
+ const { checksTimeRateControl } = getConfiguration();
2762
+ await getStorage().setItem(`${PREFIX}${cookie}`, entry, { ttl: Math.floor(checksTimeRateControl.checkEvery / 1e3) });
2763
+ },
2764
+ async delete(cookie) {
2765
+ await getStorage().removeItem(`${PREFIX}${cookie}`);
2766
+ },
2767
+ async clear() {
2768
+ await getStorage().clear();
2769
+ }
2770
+ };
2771
+
2772
+ //#endregion
2773
+ //#region src/botDetector/helpers/reputation.ts
2774
+ async function userReputation(cookie) {
2775
+ const log = getLogger().child({
2776
+ service: `BOT DETECTOR`,
2777
+ branch: `reputation`
2778
+ });
2779
+ const db = getDb();
2780
+ const { banScore, restoredReputationPoints, setNewComputedScore } = getConfiguration();
2781
+ const botScore = banScore;
2782
+ const cached = await reputationCache.get(cookie);
2783
+ if (cached) {
2784
+ log.info(`CACHE HIT cookie=${cookie} score=${String(cached.score)} → (botScore=${String(botScore)})`);
2785
+ if (cached.isBot) return;
2786
+ if (!cached.isBot && cached.score > 0 && cached.score < botScore) {
2787
+ log.info(`updating cache score cookie=${cookie} score=${String(cached.score)} → (botScore=${String(botScore)})`);
2788
+ const newReputation = Math.max(0, cached.score - restoredReputationPoints);
2789
+ if (newReputation !== cached.score) {
2790
+ getBatchQueue().addQueue(cookie, "", "score_update", {
2791
+ score: newReputation,
2792
+ cookie
2793
+ }, "deferred");
2794
+ log.info(`updating cache score to DB cookie=${cookie} score=${String(cached.score)} → (botScore=${String(botScore)})`);
2795
+ reputationCache.set(cookie, {
2796
+ isBot: cached.isBot,
2797
+ score: newReputation
2798
+ }).catch((err) => {
2799
+ log.error({ err }, "Failed to save reputationCache in storage");
2800
+ });
2801
+ }
2802
+ log.info(`finished Updating Score from cache to DB cookie: ${cookie} NEW SCORE: ${String(newReputation)}`);
2803
+ }
2804
+ return;
2805
+ }
2806
+ try {
2807
+ const visitor = await prep(db, `
2808
+ SELECT is_bot,
2809
+ suspicious_activity_score
2810
+ FROM visitors
2811
+ WHERE canary_id = ?
2812
+ LIMIT 1`).get(cookie);
2813
+ if (!visitor) {
2814
+ log.warn(`no visitor record for canary_id=${cookie}`);
2815
+ return;
2816
+ }
2817
+ const isBot = visitor.is_bot === 0 ? false : true;
2818
+ const reputation = visitor.suspicious_activity_score;
2819
+ if (isBot) return;
2820
+ if (!setNewComputedScore) reputationCache.set(cookie, {
2821
+ isBot,
2822
+ score: reputation
2823
+ }).catch((err) => {
2824
+ log.error({ err }, "Failed to save reputationCache in storage");
2825
+ });
2826
+ log.info({
2827
+ label: "[REP-GATE]",
2828
+ isBot,
2829
+ score: reputation,
2830
+ "score>0": reputation > 0,
2831
+ "score<ban": reputation < botScore,
2832
+ healPts: restoredReputationPoints
2833
+ });
2834
+ if (!isBot && reputation > 0 && reputation < botScore) {
2835
+ log.info(`calculating new score cookie=${cookie} score=${String(reputation)} → (botScore=${String(botScore)})`);
2836
+ const newReputation = Math.max(0, reputation - restoredReputationPoints);
2837
+ if (newReputation !== reputation) {
2838
+ getBatchQueue().addQueue(cookie, "", "score_update", {
2839
+ score: newReputation,
2840
+ cookie
2841
+ }, "deferred");
2842
+ log.info(`Update Score for cookie', ${cookie}, 'New Score:', ${String(newReputation)}`);
2843
+ reputationCache.set(cookie, {
2844
+ isBot,
2845
+ score: newReputation
2846
+ }).catch((err) => {
2847
+ log.error({ err }, "Failed to save reputationCache in storage");
2848
+ });
2849
+ }
2850
+ }
2851
+ } catch (err) {
2852
+ log.error({ err }, `An error occurred updating visitor reputation`);
2853
+ }
2854
+ }
2855
+
2856
+ //#endregion
2857
+ //#region src/botDetector/utils/whitelist.ts
2858
+ function isInWhiteList(ipAddress) {
2859
+ const { whiteList } = getConfiguration();
2860
+ if (whiteList) return whiteList.includes(ipAddress);
2861
+ return false;
2862
+ }
2863
+
2864
+ //#endregion
2865
+ //#region src/botDetector/utils/nowMysql.ts
2866
+ const nowMysql = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
2867
+
2868
+ //#endregion
2869
+ //#region src/botDetector/middlewares/canaryCookieChecker.ts
2870
+ function validator(buildCustomContext) {
2871
+ return async (req, res, next) => {
2872
+ const { checksTimeRateControl } = getConfiguration();
2873
+ let canary = req.cookies.canary_id;
2874
+ const ua = req.get("User-Agent") ?? "";
2875
+ const ip = req.ip ?? "";
2876
+ const log = getLogger().child({
2877
+ service: "BOT DETECTOR",
2878
+ branch: `main`,
2879
+ canary
2880
+ });
2881
+ log.info(`Validator entered for ${req.method} ${req.get("X-Forwarded-Host") ?? ""}`);
2882
+ log.info({ cookies: req.cookies }, `Incoming cookies`);
2883
+ const whiteList = isInWhiteList(ip);
2884
+ if (canary) {
2885
+ const cached = await visitorCache.get(canary);
2886
+ if (cached) {
2887
+ if (cached.banned && !whiteList) {
2888
+ res.sendStatus(403);
2889
+ return;
2890
+ }
2891
+ req.newVisitorId = cached.visitor_id;
2892
+ if (!checksTimeRateControl.checkEveryRequest) {
2893
+ next();
2894
+ return;
2895
+ }
2896
+ }
2897
+ }
2898
+ if (!canary) {
2899
+ log.info(`No canary_id cookie found. Generating a new one.`);
2900
+ const cookieValue = (0, crypto.randomBytes)(32).toString("hex");
2901
+ canary = cookieValue;
2902
+ makeCookie(res, "canary_id", cookieValue, {
2903
+ httpOnly: true,
2904
+ sameSite: "lax",
2905
+ maxAge: 1e3 * 60 * 60 * 24 * 90,
2906
+ secure: true,
2907
+ path: "/"
2908
+ });
2909
+ req.cookies.canary_id = canary;
2910
+ log.info(`New canary_id cookie set:, ${cookieValue}`);
2911
+ }
2912
+ const geo = getData(ip);
2913
+ const parsedUA = parseUA(ua);
2914
+ const visitorId = (0, crypto.randomUUID)();
2915
+ const userValidation = {
2916
+ visitorId,
2917
+ cookie: canary,
2918
+ userAgent: ua,
2919
+ ipAddress: ip,
2920
+ device_type: parsedUA.device,
2921
+ browser: parsedUA.browser,
2922
+ is_bot: false,
2923
+ first_seen: nowMysql(),
2924
+ last_seen: nowMysql(),
2925
+ request_count: 1,
2926
+ deviceVendor: parsedUA.deviceVendor,
2927
+ deviceModel: parsedUA.deviceModel,
2928
+ browserType: parsedUA.browserType,
2929
+ browserVersion: parsedUA.browserVersion,
2930
+ os: parsedUA.os,
2931
+ activity_score: "0",
2932
+ ...geo
2933
+ };
2934
+ getBatchQueue().addQueue(canary, ip, "visitor_upsert", { insert: userValidation }, "deferred");
2935
+ req.newVisitorId = visitorId;
2936
+ if (whiteList) {
2937
+ log.info(`${ip} is in white list skipping botDetection checks.`);
2938
+ visitorCache.set(canary, {
2939
+ banned: false,
2940
+ visitor_id: visitorId
2941
+ }).catch((err) => {
2942
+ log.error({ err }, "Failed to save visitorCache in storage");
2943
+ });
2944
+ req.botDetection = {
2945
+ success: true,
2946
+ banned: false,
2947
+ time: (/* @__PURE__ */ new Date()).toISOString(),
2948
+ ipAddress: ip
2949
+ };
2950
+ next();
2951
+ return;
2952
+ }
2953
+ const brvConfig = getConfiguration().checkers.enableBehaviorRateCheck;
2954
+ if (brvConfig.enable) {
2955
+ if (!await rateCache.get(canary)) {
2956
+ const ttlSeconds = Math.ceil(brvConfig.behavioral_window / 1e3);
2957
+ rateCache.set(canary, {
2958
+ score: 0,
2959
+ timestamp: Date.now(),
2960
+ request_count: 1
2961
+ }, ttlSeconds).catch((err) => {
2962
+ log.error({ err }, "Failed to pre seed rateCache");
2963
+ });
2964
+ }
2965
+ }
2966
+ const isBot = await uaAndGeoBotDetector(req, ip, ua, geo, parsedUA, buildCustomContext);
2967
+ visitorCache.set(canary, {
2968
+ banned: isBot,
2969
+ visitor_id: visitorId
2970
+ }).catch((err) => {
2971
+ log.error({ err }, "Failed to save visitorCache in storage");
2972
+ });
2973
+ if (isBot) {
2974
+ res.sendStatus(403);
2975
+ return;
2976
+ }
2977
+ req.botDetection = {
2978
+ success: true,
2979
+ banned: isBot,
2980
+ time: (/* @__PURE__ */ new Date()).toISOString(),
2981
+ ipAddress: ip
2982
+ };
2983
+ userReputation(canary).catch((err) => {
2984
+ consola.default.error("[BOT DETECTION - MIDDLEWARE] userReputation failed:", err);
2985
+ });
2986
+ next();
2987
+ };
2988
+ }
2989
+
2990
+ //#endregion
2991
+ //#region src/botDetector/routes/visitorLog.ts
2992
+ const router = (0, express.Router)();
2993
+ router.use("/check", validator(), (req, res) => {
2994
+ res.json({
2995
+ results: req.botDetection,
2996
+ message: "Fingerprint logged successfully"
2997
+ });
2998
+ });
2999
+
3000
+ //#endregion
3001
+ //#region src/botDetector/db/generator.ts
3002
+ const MMDB_DIR = node_path.default.resolve(getLibraryRoot(), "_data-sources");
3003
+ async function buildBannedMmdb(generateTypes, mmdbctlPath) {
3004
+ const log = getLogger().child({
3005
+ service: "BOT DETECTOR",
3006
+ branch: "generator",
3007
+ db: "banned"
3008
+ });
3009
+ const db = getDb();
3010
+ const rows = await db.prepare(`SELECT ip_address, country, user_agent, reason, score
3011
+ FROM banned
3012
+ WHERE ip_address IS NOT NULL`).all();
3013
+ if (rows.length === 0) {
3014
+ log.info("No banned IPs — skipping banned.mmdb");
3015
+ return;
3016
+ }
3017
+ const data = rows.map((r) => ({
3018
+ range: r.ip_address,
3019
+ score: r.score,
3020
+ country: r.country,
3021
+ userAgent: r.user_agent,
3022
+ reason: r.reason,
3023
+ comment: "Automatically generated by @riavzon/botDetector"
3024
+ }));
3025
+ await (0, _riavzon_shield_base.compiler)({
3026
+ type: "mmdb",
3027
+ input: {
3028
+ data,
3029
+ dataBaseName: "banned",
3030
+ outputPath: MMDB_DIR,
3031
+ mmdbPath: mmdbctlPath,
3032
+ generateTypes
3033
+ }
3034
+ });
3035
+ log.info(`banned.mmdb compiled — ${String(data.length)} entries`);
3036
+ if (getConfiguration().generator.deleteAfterBuild) {
3037
+ const compiled = rows.map((r) => r.ip_address);
3038
+ await prep(db, `DELETE FROM banned WHERE ip_address IN (${placeholders(db, compiled.length)})`).run(...compiled);
3039
+ log.info(`Deleted ${String(compiled.length)} rows from banned after build`);
3040
+ }
3041
+ }
3042
+ async function buildHighRiskMmdb(scoreThreshold, generateTypes, mmdbctlPath) {
3043
+ const log = getLogger().child({
3044
+ service: "BOT DETECTOR",
3045
+ branch: "generator",
3046
+ db: "highRisk"
3047
+ });
3048
+ const db = getDb();
3049
+ const rows = await prep(db, `SELECT
3050
+ ip_address, visitor_id, suspicious_activity_score,
3051
+ country, region, region_name, city, lat, lon, timezone,
3052
+ isp, org, browser, browserType, browserVersion, os,
3053
+ device_type, deviceVendor, deviceModel,
3054
+ proxy, hosting, request_count, first_seen, last_seen
3055
+ FROM visitors
3056
+ WHERE suspicious_activity_score >= ? AND ip_address IS NOT NULL`).all(scoreThreshold);
3057
+ if (rows.length === 0) {
3058
+ log.info(`No high-risk visitors (score >= ${String(scoreThreshold)}) — skipping highRisk.mmdb`);
3059
+ return;
3060
+ }
3061
+ const data = rows.map((r) => ({
3062
+ range: r.ip_address,
3063
+ visitorId: r.visitor_id,
3064
+ score: r.suspicious_activity_score,
3065
+ country: r.country,
3066
+ region: r.region,
3067
+ regionName: r.region_name,
3068
+ city: r.city,
3069
+ lat: r.lat,
3070
+ lon: r.lon,
3071
+ timezone: r.timezone,
3072
+ isp: r.isp,
3073
+ org: r.org,
3074
+ browser: r.browser,
3075
+ browserType: r.browserType,
3076
+ browserVersion: r.browserVersion,
3077
+ os: r.os,
3078
+ deviceType: r.device_type,
3079
+ deviceVendor: r.deviceVendor,
3080
+ deviceModel: r.deviceModel,
3081
+ proxy: Boolean(r.proxy),
3082
+ hosting: Boolean(r.hosting),
3083
+ requestCount: r.request_count,
3084
+ firstSeen: r.first_seen ? r.first_seen.toISOString() : null,
3085
+ lastSeen: r.last_seen ? r.last_seen.toISOString() : null,
3086
+ comment: "Automatically generated by @riavzon/botDetector"
3087
+ }));
3088
+ await (0, _riavzon_shield_base.compiler)({
3089
+ type: "mmdb",
3090
+ input: {
3091
+ data,
3092
+ dataBaseName: "highRisk",
3093
+ outputPath: MMDB_DIR,
3094
+ mmdbPath: mmdbctlPath,
3095
+ generateTypes
3096
+ }
3097
+ });
3098
+ log.info(`highRisk.mmdb compiled — ${String(data.length)} entries`);
3099
+ if (getConfiguration().generator.deleteAfterBuild) {
3100
+ const compiled = rows.map((r) => r.ip_address);
3101
+ await prep(db, `DELETE FROM visitors WHERE ip_address IN (${placeholders(db, compiled.length)}) AND suspicious_activity_score >= ${placeholders(db, 1, compiled.length)}`).run(...compiled, scoreThreshold);
3102
+ log.info(`Deleted ${String(compiled.length)} rows from visitors after build`);
3103
+ }
3104
+ }
3105
+ async function runCycle() {
3106
+ const log = getLogger().child({
3107
+ service: "BOT DETECTOR",
3108
+ branch: "generator"
3109
+ });
3110
+ const { generator } = getConfiguration();
3111
+ log.info("MMDB generation cycle started");
3112
+ try {
3113
+ await Promise.all([buildBannedMmdb(generator.generateTypes, generator.mmdbctlPath), buildHighRiskMmdb(generator.scoreThreshold, generator.generateTypes, generator.mmdbctlPath)]);
3114
+ log.info("MMDB generation cycle complete");
3115
+ } catch (err) {
3116
+ log.error({ err }, "MMDB generation cycle failed");
3117
+ }
3118
+ }
3119
+ async function runGeneration() {
3120
+ return runCycle();
3121
+ }
3122
+
3123
+ //#endregion
3124
+ //#region src/botDetector/db/warmUp.ts
3125
+ async function warmUp() {
3126
+ const db = getDb();
3127
+ await Promise.all(Array.from({ length: 10 }, () => db.sql`SELECT 1`));
3128
+ await prep(db, `SELECT last_seen, request_count
3129
+ FROM visitors
3130
+ WHERE canary_id = ?
3131
+ LIMIT 1`).get("00000000‑warm‑up‑row");
3132
+ consola.default.info("botDetector is ready!");
3133
+ }
3134
+
3135
+ //#endregion
3136
+ //#region src/botDetector/db/customUpdate.ts
3137
+ async function updateVisitors(data, cookie, visitor_id) {
3138
+ const db = getDb();
3139
+ const log = getLogger().child({
3140
+ service: "BOT DETECTOR",
3141
+ branch: "customUpdate"
3142
+ });
3143
+ const params = [
3144
+ data.userAgent,
3145
+ data.ipAddress,
3146
+ data.country,
3147
+ data.region,
3148
+ data.regionName,
3149
+ data.city,
3150
+ data.district,
3151
+ data.lat,
3152
+ data.lon,
3153
+ data.timezone,
3154
+ data.currency,
3155
+ data.isp,
3156
+ data.org,
3157
+ data.as,
3158
+ data.device_type,
3159
+ data.browser,
3160
+ data.proxy,
3161
+ data.hosting,
3162
+ data.deviceVendor,
3163
+ data.deviceModel,
3164
+ data.browserType,
3165
+ data.browserVersion,
3166
+ data.os
3167
+ ];
3168
+ try {
3169
+ if (!(await prep(db, `
3170
+ UPDATE visitors
3171
+ SET
3172
+ user_agent = ?,
3173
+ ip_address = ?,
3174
+ country = ?,
3175
+ region = ?,
3176
+ region_name = ?,
3177
+ city = ?,
3178
+ district = ?,
3179
+ lat = ?,
3180
+ lon = ?,
3181
+ timezone = ?,
3182
+ currency = ?,
3183
+ isp = ?,
3184
+ org = ?,
3185
+ as_org = ?,
3186
+ device_type = ?,
3187
+ browser = ?,
3188
+ proxy = ?,
3189
+ hosting = ?,
3190
+ deviceVendor = ?,
3191
+ deviceModel = ?,
3192
+ browserType = ?,
3193
+ browserVersion = ?,
3194
+ os = ?
3195
+ WHERE canary_id = ?
3196
+ AND visitor_id = ?
3197
+ `).run(...params, cookie, visitor_id)).success) {
3198
+ log.warn({
3199
+ cookie,
3200
+ visitor_id
3201
+ }, `updateVisitors: no rows affected`);
3202
+ return {
3203
+ success: false,
3204
+ reason: `No visitor found for canary_id=${cookie} visitor_id=${visitor_id}`
3205
+ };
3206
+ }
3207
+ log.info({ visitor_id }, `updateVisitors: visitor updated`);
3208
+ return { success: true };
3209
+ } catch (err) {
3210
+ log.error({ err }, `updateVisitors: query failed`);
3211
+ return {
3212
+ success: false,
3213
+ reason: "DB error — check logs for details"
3214
+ };
3215
+ }
3216
+ }
3217
+
3218
+ //#endregion
3219
+ exports.ApiResponse = router;
3220
+ exports.BadBotDetected = BadBotDetected;
3221
+ exports.CheckerRegistry = CheckerRegistry;
3222
+ exports.GoodBotDetected = GoodBotDetected;
3223
+ exports.banIp = banIp;
3224
+ exports.defineConfiguration = configuration;
3225
+ exports.detectBots = validator;
3226
+ exports.getBatchQueue = getBatchQueue;
3227
+ exports.getDataSources = getDataSources;
3228
+ exports.getGeoData = getData;
3229
+ exports.getStorage = getStorage;
3230
+ exports.parseUA = parseUA;
3231
+ exports.runGeneration = runGeneration;
3232
+ exports.updateBannedIP = updateBannedIP;
3233
+ exports.updateIsBot = updateIsBot;
3234
+ exports.updateVisitors = updateVisitors;
3235
+ exports.warmUp = warmUp;
3236
+ //# sourceMappingURL=main.cjs.map