yusufkhon-guard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +734 -0
  3. package/bin/cli.js +39 -0
  4. package/bin/renew.js +149 -0
  5. package/bin/scan.js +124 -0
  6. package/bin/ssl.js +181 -0
  7. package/package.json +80 -0
  8. package/src/index.js +206 -0
  9. package/src/modules/acme/client.js +240 -0
  10. package/src/modules/acme/csr.js +172 -0
  11. package/src/modules/acme/detectDomains.js +132 -0
  12. package/src/modules/acme/index.js +211 -0
  13. package/src/modules/acme/jose.js +125 -0
  14. package/src/modules/acme/renew.js +89 -0
  15. package/src/modules/adaptiveRateLimiter.js +154 -0
  16. package/src/modules/alerts.js +168 -0
  17. package/src/modules/bodyLimit.js +106 -0
  18. package/src/modules/botDetector.js +144 -0
  19. package/src/modules/connectionGuard.js +128 -0
  20. package/src/modules/csrf.js +130 -0
  21. package/src/modules/dashboard.js +257 -0
  22. package/src/modules/dashboardHtml.js +235 -0
  23. package/src/modules/datacenterRanges.js +121 -0
  24. package/src/modules/ddosGuard.js +323 -0
  25. package/src/modules/firewall.js +239 -0
  26. package/src/modules/geoBlock.js +169 -0
  27. package/src/modules/headers.js +130 -0
  28. package/src/modules/honeypot.js +149 -0
  29. package/src/modules/httpsRedirect.js +67 -0
  30. package/src/modules/integrity.js +164 -0
  31. package/src/modules/ipBlocker.js +372 -0
  32. package/src/modules/ja3.js +155 -0
  33. package/src/modules/logExport.js +134 -0
  34. package/src/modules/nosqlGuard.js +143 -0
  35. package/src/modules/osFirewall.js +182 -0
  36. package/src/modules/rateLimiter.js +196 -0
  37. package/src/modules/sanitizer.js +253 -0
  38. package/src/modules/threatDetector.js +202 -0
  39. package/src/scanner/checks/dependencies.js +125 -0
  40. package/src/scanner/checks/environment.js +118 -0
  41. package/src/scanner/checks/exposure.js +99 -0
  42. package/src/scanner/checks/headers.js +119 -0
  43. package/src/scanner/checks/ssl.js +151 -0
  44. package/src/scanner/index.js +209 -0
  45. package/src/utils/cidr.js +140 -0
  46. package/src/utils/logger.js +194 -0
  47. package/src/utils/privilege.js +212 -0
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file adaptiveRateLimiter.js
5
+ * @description Aqlli (xatti-harakatga asoslangan) rate-limiting. Oddiy sonli
6
+ * cheklovdan tashqari, IP'ning xatti-harakatini kuzatadi va anomaliya
7
+ * sezilsa limitni AVTOMAT qattiqlashtiradi.
8
+ *
9
+ * Kuzatiladigan signallar:
10
+ * • so'rov tezligi (velocity) — juda tez ketma-ket so'rovlar;
11
+ * • xatolik ulushi (4xx/401/403) — brute-force belgisi;
12
+ * • himoyalangan yo'llarga (masalan, /login) urinishlar zichligi.
13
+ * @module modules/adaptiveRateLimiter
14
+ */
15
+
16
+ const { defaultLogger } = require('../utils/logger');
17
+ const { defaultKeyGenerator } = require('./rateLimiter');
18
+
19
+ /**
20
+ * @typedef {Object} BehaviorRecord
21
+ * @property {number[]} timestamps - So'nggi so'rovlar vaqtlari (ms).
22
+ * @property {number} errors - Oynadagi xato javoblar soni.
23
+ * @property {number} sensitiveHits - Himoyalangan yo'llarga urinishlar.
24
+ * @property {number} penalty - To'plangan jarima (limitni pasaytiradi).
25
+ * @property {number} windowStart - Oyna boshi.
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} AdaptiveOptions
30
+ * @property {number} [windowMs=60000] - Kuzatuv oynasi.
31
+ * @property {number} [baseMax=100] - Boshlang'ich (normal) limit.
32
+ * @property {number} [minMax=10] - Eng past (jazolangan) limit.
33
+ * @property {number} [burstThreshold=10] - 1 soniyada shu sonli so'rovdan oshsa — burst.
34
+ * @property {number} [errorRatioThreshold=0.5] - Xato ulushi shu qiymatdan oshsa — jazo.
35
+ * @property {RegExp} [sensitivePaths] - Himoyalangan yo'llar (masalan /login).
36
+ * @property {number} [statusCode=429] - Bloklash status kodi.
37
+ * @property {import('./firewall').Firewall} [firewall] - Jiddiy holatda strike qo'shish uchun.
38
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
39
+ */
40
+
41
+ /** @type {AdaptiveOptions} */
42
+ const DEFAULTS = {
43
+ windowMs: 60 * 1000,
44
+ baseMax: 100,
45
+ minMax: 10,
46
+ burstThreshold: 10,
47
+ errorRatioThreshold: 0.5,
48
+ sensitivePaths: /\/(login|signin|auth|admin|password|token|otp)/i,
49
+ statusCode: 429,
50
+ };
51
+
52
+ /**
53
+ * Aqlli rate-limiting middleware'ini yaratadi.
54
+ *
55
+ * @example
56
+ * const { adaptiveRateLimiter } = require('yusufkhon-guard');
57
+ * app.use(adaptiveRateLimiter({ baseMax: 120, minMax: 15 }));
58
+ *
59
+ * @param {AdaptiveOptions} [userOptions={}]
60
+ * @returns {import('express').RequestHandler}
61
+ */
62
+ function adaptiveRateLimiter(userOptions = {}) {
63
+ const opts = { ...DEFAULTS, ...userOptions };
64
+ const logger = opts.logger || defaultLogger;
65
+ const keyGen = opts.keyGenerator || defaultKeyGenerator;
66
+
67
+ /** @type {Map<string, BehaviorRecord>} */
68
+ const store = new Map();
69
+
70
+ const cleanup = setInterval(() => {
71
+ const now = Date.now();
72
+ for (const [k, r] of store.entries()) {
73
+ if (now - r.windowStart > opts.windowMs * 3) store.delete(k);
74
+ }
75
+ }, opts.windowMs);
76
+ if (typeof cleanup.unref === 'function') cleanup.unref();
77
+
78
+ /**
79
+ * IP uchun joriy dinamik limitni hisoblaydi (jarimaga qarab pasayadi).
80
+ * @param {BehaviorRecord} rec
81
+ * @returns {number}
82
+ */
83
+ function currentLimit(rec) {
84
+ const reduced = Math.round(opts.baseMax / (1 + rec.penalty));
85
+ return Math.max(opts.minMax, reduced);
86
+ }
87
+
88
+ /**
89
+ * @param {import('express').Request} req
90
+ * @param {import('express').Response} res
91
+ * @param {import('express').NextFunction} next
92
+ */
93
+ return function adaptiveMiddleware(req, res, next) {
94
+ const key = keyGen(req);
95
+ const now = Date.now();
96
+
97
+ let rec = store.get(key);
98
+ if (!rec || now - rec.windowStart > opts.windowMs) {
99
+ rec = { timestamps: [], errors: 0, sensitiveHits: 0, penalty: rec ? Math.max(0, rec.penalty - 1) : 0, windowStart: now };
100
+ store.set(key, rec);
101
+ }
102
+
103
+ rec.timestamps.push(now);
104
+ // So'nggi 1 soniyadagi so'rovlarni sanaymiz (burst tahlili).
105
+ const lastSecond = rec.timestamps.filter((t) => now - t <= 1000).length;
106
+
107
+ // Himoyalangan yo'lga urinish.
108
+ const path = req.originalUrl || req.url || '';
109
+ if (opts.sensitivePaths.test(path)) rec.sensitiveHits += 1;
110
+
111
+ // --- Anomaliya baholovchi jarimalar ---
112
+ if (lastSecond > opts.burstThreshold) {
113
+ rec.penalty += 2;
114
+ logger.warn(`Adaptive: burst aniqlandi ${key} (${lastSecond}/s)`, { path });
115
+ }
116
+ if (opts.sensitivePaths.test(path) && rec.sensitiveHits > 5) {
117
+ rec.penalty += 1;
118
+ }
119
+ const total = rec.timestamps.length;
120
+ if (total >= 10 && rec.errors / total >= opts.errorRatioThreshold) {
121
+ rec.penalty += 2;
122
+ logger.warn(`Adaptive: yuqori xato ulushi ${key}`, { errors: rec.errors, total });
123
+ }
124
+
125
+ const limit = currentLimit(rec);
126
+ res.setHeader('RateLimit-Limit', limit);
127
+ res.setHeader('RateLimit-Remaining', Math.max(0, limit - total));
128
+
129
+ // Xato javoblarni kuzatish uchun `res.finish`ga ulanamiz.
130
+ res.on('finish', () => {
131
+ if (res.statusCode >= 400 && res.statusCode !== 429) rec.errors += 1;
132
+ });
133
+
134
+ if (total > limit) {
135
+ const retry = Math.ceil((rec.windowStart + opts.windowMs - now) / 1000);
136
+ res.setHeader('Retry-After', retry);
137
+ logger.blocked(key, 'Adaptive rate limit', { limit, total, penalty: rec.penalty, path });
138
+
139
+ // Og'ir jarima -> firewall'ga strike (avtomat ban tomon).
140
+ if (opts.firewall && rec.penalty >= 6) {
141
+ opts.firewall.blocker.addStrike(key, 40, 'Adaptive: og\'ir anomaliya');
142
+ }
143
+
144
+ return res.status
145
+ ? res.status(opts.statusCode).json({ success: false, error: 'Too Many Requests (adaptive)', retryAfter: retry, limit })
146
+ : (res.statusCode = opts.statusCode, res.end('Too Many Requests'));
147
+ }
148
+
149
+ next();
150
+ };
151
+ }
152
+
153
+ module.exports = adaptiveRateLimiter;
154
+ module.exports.adaptiveRateLimiter = adaptiveRateLimiter;
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file alerts.js
5
+ * @description Real vaqtli ogohlantirishlar (Real-Time Alerting). Hujum sodir
6
+ * bo'lganda yoki yuqori xavfli IP bloklanganda server adminiga
7
+ * Telegram, Discord yoki ixtiyoriy Webhook orqali darhol xabar
8
+ * yuboradi. Node 18+ global `fetch` dan foydalanadi (zero-dep).
9
+ *
10
+ * Spamning oldini olish uchun ogohlantirishlar "throttle" qilinadi.
11
+ * @module modules/alerts
12
+ */
13
+
14
+ const { defaultLogger } = require('../utils/logger');
15
+
16
+ /**
17
+ * @typedef {Object} AlertOptions
18
+ * @property {string} [telegramBotToken] - Telegram bot tokeni.
19
+ * @property {string|number} [telegramChatId] - Telegram chat ID.
20
+ * @property {string} [discordWebhookUrl] - Discord webhook URL.
21
+ * @property {string} [webhookUrl] - Ixtiyoriy umumiy webhook (JSON POST).
22
+ * @property {number} [throttleMs=15000] - Bir xil turdagi xabarlar orasidagi minimal vaqt.
23
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
24
+ */
25
+
26
+ /**
27
+ * Ogohlantirishlarni turli kanallarga yuboruvchi dispetcher.
28
+ * @class
29
+ */
30
+ class Alerter {
31
+ /**
32
+ * @param {AlertOptions} [options={}]
33
+ */
34
+ constructor(options = {}) {
35
+ this.options = options;
36
+ this.logger = options.logger || defaultLogger;
37
+ this.throttleMs = options.throttleMs != null ? options.throttleMs : 15000;
38
+ /** @type {Map<string, number>} */
39
+ this._lastSent = new Map();
40
+ }
41
+
42
+ /**
43
+ * Xabar throttle (bosilgan) holatidami?
44
+ * @private
45
+ * @param {string} key
46
+ * @returns {boolean}
47
+ */
48
+ _throttled(key) {
49
+ const now = Date.now();
50
+ const last = this._lastSent.get(key) || 0;
51
+ if (now - last < this.throttleMs) return true;
52
+ this._lastSent.set(key, now);
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Telegram orqali yuboradi.
58
+ * @private
59
+ * @param {string} text
60
+ * @returns {Promise<boolean>}
61
+ */
62
+ async _telegram(text) {
63
+ const { telegramBotToken, telegramChatId } = this.options;
64
+ if (!telegramBotToken || !telegramChatId) return false;
65
+ const url = `https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
66
+ const res = await fetch(url, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify({ chat_id: telegramChatId, text, parse_mode: 'HTML' }),
70
+ });
71
+ return res.ok;
72
+ }
73
+
74
+ /**
75
+ * Discord webhook orqali yuboradi.
76
+ * @private
77
+ * @param {string} text
78
+ * @returns {Promise<boolean>}
79
+ */
80
+ async _discord(text) {
81
+ if (!this.options.discordWebhookUrl) return false;
82
+ const res = await fetch(this.options.discordWebhookUrl, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ content: text }),
86
+ });
87
+ return res.ok;
88
+ }
89
+
90
+ /**
91
+ * Umumiy webhook orqali yuboradi (to'liq hodisa JSON'i bilan).
92
+ * @private
93
+ * @param {Object} payload
94
+ * @returns {Promise<boolean>}
95
+ */
96
+ async _webhook(payload) {
97
+ if (!this.options.webhookUrl) return false;
98
+ const res = await fetch(this.options.webhookUrl, {
99
+ method: 'POST',
100
+ headers: { 'Content-Type': 'application/json' },
101
+ body: JSON.stringify(payload),
102
+ });
103
+ return res.ok;
104
+ }
105
+
106
+ /**
107
+ * Ogohlantirish yuboradi (barcha sozlangan kanallarga).
108
+ *
109
+ * @param {Object} alert
110
+ * @param {string} alert.title - Sarlavha.
111
+ * @param {string} alert.message - Xabar matni.
112
+ * @param {string} [alert.severity='high'] - Jiddiylik darajasi.
113
+ * @param {Object} [alert.meta] - Qo'shimcha ma'lumot (webhook uchun).
114
+ * @returns {Promise<{ sent: string[], throttled: boolean }>}
115
+ */
116
+ async send(alert) {
117
+ const key = `${alert.title}:${alert.severity || 'high'}`;
118
+ if (this._throttled(key)) {
119
+ return { sent: [], throttled: true };
120
+ }
121
+
122
+ const emoji = { critical: '🟥', high: '🟧', medium: '🟨', low: '🟦' }[alert.severity] || '🛡️';
123
+ const text = `${emoji} <b>${alert.title}</b>\n${alert.message}`;
124
+ const sent = [];
125
+
126
+ const tasks = [
127
+ this._telegram(text).then((ok) => ok && sent.push('telegram')),
128
+ this._discord(`${emoji} **${alert.title}**\n${alert.message}`).then((ok) => ok && sent.push('discord')),
129
+ this._webhook({ ...alert, at: new Date().toISOString() }).then((ok) => ok && sent.push('webhook')),
130
+ ];
131
+
132
+ try {
133
+ await Promise.allSettled(tasks);
134
+ } catch (e) {
135
+ this.logger.error(`Alert yuborishda xato: ${e.message}`);
136
+ }
137
+
138
+ if (sent.length) this.logger.info(`Ogohlantirish yuborildi: ${sent.join(', ')}`);
139
+ return { sent, throttled: false };
140
+ }
141
+
142
+ /**
143
+ * Firewall (va boshqa EventEmitter) hodisalariga obuna bo'lib, avtomat
144
+ * ogohlantirish yuboradi.
145
+ *
146
+ * @param {import('events').EventEmitter} emitter - Masalan, firewall nusxasi.
147
+ * @param {Object} [opts]
148
+ * @param {string} [opts.minSeverity='high'] - Shu darajadan past tahdidlar e'tiborsiz qoldiriladi.
149
+ * @returns {void}
150
+ */
151
+ attach(emitter, opts = {}) {
152
+ const order = ['low', 'medium', 'high', 'critical'];
153
+ const min = order.indexOf(opts.minSeverity || 'high');
154
+
155
+ emitter.on('ban', ({ ip, reason }) => {
156
+ this.send({ title: 'Yangi IP bloklandi', message: `IP: ${ip}\nSabab: ${reason}`, severity: 'high', meta: { ip, reason } });
157
+ });
158
+
159
+ emitter.on('threat', ({ ip, report }) => {
160
+ const sev = report && report.severity;
161
+ if (order.indexOf(sev) >= min) {
162
+ this.send({ title: `Tahdid: ${sev}`, message: `IP: ${ip}\nBall: ${report.score}`, severity: sev, meta: { ip, score: report.score } });
163
+ }
164
+ });
165
+ }
166
+ }
167
+
168
+ module.exports = { Alerter };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file bodyLimit.js
5
+ * @description Payload (so'rov tanasi) hajmi cheklovi — DoS himoyasi. Serverga
6
+ * birdaniga katta hajmli zararli JSON/fayl yuborib uni qotirib
7
+ * qo'yish urinishlarining oldini oladi.
8
+ *
9
+ * Ikki bosqichli tekshiruv:
10
+ * 1) `Content-Length` sarlavhasi bo'yicha oldindan rad etish;
11
+ * 2) haqiqiy oqim (stream) hajmini kuzatib, chegaradan oshsa uzish.
12
+ * @module modules/bodyLimit
13
+ */
14
+
15
+ const { defaultLogger } = require('../utils/logger');
16
+
17
+ /**
18
+ * "10kb", "2mb", "1gb" kabi qatorlarni baytga o'giradi.
19
+ * @param {string|number} value
20
+ * @returns {number} Baytlar soni.
21
+ */
22
+ function parseSize(value) {
23
+ if (typeof value === 'number') return value;
24
+ const m = String(value).trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
25
+ if (!m) return NaN;
26
+ const n = parseFloat(m[1]);
27
+ const unit = m[2] || 'b';
28
+ const mult = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 }[unit];
29
+ return Math.round(n * mult);
30
+ }
31
+
32
+ /**
33
+ * @typedef {Object} BodyLimitOptions
34
+ * @property {string|number} [maxBodySize='1mb'] - Ruxsat etilgan maksimal hajm.
35
+ * @property {number} [statusCode=413] - Payload Too Large status kodi.
36
+ * @property {import('./firewall').Firewall} [firewall] - Berilsa, oshirib yuborgan IP'ga strike qo'shadi.
37
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
38
+ */
39
+
40
+ /**
41
+ * Payload hajmi cheklovi middleware'ini yaratadi.
42
+ * Diqqat: uni `express.json()` dan OLDIN ulang.
43
+ *
44
+ * @example
45
+ * const { bodyLimit } = require('yusufkhon-guard');
46
+ * app.use(bodyLimit({ maxBodySize: '512kb' }));
47
+ * app.use(express.json());
48
+ *
49
+ * @param {BodyLimitOptions} [options={}]
50
+ * @returns {import('express').RequestHandler}
51
+ */
52
+ function bodyLimit(options = {}) {
53
+ const logger = options.logger || defaultLogger;
54
+ const maxBytes = parseSize(options.maxBodySize || '1mb');
55
+ const statusCode = options.statusCode || 413;
56
+
57
+ /**
58
+ * @param {import('express').Request} req
59
+ * @param {import('express').Response} res
60
+ * @param {import('express').NextFunction} next
61
+ */
62
+ return function bodyLimitMiddleware(req, res, next) {
63
+ const reject = (bytes) => {
64
+ // `next()` chaqirilib parallel kuzatilgani uchun, downstream handler
65
+ // allaqachon javob bergan bo'lishi mumkin. Bunday holda ikkinchi javob
66
+ // "headers already sent" xatosini keltirib chiqaradi — shu bois skip qilamiz.
67
+ // (Oqim baribir chaqiruvchi tomonda `req.destroy()` bilan to'xtatiladi.)
68
+ if (res.headersSent) return;
69
+ logger.blocked(req.ip || 'unknown', 'Payload hajmi chegaradan oshdi', {
70
+ path: req.originalUrl || req.url, bytes, max: maxBytes,
71
+ });
72
+ if (options.firewall) options.firewall.blocker.addStrike(req.ip || 'unknown', 25, 'Oversized payload');
73
+ if (res.status) res.status(statusCode).json({ success: false, error: 'Payload Too Large', maxBytes });
74
+ else { res.statusCode = statusCode; res.end('Payload Too Large'); }
75
+ };
76
+
77
+ // 1. Content-Length bo'yicha oldindan tekshiruv.
78
+ const declared = Number(req.headers['content-length']);
79
+ if (!Number.isNaN(declared) && declared > maxBytes) {
80
+ return reject(declared);
81
+ }
82
+
83
+ // 2. Haqiqiy oqim hajmini kuzatamiz.
84
+ let received = 0;
85
+ let aborted = false;
86
+ const onData = (chunk) => {
87
+ if (aborted) return;
88
+ received += chunk.length;
89
+ if (received > maxBytes) {
90
+ aborted = true;
91
+ req.removeListener('data', onData);
92
+ reject(received);
93
+ // Oqimni to'xtatamiz.
94
+ if (typeof req.destroy === 'function') req.destroy();
95
+ }
96
+ };
97
+ req.on('data', onData);
98
+ req.on('end', () => req.removeListener('data', onData));
99
+
100
+ next();
101
+ };
102
+ }
103
+
104
+ module.exports = bodyLimit;
105
+ module.exports.bodyLimit = bodyLimit;
106
+ module.exports.parseSize = parseSize;
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file botDetector.js
5
+ * @description HTTP darajasidagi bot/skript aniqlash (fingerprinting). So'rov
6
+ * haqiqiy brauzerdan yoki avtomatik vositadan (curl, python-requests,
7
+ * wget, go-http, axios va h.k.) kelayotganini sarlavhalar tahlili
8
+ * orqali baholaydi.
9
+ *
10
+ * Ixtiyoriy ravishda JA3 (TLS) barmoq izi bilan birga ishlaydi:
11
+ * agar so'rov soketiga `ja3Hash` biriktirilgan bo'lsa, u ham
12
+ * hisobga olinadi.
13
+ * @module modules/botDetector
14
+ */
15
+
16
+ const { defaultLogger } = require('../utils/logger');
17
+
18
+ /**
19
+ * Aniq skript/kutubxona User-Agent belgilari.
20
+ * @type {RegExp}
21
+ */
22
+ const SCRIPT_AGENTS = /(curl|wget|python-requests|python-urllib|go-http-client|libwww-perl|java\/|okhttp|axios\/|node-fetch|got\/|httpie|postmanruntime|scrapy|aiohttp|guzzlehttp|winhttp|powershell)/i;
23
+
24
+ /**
25
+ * Haqiqiy brauzer belgilari (Chrome/Firefox/Safari/Edge).
26
+ * @type {RegExp}
27
+ */
28
+ const BROWSER_AGENTS = /(mozilla\/5\.0.*(chrome|firefox|safari|edg|opr)\/)/i;
29
+
30
+ /**
31
+ * @typedef {Object} BotReport
32
+ * @property {number} score - Bot ehtimoli balli (0..100, yuqori — bot).
33
+ * @property {boolean} isBot - Chegaradan oshdimi.
34
+ * @property {string[]} reasons - Aniqlangan belgilar.
35
+ * @property {string|null} ja3 - JA3 hash (mavjud bo'lsa).
36
+ */
37
+
38
+ /**
39
+ * So'rovni tahlil qilib, bot-hisobotini qaytaradi (middleware'siz, alohida ham ishlatiladi).
40
+ * @param {import('express').Request} req
41
+ * @returns {BotReport}
42
+ */
43
+ function analyzeRequest(req) {
44
+ const h = req.headers || {};
45
+ const ua = String(h['user-agent'] || '');
46
+ const reasons = [];
47
+ let score = 0;
48
+
49
+ // 1. User-Agent umuman yo'q.
50
+ if (!ua) { score += 40; reasons.push('user-agent yo\'q'); }
51
+
52
+ // 2. Aniq skript kutubxonasi.
53
+ if (SCRIPT_AGENTS.test(ua)) { score += 55; reasons.push('skript user-agent'); }
54
+
55
+ // 3. Brauzerga o'xshamaydi (lekin UA bor).
56
+ if (ua && !BROWSER_AGENTS.test(ua) && !SCRIPT_AGENTS.test(ua)) {
57
+ score += 20; reasons.push('brauzer bo\'lmagan user-agent');
58
+ }
59
+
60
+ // 4. Brauzerlar odatda yuboradigan sarlavhalarning yo'qligi.
61
+ if (!h['accept']) { score += 15; reasons.push('accept yo\'q'); }
62
+ if (!h['accept-language']) { score += 15; reasons.push('accept-language yo\'q'); }
63
+ if (!h['accept-encoding']) { score += 10; reasons.push('accept-encoding yo\'q'); }
64
+
65
+ // 5. Zamonaviy Chrome yuboradigan sec-* sarlavhalari (brauzer da'vo qilsa, lekin yo'q bo'lsa).
66
+ const claimsChrome = /chrome\/|edg\//i.test(ua);
67
+ if (claimsChrome && !h['sec-fetch-site'] && !h['sec-ch-ua']) {
68
+ score += 25; reasons.push('Chrome da\'vo qiladi, lekin sec-* sarlavhalari yo\'q (soxta UA)');
69
+ }
70
+
71
+ // 6. JA3 (TLS) — agar soketga biriktirilgan bo'lsa.
72
+ const ja3 = (req.socket && req.socket.ja3Hash) || null;
73
+
74
+ score = Math.min(100, score);
75
+ return { score, isBot: false, reasons, ja3 };
76
+ }
77
+
78
+ /**
79
+ * @typedef {Object} BotDetectorOptions
80
+ * @property {number} [threshold=50] - Shu balldan oshsa "bot" deb hisoblanadi.
81
+ * @property {boolean} [block=false] - Bot aniqlansa so'rovni bloklash (aks holda faqat belgilaydi).
82
+ * @property {number} [statusCode=403] - Bloklashda status kodi.
83
+ * @property {string[]} [allowedBots] - Ruxsat etilgan botlar (masalan, 'googlebot','bingbot').
84
+ * @property {import('./firewall').Firewall} [firewall] - Berilsa, bot IP'ga strike qo'shadi.
85
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
86
+ */
87
+
88
+ /**
89
+ * Bot aniqlash middleware'ini yaratadi. Natija `req.botScore` va `req.isBot`
90
+ * sifatida keyingi middleware'larga uzatiladi.
91
+ *
92
+ * @param {BotDetectorOptions} [options={}]
93
+ * @returns {import('express').RequestHandler}
94
+ */
95
+ function botDetector(options = {}) {
96
+ const threshold = options.threshold != null ? options.threshold : 50;
97
+ const block = !!options.block;
98
+ const statusCode = options.statusCode || 403;
99
+ const allowed = (options.allowedBots || []).map((b) => b.toLowerCase());
100
+ const logger = options.logger || defaultLogger;
101
+
102
+ /**
103
+ * @param {import('express').Request} req
104
+ * @param {import('express').Response} res
105
+ * @param {import('express').NextFunction} next
106
+ */
107
+ return function botDetectorMiddleware(req, res, next) {
108
+ const ua = String((req.headers && req.headers['user-agent']) || '').toLowerCase();
109
+
110
+ // Ruxsat etilgan botlar (masalan, qidiruv tizimlari) o'tkaziladi.
111
+ if (allowed.some((b) => ua.includes(b))) {
112
+ req.isBot = false;
113
+ req.botScore = 0;
114
+ return next();
115
+ }
116
+
117
+ const report = analyzeRequest(req);
118
+ report.isBot = report.score >= threshold;
119
+
120
+ req.botScore = report.score;
121
+ req.isBot = report.isBot;
122
+ req.botReport = report;
123
+
124
+ if (report.isBot && block) {
125
+ const ip = req.ip || 'unknown';
126
+ logger.blocked(ip, 'Bot/skript aniqlandi', {
127
+ score: report.score,
128
+ reasons: report.reasons,
129
+ ja3: report.ja3,
130
+ });
131
+ if (options.firewall) options.firewall.blocker.addStrike(ip, 50, 'Bot fingerprint');
132
+ return res.status
133
+ ? res.status(statusCode).json({ success: false, error: 'Avtomatlashtirilgan so\'rovlar cheklangan.' })
134
+ : (res.statusCode = statusCode, res.end('Forbidden'));
135
+ }
136
+
137
+ next();
138
+ };
139
+ }
140
+
141
+ module.exports = botDetector;
142
+ module.exports.botDetector = botDetector;
143
+ module.exports.analyzeRequest = analyzeRequest;
144
+ module.exports.SCRIPT_AGENTS = SCRIPT_AGENTS;