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.
- package/LICENSE +21 -0
- package/README.md +734 -0
- package/bin/cli.js +39 -0
- package/bin/renew.js +149 -0
- package/bin/scan.js +124 -0
- package/bin/ssl.js +181 -0
- package/package.json +80 -0
- package/src/index.js +206 -0
- package/src/modules/acme/client.js +240 -0
- package/src/modules/acme/csr.js +172 -0
- package/src/modules/acme/detectDomains.js +132 -0
- package/src/modules/acme/index.js +211 -0
- package/src/modules/acme/jose.js +125 -0
- package/src/modules/acme/renew.js +89 -0
- package/src/modules/adaptiveRateLimiter.js +154 -0
- package/src/modules/alerts.js +168 -0
- package/src/modules/bodyLimit.js +106 -0
- package/src/modules/botDetector.js +144 -0
- package/src/modules/connectionGuard.js +128 -0
- package/src/modules/csrf.js +130 -0
- package/src/modules/dashboard.js +257 -0
- package/src/modules/dashboardHtml.js +235 -0
- package/src/modules/datacenterRanges.js +121 -0
- package/src/modules/ddosGuard.js +323 -0
- package/src/modules/firewall.js +239 -0
- package/src/modules/geoBlock.js +169 -0
- package/src/modules/headers.js +130 -0
- package/src/modules/honeypot.js +149 -0
- package/src/modules/httpsRedirect.js +67 -0
- package/src/modules/integrity.js +164 -0
- package/src/modules/ipBlocker.js +372 -0
- package/src/modules/ja3.js +155 -0
- package/src/modules/logExport.js +134 -0
- package/src/modules/nosqlGuard.js +143 -0
- package/src/modules/osFirewall.js +182 -0
- package/src/modules/rateLimiter.js +196 -0
- package/src/modules/sanitizer.js +253 -0
- package/src/modules/threatDetector.js +202 -0
- package/src/scanner/checks/dependencies.js +125 -0
- package/src/scanner/checks/environment.js +118 -0
- package/src/scanner/checks/exposure.js +99 -0
- package/src/scanner/checks/headers.js +119 -0
- package/src/scanner/checks/ssl.js +151 -0
- package/src/scanner/index.js +209 -0
- package/src/utils/cidr.js +140 -0
- package/src/utils/logger.js +194 -0
- 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;
|