@guardcore/core 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/chunk-4HBVN5N7.js +256 -0
- package/dist/chunk-4HBVN5N7.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-I634N6VV.js +1099 -0
- package/dist/chunk-I634N6VV.js.map +1 -0
- package/dist/chunk-LK7N5CAQ.js +132 -0
- package/dist/chunk-LK7N5CAQ.js.map +1 -0
- package/dist/cloud-46J7XK7I.js +8 -0
- package/dist/cloud-46J7XK7I.js.map +1 -0
- package/dist/index.cjs +4120 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +898 -0
- package/dist/index.d.ts +898 -0
- package/dist/index.js +2533 -0
- package/dist/index.js.map +1 -0
- package/dist/sus-patterns-YZFPGJEF.js +8 -0
- package/dist/sus-patterns-YZFPGJEF.js.map +1 -0
- package/dist/utils-5L6SNIYK.js +22 -0
- package/dist/utils-5L6SNIYK.js.map +1 -0
- package/package.json +76 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/detection-engine/compiler.ts
|
|
34
|
+
async function loadRE2() {
|
|
35
|
+
if (RE2Ctor) return RE2Ctor;
|
|
36
|
+
try {
|
|
37
|
+
const mod = await import("re2-wasm");
|
|
38
|
+
RE2Ctor = mod.RE2 ?? mod.default?.RE2 ?? mod.default;
|
|
39
|
+
return RE2Ctor;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
var DANGEROUS_PATTERNS, DEFAULT_TEST_STRINGS, RE2Ctor, PatternCompiler;
|
|
45
|
+
var init_compiler = __esm({
|
|
46
|
+
"src/detection-engine/compiler.ts"() {
|
|
47
|
+
"use strict";
|
|
48
|
+
DANGEROUS_PATTERNS = [
|
|
49
|
+
/\(\.\*\)\+/,
|
|
50
|
+
/\(\.\+\)\+/,
|
|
51
|
+
/\([^)]*\*\)\+/,
|
|
52
|
+
/\([^)]*\+\)\+/,
|
|
53
|
+
/(?:\.\*){2,}/,
|
|
54
|
+
/(?:\.\+){2,}/
|
|
55
|
+
];
|
|
56
|
+
DEFAULT_TEST_STRINGS = [
|
|
57
|
+
"a".repeat(10),
|
|
58
|
+
"a".repeat(100),
|
|
59
|
+
"a".repeat(1e3),
|
|
60
|
+
"x".repeat(50) + "y".repeat(50),
|
|
61
|
+
"<".repeat(100) + ">".repeat(100)
|
|
62
|
+
];
|
|
63
|
+
RE2Ctor = null;
|
|
64
|
+
PatternCompiler = class {
|
|
65
|
+
constructor(defaultTimeoutMs = 2e3, maxCacheSize = 1e3) {
|
|
66
|
+
this.defaultTimeoutMs = defaultTimeoutMs;
|
|
67
|
+
this.maxCacheSize = maxCacheSize;
|
|
68
|
+
this.maxCacheSize = Math.min(maxCacheSize, 5e3);
|
|
69
|
+
}
|
|
70
|
+
cache = /* @__PURE__ */ new Map();
|
|
71
|
+
cacheOrder = [];
|
|
72
|
+
re2Available = null;
|
|
73
|
+
async ensureRE2() {
|
|
74
|
+
if (this.re2Available !== null) return this.re2Available;
|
|
75
|
+
const ctor = await loadRE2();
|
|
76
|
+
this.re2Available = ctor !== null;
|
|
77
|
+
return this.re2Available;
|
|
78
|
+
}
|
|
79
|
+
async compile(pattern, flags = "gi") {
|
|
80
|
+
const key = `${pattern}:${flags}`;
|
|
81
|
+
if (this.cache.has(key)) {
|
|
82
|
+
const idx = this.cacheOrder.indexOf(key);
|
|
83
|
+
if (idx !== -1) {
|
|
84
|
+
this.cacheOrder.splice(idx, 1);
|
|
85
|
+
this.cacheOrder.push(key);
|
|
86
|
+
}
|
|
87
|
+
return this.cache.get(key);
|
|
88
|
+
}
|
|
89
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
90
|
+
const oldest = this.cacheOrder.shift();
|
|
91
|
+
if (oldest) this.cache.delete(oldest);
|
|
92
|
+
}
|
|
93
|
+
let compiled;
|
|
94
|
+
if (await this.ensureRE2()) {
|
|
95
|
+
try {
|
|
96
|
+
compiled = new RE2Ctor(pattern, flags);
|
|
97
|
+
} catch {
|
|
98
|
+
compiled = new RegExp(pattern, flags);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
compiled = new RegExp(pattern, flags);
|
|
102
|
+
}
|
|
103
|
+
this.cache.set(key, compiled);
|
|
104
|
+
this.cacheOrder.push(key);
|
|
105
|
+
return compiled;
|
|
106
|
+
}
|
|
107
|
+
compileSync(pattern, flags = "gi") {
|
|
108
|
+
return new RegExp(pattern, flags);
|
|
109
|
+
}
|
|
110
|
+
async safeMatch(pattern, content, timeoutMs) {
|
|
111
|
+
try {
|
|
112
|
+
const compiled = await this.compile(pattern);
|
|
113
|
+
return compiled.exec(content);
|
|
114
|
+
} catch {
|
|
115
|
+
return this.fallbackMatch(pattern, content, timeoutMs ?? this.defaultTimeoutMs);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async fallbackMatch(pattern, content, timeoutMs) {
|
|
119
|
+
try {
|
|
120
|
+
const { Worker } = await import("worker_threads");
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const workerCode = `
|
|
123
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
124
|
+
try {
|
|
125
|
+
const re = new RegExp(workerData.pattern, workerData.flags);
|
|
126
|
+
const result = re.exec(workerData.content);
|
|
127
|
+
parentPort.postMessage({ result });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
parentPort.postMessage({ result: null });
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
const worker = new Worker(workerCode, {
|
|
133
|
+
eval: true,
|
|
134
|
+
workerData: { pattern, content, flags: "gi" }
|
|
135
|
+
});
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
worker.terminate();
|
|
138
|
+
resolve(null);
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
worker.on("message", (msg) => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
worker.terminate();
|
|
143
|
+
resolve(msg.result);
|
|
144
|
+
});
|
|
145
|
+
worker.on("error", () => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
worker.terminate();
|
|
148
|
+
resolve(null);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
} catch {
|
|
152
|
+
try {
|
|
153
|
+
const re = new RegExp(pattern, "gi");
|
|
154
|
+
return re.exec(content);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
validatePatternSafety(pattern, testStrings) {
|
|
161
|
+
for (const dangerous of DANGEROUS_PATTERNS) {
|
|
162
|
+
if (dangerous.test(pattern)) {
|
|
163
|
+
return [false, `Pattern contains dangerous construct: ${dangerous.source}`];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const strings = testStrings ?? DEFAULT_TEST_STRINGS;
|
|
167
|
+
try {
|
|
168
|
+
const compiled = this.compileSync(pattern);
|
|
169
|
+
for (const testStr of strings) {
|
|
170
|
+
const start = performance.now();
|
|
171
|
+
compiled.exec(testStr);
|
|
172
|
+
const elapsed = performance.now() - start;
|
|
173
|
+
if (elapsed > 50) {
|
|
174
|
+
return [false, `Pattern timed out on test string of length ${testStr.length}`];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return [false, `Pattern validation failed: ${String(e)}`];
|
|
179
|
+
}
|
|
180
|
+
return [true, "Pattern appears safe"];
|
|
181
|
+
}
|
|
182
|
+
async batchCompile(patterns, validate = true) {
|
|
183
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const pattern of patterns) {
|
|
185
|
+
if (validate) {
|
|
186
|
+
const [isSafe] = this.validatePatternSafety(pattern);
|
|
187
|
+
if (!isSafe) continue;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
compiled.set(pattern, await this.compile(pattern));
|
|
191
|
+
} catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return compiled;
|
|
196
|
+
}
|
|
197
|
+
async clearCache() {
|
|
198
|
+
this.cache.clear();
|
|
199
|
+
this.cacheOrder = [];
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// src/detection-engine/preprocessor.ts
|
|
206
|
+
var ATTACK_INDICATORS, LOOKALIKES, CONTROL_CHARS_RE, ContentPreprocessor;
|
|
207
|
+
var init_preprocessor = __esm({
|
|
208
|
+
"src/detection-engine/preprocessor.ts"() {
|
|
209
|
+
"use strict";
|
|
210
|
+
ATTACK_INDICATORS = [
|
|
211
|
+
/<script/i,
|
|
212
|
+
/javascript:/i,
|
|
213
|
+
/on\w+=/i,
|
|
214
|
+
/SELECT\s+.{0,50}?\s+FROM/i,
|
|
215
|
+
/UNION\s+SELECT/i,
|
|
216
|
+
/\.\.\//,
|
|
217
|
+
/eval\s*\(/i,
|
|
218
|
+
/exec\s*\(/i,
|
|
219
|
+
/system\s*\(/i,
|
|
220
|
+
/<\?php/i,
|
|
221
|
+
/<%%/,
|
|
222
|
+
/\{\{/,
|
|
223
|
+
/\{%/,
|
|
224
|
+
/<iframe/i,
|
|
225
|
+
/<object/i,
|
|
226
|
+
/<embed/i,
|
|
227
|
+
/onerror\s*=/i,
|
|
228
|
+
/onload\s*=/i,
|
|
229
|
+
/\$\{/,
|
|
230
|
+
/\\x[0-9a-fA-F]{2}/,
|
|
231
|
+
/%[0-9a-fA-F]{2}/
|
|
232
|
+
];
|
|
233
|
+
LOOKALIKES = {
|
|
234
|
+
"\u2044": "/",
|
|
235
|
+
"\uFF0F": "/",
|
|
236
|
+
"\u29F8": "/",
|
|
237
|
+
"\u0130": "I",
|
|
238
|
+
"\u0131": "i",
|
|
239
|
+
"\u200B": "",
|
|
240
|
+
"\u200C": "",
|
|
241
|
+
"\u200D": "",
|
|
242
|
+
"\uFEFF": "",
|
|
243
|
+
"\xAD": "",
|
|
244
|
+
"\u034F": "",
|
|
245
|
+
"\u180E": "",
|
|
246
|
+
"\u2028": "\n",
|
|
247
|
+
"\u2029": "\n",
|
|
248
|
+
"\uE000": "",
|
|
249
|
+
"\uFFF0": "",
|
|
250
|
+
"\u01C0": "|",
|
|
251
|
+
"\u037E": ";",
|
|
252
|
+
"\u2215": "/",
|
|
253
|
+
"\u2216": "\\",
|
|
254
|
+
"\uFF1C": "<",
|
|
255
|
+
"\uFF1E": ">"
|
|
256
|
+
};
|
|
257
|
+
CONTROL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
|
|
258
|
+
ContentPreprocessor = class {
|
|
259
|
+
maxContentLength;
|
|
260
|
+
preserveAttackPatterns;
|
|
261
|
+
constructor(maxContentLength = 1e4, preserveAttackPatterns = true) {
|
|
262
|
+
this.maxContentLength = maxContentLength;
|
|
263
|
+
this.preserveAttackPatterns = preserveAttackPatterns;
|
|
264
|
+
}
|
|
265
|
+
normalizeUnicode(content) {
|
|
266
|
+
let normalized = content.normalize("NFKC");
|
|
267
|
+
for (const [char, replacement] of Object.entries(LOOKALIKES)) {
|
|
268
|
+
normalized = normalized.replaceAll(char, replacement);
|
|
269
|
+
}
|
|
270
|
+
return normalized;
|
|
271
|
+
}
|
|
272
|
+
removeNullBytes(content) {
|
|
273
|
+
return content.replace(/\x00/g, "").replace(CONTROL_CHARS_RE, "");
|
|
274
|
+
}
|
|
275
|
+
removeExcessiveWhitespace(content) {
|
|
276
|
+
return content.replace(/\s+/g, " ").trim();
|
|
277
|
+
}
|
|
278
|
+
decodeCommonEncodings(content) {
|
|
279
|
+
const maxIterations = 3;
|
|
280
|
+
let current = content;
|
|
281
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
282
|
+
const original = current;
|
|
283
|
+
try {
|
|
284
|
+
const decoded = decodeURIComponent(current);
|
|
285
|
+
if (decoded !== current) current = decoded;
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
current = this.decodeHtmlEntities(current);
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
if (current === original) break;
|
|
293
|
+
}
|
|
294
|
+
return current;
|
|
295
|
+
}
|
|
296
|
+
decodeHtmlEntities(content) {
|
|
297
|
+
const entityMap = {
|
|
298
|
+
"&": "&",
|
|
299
|
+
"<": "<",
|
|
300
|
+
">": ">",
|
|
301
|
+
""": '"',
|
|
302
|
+
"'": "'",
|
|
303
|
+
"'": "'",
|
|
304
|
+
"'": "'",
|
|
305
|
+
"/": "/",
|
|
306
|
+
"/": "/",
|
|
307
|
+
" ": " "
|
|
308
|
+
};
|
|
309
|
+
let result = content;
|
|
310
|
+
for (const [entity, char] of Object.entries(entityMap)) {
|
|
311
|
+
result = result.replaceAll(entity, char);
|
|
312
|
+
}
|
|
313
|
+
result = result.replace(
|
|
314
|
+
/&#x([0-9a-fA-F]+);/g,
|
|
315
|
+
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
316
|
+
);
|
|
317
|
+
result = result.replace(
|
|
318
|
+
/&#(\d+);/g,
|
|
319
|
+
(_, dec) => String.fromCharCode(parseInt(dec, 10))
|
|
320
|
+
);
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
extractAttackRegions(content) {
|
|
324
|
+
const maxRegions = Math.min(100, Math.floor(this.maxContentLength / 100));
|
|
325
|
+
const regions = [];
|
|
326
|
+
for (const indicator of ATTACK_INDICATORS) {
|
|
327
|
+
const regex = new RegExp(indicator.source, indicator.flags + "g");
|
|
328
|
+
let match;
|
|
329
|
+
while ((match = regex.exec(content)) !== null) {
|
|
330
|
+
if (regions.length >= maxRegions) break;
|
|
331
|
+
const start = Math.max(0, match.index - 100);
|
|
332
|
+
const end = Math.min(content.length, match.index + match[0].length + 100);
|
|
333
|
+
regions.push([start, end]);
|
|
334
|
+
}
|
|
335
|
+
if (regions.length >= maxRegions) break;
|
|
336
|
+
}
|
|
337
|
+
if (regions.length === 0) return [];
|
|
338
|
+
regions.sort((a, b) => a[0] - b[0]);
|
|
339
|
+
const merged = [regions[0]];
|
|
340
|
+
for (let i = 1; i < regions.length; i++) {
|
|
341
|
+
const [start, end] = regions[i];
|
|
342
|
+
const last = merged[merged.length - 1];
|
|
343
|
+
if (start <= last[1]) {
|
|
344
|
+
last[1] = Math.max(last[1], end);
|
|
345
|
+
} else {
|
|
346
|
+
merged.push([start, end]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return merged.slice(0, maxRegions);
|
|
350
|
+
}
|
|
351
|
+
truncateSafely(content) {
|
|
352
|
+
if (content.length <= this.maxContentLength) return content;
|
|
353
|
+
if (!this.preserveAttackPatterns) return content.slice(0, this.maxContentLength);
|
|
354
|
+
const attackRegions = this.extractAttackRegions(content);
|
|
355
|
+
if (attackRegions.length === 0) return content.slice(0, this.maxContentLength);
|
|
356
|
+
const attackLength = attackRegions.reduce((sum, [s, e]) => sum + (e - s), 0);
|
|
357
|
+
if (attackLength >= this.maxContentLength) {
|
|
358
|
+
let result = "";
|
|
359
|
+
let remaining2 = this.maxContentLength;
|
|
360
|
+
for (const [start, end] of attackRegions) {
|
|
361
|
+
const chunkLen = Math.min(end - start, remaining2);
|
|
362
|
+
result += content.slice(start, start + chunkLen);
|
|
363
|
+
remaining2 -= chunkLen;
|
|
364
|
+
if (remaining2 <= 0) break;
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
const parts = [];
|
|
369
|
+
for (const [start, end] of attackRegions) {
|
|
370
|
+
parts.push(content.slice(start, end));
|
|
371
|
+
}
|
|
372
|
+
let remaining = this.maxContentLength - attackLength;
|
|
373
|
+
let lastEnd = 0;
|
|
374
|
+
const contextParts = [];
|
|
375
|
+
for (const [start, end] of attackRegions) {
|
|
376
|
+
if (lastEnd < start && remaining > 0) {
|
|
377
|
+
const chunkLen = Math.min(start - lastEnd, remaining);
|
|
378
|
+
contextParts.push(content.slice(lastEnd, lastEnd + chunkLen));
|
|
379
|
+
remaining -= chunkLen;
|
|
380
|
+
}
|
|
381
|
+
lastEnd = end;
|
|
382
|
+
}
|
|
383
|
+
return [...contextParts, ...parts].join("");
|
|
384
|
+
}
|
|
385
|
+
async preprocess(content) {
|
|
386
|
+
if (!content) return "";
|
|
387
|
+
let result = this.normalizeUnicode(content);
|
|
388
|
+
result = this.decodeCommonEncodings(result);
|
|
389
|
+
result = this.removeNullBytes(result);
|
|
390
|
+
result = this.removeExcessiveWhitespace(result);
|
|
391
|
+
result = this.truncateSafely(result);
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
async preprocessBatch(contents) {
|
|
395
|
+
return Promise.all(contents.map((c) => this.preprocess(c)));
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// src/detection-engine/monitor.ts
|
|
402
|
+
function mean(values) {
|
|
403
|
+
if (values.length === 0) return 0;
|
|
404
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
405
|
+
}
|
|
406
|
+
function stdev(values) {
|
|
407
|
+
if (values.length <= 1) return 0;
|
|
408
|
+
const avg = mean(values);
|
|
409
|
+
const squareDiffs = values.map((v) => (v - avg) ** 2);
|
|
410
|
+
return Math.sqrt(squareDiffs.reduce((a, b) => a + b, 0) / (values.length - 1));
|
|
411
|
+
}
|
|
412
|
+
function truncatePattern(pattern) {
|
|
413
|
+
return pattern.length > 50 ? pattern.slice(0, 50) + "..." : pattern;
|
|
414
|
+
}
|
|
415
|
+
function patternHash(pattern) {
|
|
416
|
+
let hash = 0;
|
|
417
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
418
|
+
hash = (hash << 5) - hash + pattern.charCodeAt(i);
|
|
419
|
+
hash |= 0;
|
|
420
|
+
}
|
|
421
|
+
return String(hash).slice(0, 8);
|
|
422
|
+
}
|
|
423
|
+
var MAX_RECENT_TIMES, MAX_PATTERN_LENGTH, MIN_SAMPLES_FOR_STATS, PerformanceMonitor;
|
|
424
|
+
var init_monitor = __esm({
|
|
425
|
+
"src/detection-engine/monitor.ts"() {
|
|
426
|
+
"use strict";
|
|
427
|
+
MAX_RECENT_TIMES = 100;
|
|
428
|
+
MAX_PATTERN_LENGTH = 100;
|
|
429
|
+
MIN_SAMPLES_FOR_STATS = 10;
|
|
430
|
+
PerformanceMonitor = class {
|
|
431
|
+
anomalyThreshold;
|
|
432
|
+
slowPatternThreshold;
|
|
433
|
+
historySize;
|
|
434
|
+
maxTrackedPatterns;
|
|
435
|
+
patternStats = /* @__PURE__ */ new Map();
|
|
436
|
+
recentMetrics = [];
|
|
437
|
+
anomalyCallbacks = [];
|
|
438
|
+
constructor(anomalyThreshold = 3, slowPatternThreshold = 0.1, historySize = 1e3, maxTrackedPatterns = 1e3) {
|
|
439
|
+
this.anomalyThreshold = Math.max(1, Math.min(10, anomalyThreshold));
|
|
440
|
+
this.slowPatternThreshold = Math.max(0.01, Math.min(10, slowPatternThreshold));
|
|
441
|
+
this.historySize = Math.max(100, Math.min(1e4, historySize));
|
|
442
|
+
this.maxTrackedPatterns = Math.max(100, Math.min(5e3, maxTrackedPatterns));
|
|
443
|
+
}
|
|
444
|
+
async recordMetric(pattern, executionTime, contentLength, matched, timeout = false, agentHandler = null, correlationId = null) {
|
|
445
|
+
let truncatedPattern = pattern;
|
|
446
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
447
|
+
truncatedPattern = pattern.slice(0, MAX_PATTERN_LENGTH) + "...[truncated]";
|
|
448
|
+
}
|
|
449
|
+
executionTime = Math.max(0, executionTime);
|
|
450
|
+
contentLength = Math.max(0, contentLength);
|
|
451
|
+
const metric = {
|
|
452
|
+
pattern: truncatedPattern,
|
|
453
|
+
executionTime,
|
|
454
|
+
contentLength,
|
|
455
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
456
|
+
matched,
|
|
457
|
+
timeout
|
|
458
|
+
};
|
|
459
|
+
this.recentMetrics.push(metric);
|
|
460
|
+
if (this.recentMetrics.length > this.historySize) {
|
|
461
|
+
this.recentMetrics.shift();
|
|
462
|
+
}
|
|
463
|
+
if (!this.patternStats.has(truncatedPattern)) {
|
|
464
|
+
if (this.patternStats.size >= this.maxTrackedPatterns) {
|
|
465
|
+
const oldestKey = this.patternStats.keys().next().value;
|
|
466
|
+
this.patternStats.delete(oldestKey);
|
|
467
|
+
}
|
|
468
|
+
this.patternStats.set(truncatedPattern, {
|
|
469
|
+
pattern: truncatedPattern,
|
|
470
|
+
totalExecutions: 0,
|
|
471
|
+
totalMatches: 0,
|
|
472
|
+
totalTimeouts: 0,
|
|
473
|
+
avgExecutionTime: 0,
|
|
474
|
+
maxExecutionTime: 0,
|
|
475
|
+
minExecutionTime: Infinity,
|
|
476
|
+
recentTimes: []
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const stats = this.patternStats.get(truncatedPattern);
|
|
480
|
+
stats.totalExecutions++;
|
|
481
|
+
if (matched) stats.totalMatches++;
|
|
482
|
+
if (timeout) stats.totalTimeouts++;
|
|
483
|
+
if (!timeout) {
|
|
484
|
+
stats.recentTimes.push(executionTime);
|
|
485
|
+
if (stats.recentTimes.length > MAX_RECENT_TIMES) {
|
|
486
|
+
stats.recentTimes.shift();
|
|
487
|
+
}
|
|
488
|
+
stats.maxExecutionTime = Math.max(stats.maxExecutionTime, executionTime);
|
|
489
|
+
stats.minExecutionTime = Math.min(stats.minExecutionTime, executionTime);
|
|
490
|
+
if (stats.recentTimes.length > 0) {
|
|
491
|
+
stats.avgExecutionTime = mean(stats.recentTimes);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
await this.checkAnomalies(metric, agentHandler, correlationId);
|
|
495
|
+
}
|
|
496
|
+
async checkAnomalies(metric, agentHandler, correlationId) {
|
|
497
|
+
const anomalies = [];
|
|
498
|
+
if (metric.timeout) {
|
|
499
|
+
anomalies.push({
|
|
500
|
+
type: "timeout",
|
|
501
|
+
pattern: metric.pattern,
|
|
502
|
+
contentLength: metric.contentLength
|
|
503
|
+
});
|
|
504
|
+
} else if (metric.executionTime > this.slowPatternThreshold) {
|
|
505
|
+
anomalies.push({
|
|
506
|
+
type: "slow_execution",
|
|
507
|
+
pattern: metric.pattern,
|
|
508
|
+
executionTime: metric.executionTime,
|
|
509
|
+
contentLength: metric.contentLength
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const stats = this.patternStats.get(metric.pattern);
|
|
513
|
+
if (stats && stats.recentTimes.length >= MIN_SAMPLES_FOR_STATS) {
|
|
514
|
+
const avgTime = mean(stats.recentTimes);
|
|
515
|
+
const stdTime = stdev(stats.recentTimes);
|
|
516
|
+
if (stdTime > 0) {
|
|
517
|
+
const zScore = (metric.executionTime - avgTime) / stdTime;
|
|
518
|
+
if (Math.abs(zScore) > this.anomalyThreshold) {
|
|
519
|
+
anomalies.push({
|
|
520
|
+
type: "statistical_anomaly",
|
|
521
|
+
pattern: metric.pattern,
|
|
522
|
+
executionTime: metric.executionTime,
|
|
523
|
+
zScore,
|
|
524
|
+
avgTime,
|
|
525
|
+
stdTime
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (agentHandler) {
|
|
531
|
+
for (const anomaly of anomalies) {
|
|
532
|
+
try {
|
|
533
|
+
await agentHandler.sendEvent({
|
|
534
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
535
|
+
eventType: `pattern_anomaly_${anomaly["type"]}`,
|
|
536
|
+
ipAddress: "system",
|
|
537
|
+
actionTaken: "anomaly_detected",
|
|
538
|
+
reason: `Pattern performance anomaly: ${anomaly["type"]}`,
|
|
539
|
+
metadata: { component: "PerformanceMonitor", correlationId, ...anomaly }
|
|
540
|
+
});
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const anomaly of anomalies) {
|
|
546
|
+
const safe = { ...anomaly };
|
|
547
|
+
if (typeof safe["pattern"] === "string") {
|
|
548
|
+
safe["pattern"] = truncatePattern(safe["pattern"]);
|
|
549
|
+
safe["patternHash"] = patternHash(anomaly["pattern"]);
|
|
550
|
+
}
|
|
551
|
+
for (const callback of this.anomalyCallbacks) {
|
|
552
|
+
try {
|
|
553
|
+
callback(safe);
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
getPatternReport(pattern) {
|
|
560
|
+
let key = pattern;
|
|
561
|
+
if (key.length > MAX_PATTERN_LENGTH) {
|
|
562
|
+
key = key.slice(0, MAX_PATTERN_LENGTH) + "...[truncated]";
|
|
563
|
+
}
|
|
564
|
+
const stats = this.patternStats.get(key);
|
|
565
|
+
if (!stats) return null;
|
|
566
|
+
return {
|
|
567
|
+
pattern: truncatePattern(key),
|
|
568
|
+
patternHash: patternHash(key),
|
|
569
|
+
totalExecutions: stats.totalExecutions,
|
|
570
|
+
totalMatches: stats.totalMatches,
|
|
571
|
+
totalTimeouts: stats.totalTimeouts,
|
|
572
|
+
matchRate: stats.totalMatches / Math.max(stats.totalExecutions, 1),
|
|
573
|
+
timeoutRate: stats.totalTimeouts / Math.max(stats.totalExecutions, 1),
|
|
574
|
+
avgExecutionTime: Math.round(stats.avgExecutionTime * 1e4) / 1e4,
|
|
575
|
+
maxExecutionTime: Math.round(stats.maxExecutionTime * 1e4) / 1e4,
|
|
576
|
+
minExecutionTime: Math.round(
|
|
577
|
+
(stats.minExecutionTime === Infinity ? 0 : stats.minExecutionTime) * 1e4
|
|
578
|
+
) / 1e4
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
getSlowPatterns(limit = 10) {
|
|
582
|
+
const entries = [...this.patternStats.entries()].filter(([, s]) => s.recentTimes.length > 0).sort(([, a], [, b]) => b.avgExecutionTime - a.avgExecutionTime).slice(0, limit);
|
|
583
|
+
const reports = [];
|
|
584
|
+
for (const [pattern] of entries) {
|
|
585
|
+
const report = this.getPatternReport(pattern);
|
|
586
|
+
if (report) reports.push(report);
|
|
587
|
+
}
|
|
588
|
+
return reports;
|
|
589
|
+
}
|
|
590
|
+
getProblematicPatterns() {
|
|
591
|
+
const problematic = [];
|
|
592
|
+
for (const [pattern, stats] of this.patternStats) {
|
|
593
|
+
if (stats.totalExecutions === 0) continue;
|
|
594
|
+
const timeoutRate = stats.totalTimeouts / stats.totalExecutions;
|
|
595
|
+
if (timeoutRate > 0.1) {
|
|
596
|
+
const report = this.getPatternReport(pattern);
|
|
597
|
+
if (report) {
|
|
598
|
+
report.issue = "high_timeout_rate";
|
|
599
|
+
problematic.push(report);
|
|
600
|
+
}
|
|
601
|
+
} else if (stats.avgExecutionTime > this.slowPatternThreshold) {
|
|
602
|
+
const report = this.getPatternReport(pattern);
|
|
603
|
+
if (report) {
|
|
604
|
+
report.issue = "consistently_slow";
|
|
605
|
+
problematic.push(report);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return problematic;
|
|
610
|
+
}
|
|
611
|
+
getSummaryStats() {
|
|
612
|
+
if (this.recentMetrics.length === 0) {
|
|
613
|
+
return { totalExecutions: 0, avgExecutionTime: 0, timeoutRate: 0, matchRate: 0 };
|
|
614
|
+
}
|
|
615
|
+
const recentTimes = this.recentMetrics.filter((m) => !m.timeout).map((m) => m.executionTime);
|
|
616
|
+
const timeouts = this.recentMetrics.filter((m) => m.timeout).length;
|
|
617
|
+
const matches = this.recentMetrics.filter((m) => m.matched).length;
|
|
618
|
+
const total = this.recentMetrics.length;
|
|
619
|
+
return {
|
|
620
|
+
totalExecutions: total,
|
|
621
|
+
avgExecutionTime: recentTimes.length > 0 ? mean(recentTimes) : 0,
|
|
622
|
+
maxExecutionTime: recentTimes.length > 0 ? Math.max(...recentTimes) : 0,
|
|
623
|
+
minExecutionTime: recentTimes.length > 0 ? Math.min(...recentTimes) : 0,
|
|
624
|
+
timeoutRate: timeouts / total,
|
|
625
|
+
matchRate: matches / total,
|
|
626
|
+
totalPatterns: this.patternStats.size
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
registerAnomalyCallback(callback) {
|
|
630
|
+
this.anomalyCallbacks.push(callback);
|
|
631
|
+
}
|
|
632
|
+
async clearStats() {
|
|
633
|
+
this.patternStats.clear();
|
|
634
|
+
this.recentMetrics = [];
|
|
635
|
+
}
|
|
636
|
+
async removePatternStats(pattern) {
|
|
637
|
+
this.patternStats.delete(pattern);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// src/detection-engine/semantic.ts
|
|
644
|
+
var ATTACK_KEYWORDS, ATTACK_STRUCTURES, PATTERN_CHECKS, INJECTION_KEYWORDS, MAX_CONTENT_LENGTH, MAX_TOKENS, MAX_ENTROPY_LENGTH, MAX_SCAN_LENGTH, MAX_AST_LENGTH, SemanticAnalyzer;
|
|
645
|
+
var init_semantic = __esm({
|
|
646
|
+
"src/detection-engine/semantic.ts"() {
|
|
647
|
+
"use strict";
|
|
648
|
+
ATTACK_KEYWORDS = {
|
|
649
|
+
xss: /* @__PURE__ */ new Set([
|
|
650
|
+
"script",
|
|
651
|
+
"javascript",
|
|
652
|
+
"onerror",
|
|
653
|
+
"onload",
|
|
654
|
+
"onclick",
|
|
655
|
+
"onmouseover",
|
|
656
|
+
"alert",
|
|
657
|
+
"eval",
|
|
658
|
+
"document",
|
|
659
|
+
"cookie",
|
|
660
|
+
"window",
|
|
661
|
+
"location"
|
|
662
|
+
]),
|
|
663
|
+
sql: /* @__PURE__ */ new Set([
|
|
664
|
+
"select",
|
|
665
|
+
"union",
|
|
666
|
+
"insert",
|
|
667
|
+
"update",
|
|
668
|
+
"delete",
|
|
669
|
+
"drop",
|
|
670
|
+
"from",
|
|
671
|
+
"where",
|
|
672
|
+
"order",
|
|
673
|
+
"group",
|
|
674
|
+
"having",
|
|
675
|
+
"concat",
|
|
676
|
+
"substring",
|
|
677
|
+
"database",
|
|
678
|
+
"table",
|
|
679
|
+
"column"
|
|
680
|
+
]),
|
|
681
|
+
command: /* @__PURE__ */ new Set([
|
|
682
|
+
"exec",
|
|
683
|
+
"system",
|
|
684
|
+
"shell",
|
|
685
|
+
"cmd",
|
|
686
|
+
"bash",
|
|
687
|
+
"powershell",
|
|
688
|
+
"wget",
|
|
689
|
+
"curl",
|
|
690
|
+
"nc",
|
|
691
|
+
"netcat",
|
|
692
|
+
"chmod",
|
|
693
|
+
"chown",
|
|
694
|
+
"sudo",
|
|
695
|
+
"passwd"
|
|
696
|
+
]),
|
|
697
|
+
path: /* @__PURE__ */ new Set(["etc", "passwd", "shadow", "hosts", "proc", "boot", "win", "ini"]),
|
|
698
|
+
template: /* @__PURE__ */ new Set([
|
|
699
|
+
"render",
|
|
700
|
+
"template",
|
|
701
|
+
"jinja",
|
|
702
|
+
"mustache",
|
|
703
|
+
"handlebars",
|
|
704
|
+
"ejs",
|
|
705
|
+
"pug",
|
|
706
|
+
"twig"
|
|
707
|
+
])
|
|
708
|
+
};
|
|
709
|
+
ATTACK_STRUCTURES = {
|
|
710
|
+
tag_like: /<[^>]+>/gi,
|
|
711
|
+
function_call: /\w+\s*\([^)]*\)/gi,
|
|
712
|
+
command_chain: /[;&|]{1,2}/g,
|
|
713
|
+
path_traversal: /\.{2,}[/\\]/g,
|
|
714
|
+
url_pattern: /[a-z]+:\/\//gi
|
|
715
|
+
};
|
|
716
|
+
PATTERN_CHECKS = {
|
|
717
|
+
xss: [/<[^>]+>/g, ""],
|
|
718
|
+
sql: [/\b(?:union|select|from|where)\b/gi, ""],
|
|
719
|
+
command: [/[;&|]/g, ""],
|
|
720
|
+
path: [/\.{2,}[/\\]/g, ""]
|
|
721
|
+
};
|
|
722
|
+
INJECTION_KEYWORDS = ["eval", "exec", "compile", "__import__", "globals", "locals"];
|
|
723
|
+
MAX_CONTENT_LENGTH = 5e4;
|
|
724
|
+
MAX_TOKENS = 1e3;
|
|
725
|
+
MAX_ENTROPY_LENGTH = 1e4;
|
|
726
|
+
MAX_SCAN_LENGTH = 1e4;
|
|
727
|
+
MAX_AST_LENGTH = 1e3;
|
|
728
|
+
SemanticAnalyzer = class {
|
|
729
|
+
extractTokens(content) {
|
|
730
|
+
let truncated = content;
|
|
731
|
+
if (truncated.length > MAX_CONTENT_LENGTH) {
|
|
732
|
+
truncated = truncated.slice(0, MAX_CONTENT_LENGTH);
|
|
733
|
+
}
|
|
734
|
+
truncated = truncated.replace(/\s+/g, " ");
|
|
735
|
+
const wordTokens = (truncated.toLowerCase().match(/\b\w+\b/g) ?? []).slice(0, MAX_TOKENS);
|
|
736
|
+
const specialPatterns = [];
|
|
737
|
+
for (const [, regex] of Object.entries(ATTACK_STRUCTURES)) {
|
|
738
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
739
|
+
let match;
|
|
740
|
+
while ((match = re.exec(truncated)) !== null && specialPatterns.length < 50) {
|
|
741
|
+
specialPatterns.push(match[0]);
|
|
742
|
+
}
|
|
743
|
+
if (specialPatterns.length >= 50) break;
|
|
744
|
+
}
|
|
745
|
+
return [...wordTokens, ...specialPatterns].slice(0, MAX_TOKENS);
|
|
746
|
+
}
|
|
747
|
+
calculateEntropy(content) {
|
|
748
|
+
if (!content) return 0;
|
|
749
|
+
const truncated = content.length > MAX_ENTROPY_LENGTH ? content.slice(0, MAX_ENTROPY_LENGTH) : content;
|
|
750
|
+
const counts = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const char of truncated) {
|
|
752
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
753
|
+
}
|
|
754
|
+
const length = truncated.length;
|
|
755
|
+
let entropy = 0;
|
|
756
|
+
for (const count of counts.values()) {
|
|
757
|
+
const probability = count / length;
|
|
758
|
+
if (probability > 0) {
|
|
759
|
+
entropy -= probability * Math.log2(probability);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return entropy;
|
|
763
|
+
}
|
|
764
|
+
detectEncodingLayers(content) {
|
|
765
|
+
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
766
|
+
let layers = 0;
|
|
767
|
+
if (/%[0-9a-fA-F]{2}/.test(truncated)) layers++;
|
|
768
|
+
if (/[A-Za-z0-9+/]{4,}={0,2}/.test(truncated)) layers++;
|
|
769
|
+
if (/(?:0x)?[0-9a-fA-F]{4,}/.test(truncated)) layers++;
|
|
770
|
+
if (/\\u[0-9a-fA-F]{4}/.test(truncated)) layers++;
|
|
771
|
+
if (/&[#\w]+;/.test(truncated)) layers++;
|
|
772
|
+
return layers;
|
|
773
|
+
}
|
|
774
|
+
analyzeAttackProbability(content) {
|
|
775
|
+
const tokens = this.extractTokens(content);
|
|
776
|
+
const tokenSet = new Set(tokens);
|
|
777
|
+
const probabilities = {};
|
|
778
|
+
for (const [attackType, keywords] of Object.entries(ATTACK_KEYWORDS)) {
|
|
779
|
+
let matches = 0;
|
|
780
|
+
for (const token of tokenSet) {
|
|
781
|
+
if (keywords.has(token)) matches++;
|
|
782
|
+
}
|
|
783
|
+
const baseScore = keywords.size > 0 ? matches / keywords.size : 0;
|
|
784
|
+
let patternBoost = 0;
|
|
785
|
+
const check = PATTERN_CHECKS[attackType];
|
|
786
|
+
if (check) {
|
|
787
|
+
const [re] = check;
|
|
788
|
+
if (new RegExp(re.source, re.flags).test(content)) {
|
|
789
|
+
patternBoost = 0.3;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
probabilities[attackType] = Math.min(baseScore + patternBoost, 1);
|
|
793
|
+
}
|
|
794
|
+
return probabilities;
|
|
795
|
+
}
|
|
796
|
+
detectObfuscation(content) {
|
|
797
|
+
if (this.calculateEntropy(content) > 4.5) return true;
|
|
798
|
+
if (this.detectEncodingLayers(content) > 2) return true;
|
|
799
|
+
const specialChars = (content.match(/[^a-zA-Z0-9\s]/g) ?? []).length;
|
|
800
|
+
if (specialChars / Math.max(content.length, 1) > 0.4) return true;
|
|
801
|
+
if (/\S{100,}/.test(content)) return true;
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
extractSuspiciousPatterns(content) {
|
|
805
|
+
const patterns = [];
|
|
806
|
+
for (const [name, regex] of Object.entries(ATTACK_STRUCTURES)) {
|
|
807
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
808
|
+
let match;
|
|
809
|
+
while ((match = re.exec(content)) !== null) {
|
|
810
|
+
const contextStart = Math.max(0, match.index - 20);
|
|
811
|
+
const contextEnd = Math.min(content.length, match.index + match[0].length + 20);
|
|
812
|
+
patterns.push({
|
|
813
|
+
type: name,
|
|
814
|
+
pattern: match[0],
|
|
815
|
+
position: match.index,
|
|
816
|
+
context: content.slice(contextStart, contextEnd)
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return patterns;
|
|
821
|
+
}
|
|
822
|
+
analyzeCodeInjectionRisk(content) {
|
|
823
|
+
let riskScore = 0;
|
|
824
|
+
if (/[{}].*[{}]/.test(content)) riskScore += 0.2;
|
|
825
|
+
if (/\w+\s*\([^)]*\)/.test(content)) riskScore += 0.2;
|
|
826
|
+
if (/[$@]\w+/.test(content)) riskScore += 0.1;
|
|
827
|
+
if (/[=+\-*/]{2,}/.test(content)) riskScore += 0.1;
|
|
828
|
+
if (content.length <= MAX_AST_LENGTH) {
|
|
829
|
+
try {
|
|
830
|
+
const acorn = require("acorn");
|
|
831
|
+
acorn.parse(content, { ecmaVersion: "latest", sourceType: "module" });
|
|
832
|
+
riskScore += 0.3;
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
for (const keyword of INJECTION_KEYWORDS) {
|
|
837
|
+
if (new RegExp(`\\b${keyword}\\b`, "i").test(content)) {
|
|
838
|
+
riskScore += 0.2;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return Math.min(riskScore, 1);
|
|
843
|
+
}
|
|
844
|
+
analyze(content) {
|
|
845
|
+
return {
|
|
846
|
+
attackProbabilities: this.analyzeAttackProbability(content),
|
|
847
|
+
entropy: this.calculateEntropy(content),
|
|
848
|
+
encodingLayers: this.detectEncodingLayers(content),
|
|
849
|
+
isObfuscated: this.detectObfuscation(content),
|
|
850
|
+
suspiciousPatterns: this.extractSuspiciousPatterns(content),
|
|
851
|
+
codeInjectionRisk: this.analyzeCodeInjectionRisk(content),
|
|
852
|
+
tokenCount: this.extractTokens(content).length
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
getThreatScore(analysis) {
|
|
856
|
+
let score = 0;
|
|
857
|
+
const probs = analysis.attackProbabilities;
|
|
858
|
+
const maxProb = Object.values(probs).length > 0 ? Math.max(...Object.values(probs)) : 0;
|
|
859
|
+
score += maxProb * 0.3;
|
|
860
|
+
if (analysis.isObfuscated) score += 0.2;
|
|
861
|
+
if (analysis.encodingLayers > 0) {
|
|
862
|
+
score += Math.min(analysis.encodingLayers * 0.1, 0.2);
|
|
863
|
+
}
|
|
864
|
+
score += analysis.codeInjectionRisk * 0.2;
|
|
865
|
+
if (analysis.suspiciousPatterns.length > 0) {
|
|
866
|
+
score += Math.min(analysis.suspiciousPatterns.length * 0.05, 0.1);
|
|
867
|
+
}
|
|
868
|
+
return Math.min(score, 1);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// src/handlers/cloud.ts
|
|
875
|
+
var cloud_exports = {};
|
|
876
|
+
__export(cloud_exports, {
|
|
877
|
+
CloudHandler: () => CloudHandler
|
|
878
|
+
});
|
|
879
|
+
var ipaddr2, AWS_RANGES_URL, GCP_RANGES_URL, AZURE_DOWNLOAD_PAGE, AZURE_JSON_HREF_RE, CloudHandler;
|
|
880
|
+
var init_cloud = __esm({
|
|
881
|
+
"src/handlers/cloud.ts"() {
|
|
882
|
+
"use strict";
|
|
883
|
+
ipaddr2 = __toESM(require("ipaddr.js"), 1);
|
|
884
|
+
AWS_RANGES_URL = "https://ip-ranges.amazonaws.com/ip-ranges.json";
|
|
885
|
+
GCP_RANGES_URL = "https://www.gstatic.com/ipranges/cloud.json";
|
|
886
|
+
AZURE_DOWNLOAD_PAGE = "https://www.microsoft.com/en-us/download/details.aspx?id=56519";
|
|
887
|
+
AZURE_JSON_HREF_RE = /href=["'](https:\/\/download\.microsoft\.com\/.{1,500}?\.json)["']/;
|
|
888
|
+
CloudHandler = class {
|
|
889
|
+
constructor(logger) {
|
|
890
|
+
this.logger = logger;
|
|
891
|
+
}
|
|
892
|
+
ipRanges = /* @__PURE__ */ new Map();
|
|
893
|
+
lastUpdated = /* @__PURE__ */ new Map();
|
|
894
|
+
redisHandler = null;
|
|
895
|
+
agentHandler = null;
|
|
896
|
+
async initializeRedis(redisHandler, providers, ttl = 3600) {
|
|
897
|
+
this.redisHandler = redisHandler;
|
|
898
|
+
await this.refreshAsync(providers, ttl);
|
|
899
|
+
}
|
|
900
|
+
async initializeAgent(agentHandler) {
|
|
901
|
+
this.agentHandler = agentHandler;
|
|
902
|
+
}
|
|
903
|
+
async refreshAsync(providers, ttl = 3600) {
|
|
904
|
+
for (const provider of providers) {
|
|
905
|
+
try {
|
|
906
|
+
const ranges = await this.fetchProviderRanges(provider);
|
|
907
|
+
this.ipRanges.set(provider, ranges);
|
|
908
|
+
this.lastUpdated.set(provider, /* @__PURE__ */ new Date());
|
|
909
|
+
if (this.redisHandler) {
|
|
910
|
+
await this.redisHandler.setKey("cloud_ranges", provider, ranges.join(","), ttl);
|
|
911
|
+
}
|
|
912
|
+
this.logger.info(`Refreshed ${ranges.length} IP ranges for ${provider}`);
|
|
913
|
+
} catch (e) {
|
|
914
|
+
this.logger.error(`Failed to refresh ${provider} IP ranges: ${e}`);
|
|
915
|
+
if (this.redisHandler) {
|
|
916
|
+
const cached = await this.redisHandler.getKey("cloud_ranges", provider);
|
|
917
|
+
if (typeof cached === "string" && cached.length > 0) {
|
|
918
|
+
this.ipRanges.set(provider, cached.split(","));
|
|
919
|
+
this.logger.info(`Loaded ${provider} IP ranges from Redis cache`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async fetchProviderRanges(provider) {
|
|
926
|
+
switch (provider) {
|
|
927
|
+
case "AWS":
|
|
928
|
+
return this.fetchAwsRanges();
|
|
929
|
+
case "GCP":
|
|
930
|
+
return this.fetchGcpRanges();
|
|
931
|
+
case "Azure":
|
|
932
|
+
return this.fetchAzureRanges();
|
|
933
|
+
default:
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
async fetchAwsRanges() {
|
|
938
|
+
const resp = await fetch(AWS_RANGES_URL);
|
|
939
|
+
const data = await resp.json();
|
|
940
|
+
return data.prefixes.filter((p) => p.service === "AMAZON").map((p) => p.ip_prefix);
|
|
941
|
+
}
|
|
942
|
+
async fetchGcpRanges() {
|
|
943
|
+
const resp = await fetch(GCP_RANGES_URL);
|
|
944
|
+
const data = await resp.json();
|
|
945
|
+
return data.prefixes.map((p) => p.ipv4Prefix ?? p.ipv6Prefix).filter((p) => p !== void 0);
|
|
946
|
+
}
|
|
947
|
+
async fetchAzureRanges() {
|
|
948
|
+
const pageResp = await fetch(AZURE_DOWNLOAD_PAGE);
|
|
949
|
+
const html = await pageResp.text();
|
|
950
|
+
const match = AZURE_JSON_HREF_RE.exec(html);
|
|
951
|
+
if (!match) {
|
|
952
|
+
this.logger.warn("Could not find Azure IP ranges download URL");
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
const jsonResp = await fetch(match[1]);
|
|
956
|
+
const data = await jsonResp.json();
|
|
957
|
+
return data.values.flatMap((v) => v.properties.addressPrefixes);
|
|
958
|
+
}
|
|
959
|
+
isCloudIp(ip, providers) {
|
|
960
|
+
try {
|
|
961
|
+
const parsed = ipaddr2.parse(ip);
|
|
962
|
+
for (const provider of providers) {
|
|
963
|
+
const ranges = this.ipRanges.get(provider);
|
|
964
|
+
if (!ranges) continue;
|
|
965
|
+
for (const cidr of ranges) {
|
|
966
|
+
try {
|
|
967
|
+
const [addr, prefixLen] = ipaddr2.parseCIDR(cidr);
|
|
968
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) {
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
getCloudProviderDetails(ip, providers) {
|
|
981
|
+
try {
|
|
982
|
+
const parsed = ipaddr2.parse(ip);
|
|
983
|
+
for (const provider of providers) {
|
|
984
|
+
const ranges = this.ipRanges.get(provider);
|
|
985
|
+
if (!ranges) continue;
|
|
986
|
+
for (const cidr of ranges) {
|
|
987
|
+
try {
|
|
988
|
+
const [addr, prefixLen] = ipaddr2.parseCIDR(cidr);
|
|
989
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) {
|
|
990
|
+
return [provider, cidr];
|
|
991
|
+
}
|
|
992
|
+
} catch {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
async reset() {
|
|
1002
|
+
this.ipRanges.clear();
|
|
1003
|
+
this.lastUpdated.clear();
|
|
1004
|
+
if (this.redisHandler) {
|
|
1005
|
+
await this.redisHandler.deletePattern("cloud_ranges:*");
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// src/handlers/sus-patterns.ts
|
|
1013
|
+
var sus_patterns_exports = {};
|
|
1014
|
+
__export(sus_patterns_exports, {
|
|
1015
|
+
SusPatternsManager: () => SusPatternsManager
|
|
1016
|
+
});
|
|
1017
|
+
var CTX_XSS, CTX_SQLI, CTX_DIR_TRAVERSAL, CTX_CMD_INJECTION, CTX_FILE_INCLUSION, CTX_LDAP, CTX_XML, CTX_SSRF, CTX_NOSQL, CTX_FILE_UPLOAD, CTX_PATH_TRAVERSAL, CTX_TEMPLATE, CTX_HTTP_SPLIT, CTX_SENSITIVE_FILE, CTX_CMS_PROBING, CTX_RECON, CTX_ALL, KNOWN_CONTEXTS, PATTERN_DEFINITIONS, SusPatternsManager;
|
|
1018
|
+
var init_sus_patterns = __esm({
|
|
1019
|
+
"src/handlers/sus-patterns.ts"() {
|
|
1020
|
+
"use strict";
|
|
1021
|
+
init_preprocessor();
|
|
1022
|
+
init_compiler();
|
|
1023
|
+
init_monitor();
|
|
1024
|
+
init_semantic();
|
|
1025
|
+
CTX_XSS = /* @__PURE__ */ new Set(["query_param", "header", "request_body", "unknown"]);
|
|
1026
|
+
CTX_SQLI = /* @__PURE__ */ new Set(["query_param", "request_body", "unknown"]);
|
|
1027
|
+
CTX_DIR_TRAVERSAL = /* @__PURE__ */ new Set(["url_path", "query_param", "request_body", "unknown"]);
|
|
1028
|
+
CTX_CMD_INJECTION = /* @__PURE__ */ new Set(["query_param", "request_body", "unknown"]);
|
|
1029
|
+
CTX_FILE_INCLUSION = /* @__PURE__ */ new Set(["url_path", "query_param", "request_body", "unknown"]);
|
|
1030
|
+
CTX_LDAP = /* @__PURE__ */ new Set(["query_param", "request_body", "unknown"]);
|
|
1031
|
+
CTX_XML = /* @__PURE__ */ new Set(["header", "request_body", "unknown"]);
|
|
1032
|
+
CTX_SSRF = /* @__PURE__ */ new Set(["query_param", "request_body", "unknown"]);
|
|
1033
|
+
CTX_NOSQL = /* @__PURE__ */ new Set(["query_param", "request_body", "unknown"]);
|
|
1034
|
+
CTX_FILE_UPLOAD = /* @__PURE__ */ new Set(["header", "request_body", "unknown"]);
|
|
1035
|
+
CTX_PATH_TRAVERSAL = /* @__PURE__ */ new Set(["url_path", "query_param", "request_body", "unknown"]);
|
|
1036
|
+
CTX_TEMPLATE = /* @__PURE__ */ new Set(["query_param", "request_body", "unknown"]);
|
|
1037
|
+
CTX_HTTP_SPLIT = /* @__PURE__ */ new Set(["header", "query_param", "request_body", "unknown"]);
|
|
1038
|
+
CTX_SENSITIVE_FILE = /* @__PURE__ */ new Set(["url_path", "request_body", "unknown"]);
|
|
1039
|
+
CTX_CMS_PROBING = /* @__PURE__ */ new Set(["url_path", "request_body", "unknown"]);
|
|
1040
|
+
CTX_RECON = /* @__PURE__ */ new Set(["url_path", "unknown"]);
|
|
1041
|
+
CTX_ALL = /* @__PURE__ */ new Set(["query_param", "header", "url_path", "request_body", "unknown"]);
|
|
1042
|
+
KNOWN_CONTEXTS = /* @__PURE__ */ new Set(["query_param", "header", "url_path", "request_body", "unknown"]);
|
|
1043
|
+
PATTERN_DEFINITIONS = [
|
|
1044
|
+
[String.raw`<script[^>]*>[^<]*<\/script\s*>`, CTX_XSS],
|
|
1045
|
+
[String.raw`javascript:\s*[^\s]+`, CTX_XSS],
|
|
1046
|
+
[String.raw`(?:on(?:error|load|click|mouseover|submit|mouse|unload|change|focus|blur|drag))=(?:["'][^"']*["']|[^\s>]+)`, CTX_XSS],
|
|
1047
|
+
[String.raw`(?:<[^>]+\s+(?:href|src|data|action)\s*=[\s"']*(?:javascript|vbscript|data):)`, CTX_XSS],
|
|
1048
|
+
[String.raw`(?:<[^>]+style\s*=[\s"']*[^>"']*(?:expression|behavior|url)\s*\([^)]*\))`, CTX_XSS],
|
|
1049
|
+
[String.raw`(?:<object[^>]*>[\s\S]*<\/object\s*>)`, CTX_XSS],
|
|
1050
|
+
[String.raw`(?:<embed[^>]*>[\s\S]*<\/embed\s*>)`, CTX_XSS],
|
|
1051
|
+
[String.raw`(?:<applet[^>]*>[\s\S]*<\/applet\s*>)`, CTX_XSS],
|
|
1052
|
+
[String.raw`SELECT\s+[\w\s,*]+\s+FROM\s+[\w\s._]+`, CTX_SQLI],
|
|
1053
|
+
[String.raw`UNION\s+(?:ALL\s+)?SELECT`, CTX_SQLI],
|
|
1054
|
+
[String.raw`('\s*(?:OR|AND)\s*[(\s]*'?[\d\w]+\s*(?:=|LIKE|<|>|<=|>=)\s*[(\s]*'?[\d\w]+)`, CTX_SQLI],
|
|
1055
|
+
[String.raw`(UNION\s+(?:ALL\s+)?SELECT\s+(?:NULL[,\s]*)+|\(\s*SELECT\s+(?:@@|VERSION))`, CTX_SQLI],
|
|
1056
|
+
[String.raw`(?:INTO\s+(?:OUTFILE|DUMPFILE)\s+'[^']+')`, CTX_SQLI],
|
|
1057
|
+
[String.raw`(?:LOAD_FILE\s*\([^)]+\))`, CTX_SQLI],
|
|
1058
|
+
[String.raw`(?:BENCHMARK\s*\(\s*\d+\s*,)`, CTX_SQLI],
|
|
1059
|
+
[String.raw`(?:SLEEP\s*\(\s*\d+\s*\))`, CTX_SQLI],
|
|
1060
|
+
[String.raw`(?:\/\*![0-9]*\s*(?:OR|AND|UNION|SELECT|INSERT|DELETE|DROP|CONCAT|CHAR|UPDATE)\b)`, CTX_SQLI],
|
|
1061
|
+
[String.raw`(?:\.\.\/|\.\.\\)(?:\.\.\/|\.\.\\)+`, CTX_DIR_TRAVERSAL],
|
|
1062
|
+
[String.raw`(?:/etc/(?:passwd|shadow|group|hosts|motd|issue|mysql/my.cnf|ssh/ssh_config)$)`, CTX_DIR_TRAVERSAL],
|
|
1063
|
+
[String.raw`(?:boot\.ini|win\.ini|system\.ini|config\.sys)\s*$`, CTX_DIR_TRAVERSAL],
|
|
1064
|
+
[String.raw`(?:\/proc\/self\/environ$)`, CTX_DIR_TRAVERSAL],
|
|
1065
|
+
[String.raw`(?:\/var\/log\/[^/]+$)`, CTX_DIR_TRAVERSAL],
|
|
1066
|
+
[String.raw`;\s*(?:ls|cat|rm|chmod|chown|wget|curl|nc|netcat|ping|telnet)\s+-[a-zA-Z]+\s+`, CTX_CMD_INJECTION],
|
|
1067
|
+
[String.raw`\|\s*(?:wget|curl|fetch|lwp-download|lynx|links|GET)\s+`, CTX_CMD_INJECTION],
|
|
1068
|
+
[String.raw`(?:[;&|` + "`" + String.raw`]\s*(?:\$\([^)]+\)|\$\{[^}]+\}))`, CTX_CMD_INJECTION],
|
|
1069
|
+
[String.raw`(?:^|;)\s*(?:bash|sh|ksh|csh|tsch|zsh|ash)\s+-[a-zA-Z]+`, CTX_CMD_INJECTION],
|
|
1070
|
+
[String.raw`\b(?:eval|system|exec|shell_exec|passthru|popen|proc_open)\s*\(`, CTX_CMD_INJECTION],
|
|
1071
|
+
[String.raw`(?:php|data|zip|rar|file|glob|expect|input|phpinfo|zlib|phar|ssh2|rar|ogg|expect)://[^\s]+`, CTX_FILE_INCLUSION],
|
|
1072
|
+
[String.raw`(?:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:[0-9]+)?(?:\/?)?(?:[a-zA-Z0-9\-.,?'/\\+&%$#_]*)?)`, CTX_FILE_INCLUSION],
|
|
1073
|
+
[String.raw`\(\s*[|&]\s*\(\s*[^)]+=[*]`, CTX_LDAP],
|
|
1074
|
+
[String.raw`(?:\*(?:[\s\d\w]+\s*=|=\s*[\d\w\s]+))`, CTX_LDAP],
|
|
1075
|
+
[String.raw`(?:\(\s*[&|]\s*)`, CTX_LDAP],
|
|
1076
|
+
[String.raw`<!(?:ENTITY|DOCTYPE)[^>]+SYSTEM[^>]+>`, CTX_XML],
|
|
1077
|
+
[String.raw`(?:<!\[CDATA\[.*?\]\]>)`, CTX_XML],
|
|
1078
|
+
[String.raw`(?:<\?xml.*?\?>)`, CTX_XML],
|
|
1079
|
+
[String.raw`(?:^|\s|/)(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::(?:\d*)\]|(?:169\.254|192\.168|10\.|172\.(?:1[6-9]|2[0-9]|3[01]))\.\d+)(?:\s|$|/)`, CTX_SSRF],
|
|
1080
|
+
[String.raw`(?:file|dict|gopher|jar|tftp)://[^\s]+`, CTX_SSRF],
|
|
1081
|
+
[String.raw`\{\s*\$(?:where|gt|lt|ne|eq|regex|in|nin|all|size|exists|type|mod|options):`, CTX_NOSQL],
|
|
1082
|
+
[String.raw`(?:\{\s*\$[a-zA-Z]+\s*:\s*(?:\{|\[))`, CTX_NOSQL],
|
|
1083
|
+
[String.raw`filename=["'].*?\.(?:php\d*|phar|phtml|exe|jsp|asp|aspx|sh|bash|rb|py|pl|cgi|com|bat|cmd|vbs|vbe|js|ws|wsf|msi|hta)["']`, CTX_FILE_UPLOAD],
|
|
1084
|
+
[String.raw`(?:%2e%2e|%252e%252e|%uff0e%uff0e|%c0%ae%c0%ae|%e0%40%ae|%c0%ae%e0%80%ae|%25c0%25ae)/`, CTX_PATH_TRAVERSAL],
|
|
1085
|
+
[String.raw`\{\{\s*[^}]+(?:system|exec|popen|eval|require|include)\s*\}\}`, CTX_TEMPLATE],
|
|
1086
|
+
[String.raw`\{%\s*[^%]+(?:system|exec|popen|eval|require|include)\s*%\}`, CTX_TEMPLATE],
|
|
1087
|
+
[String.raw`[\r\n]\s*(?:HTTP\/[0-9.]+|Location:|Set-Cookie:)`, CTX_HTTP_SPLIT],
|
|
1088
|
+
[String.raw`(?:^|/)\.env(?:\.\w+)?(?:\?|$|/)`, CTX_SENSITIVE_FILE],
|
|
1089
|
+
[String.raw`(?:^|/)[\w-]*config[\w-]*\.(?:env|yml|yaml|json|toml|ini|xml|conf)(?:\?|$)`, CTX_SENSITIVE_FILE],
|
|
1090
|
+
[String.raw`(?:^|/)[\w./-]*\.map(?:\?|$)`, CTX_SENSITIVE_FILE],
|
|
1091
|
+
[String.raw`(?:^|/)[\w./-]*\.(?:ts|tsx|jsx|py|rb|java|go|rs|php|pl|sh|sql)(?:\?|$)`, CTX_SENSITIVE_FILE],
|
|
1092
|
+
[String.raw`(?:^|/)\.(?:git|svn|hg|bzr)(?:/|$)`, CTX_SENSITIVE_FILE],
|
|
1093
|
+
[String.raw`(?:^|/)(?:wp-(?:admin|login|content|includes|config)|administrator|xmlrpc)\.?(?:php)?(?:/|$|\?)`, CTX_CMS_PROBING],
|
|
1094
|
+
[String.raw`(?:^|/)(?:phpinfo|info|test|php_info)\.php(?:\?|$)`, CTX_CMS_PROBING],
|
|
1095
|
+
[String.raw`(?:^|/)[\w./-]*\.(?:bak|backup|old|orig|save|swp|swo|tmp|temp)(?:\?|$)`, CTX_CMS_PROBING],
|
|
1096
|
+
[String.raw`(?:^|/)(?:\.htaccess|\.htpasswd|\.DS_Store|Thumbs\.db|\.npmrc|\.dockerenv|web\.config)(?:\?|$)`, CTX_CMS_PROBING],
|
|
1097
|
+
[String.raw`(?:^|/)[\w./-]*\.(?:asp|aspx|jsp|jsa|jhtml|shtml|cfm|cgi|do|action|lua|inc|woa|nsf|esp|html?|js|css|properties|png|gif|jpg|jpeg|svg|webp|bmp|pl)(?:\?|$)`, CTX_RECON],
|
|
1098
|
+
[String.raw`^/(?:api|rest|v\d+|management|system|version|status|config|config_dump|credentials)(?:/|$|\?)`, CTX_RECON],
|
|
1099
|
+
[String.raw`^/admin(?:istrator)?(?:[./?\-]|$)`, CTX_RECON],
|
|
1100
|
+
[String.raw`^/(?:login|logon|signin)(?:[./?\-]|$|/)`, CTX_RECON],
|
|
1101
|
+
[String.raw`(?:^|/)account/login(?:\?|$|/)`, CTX_RECON],
|
|
1102
|
+
[String.raw`(?:^|/)(?:actuator|server-status|telescope)(?:/|$|\?)`, CTX_RECON],
|
|
1103
|
+
[String.raw`(?:CSCOE|dana-(?:na|cached)|sslvpn|RDWeb|/owa/|/ecp/|global-protect|ssl-vpn/|svpn/|sonicui|/remote/login|myvpn|vpntunnel|versa/login)`, CTX_RECON],
|
|
1104
|
+
[String.raw`(?:^|/)(?:geoserver|confluence|nifi|ScadaBR|pandora_console|centreon|kylin|decisioncenter|evox|MagicInfo|metasys|officescan|helpdesk|ignite)(?:/|$|\?|\.|\-)`, CTX_RECON],
|
|
1105
|
+
[String.raw`(?:^|/)cgi-(?:bin|mod)/`, CTX_RECON],
|
|
1106
|
+
[String.raw`(?:^|/)(?:HNAP1|IPCamDesc\.xml|SDK/webLanguage)(?:\?|$|/)`, CTX_RECON],
|
|
1107
|
+
[String.raw`^/(?:scripts|language|languages|images|css|img)/`, CTX_RECON],
|
|
1108
|
+
[String.raw`(?:^|/)(?:robots\.txt|sitemap\.xml|security\.txt|readme\.txt|README\.md|CHANGELOG|pom\.xml|build\.gradle|appsettings\.json|crossdomain\.xml)(?:\?|$|\.)`, CTX_RECON],
|
|
1109
|
+
[String.raw`(?:^|/)(?:sap|ise|nidp|cslu|rustfs|developmentserver|fog/management|lms/db|json/login_session|sms_mp|plugin/webs_model|wsman|am_bin)(?:/|$|\?)`, CTX_RECON],
|
|
1110
|
+
[String.raw`(?:nmaplowercheck|nice\s+ports|Trinity\.txt)`, CTX_RECON],
|
|
1111
|
+
[String.raw`(?:^|/)\.(?:openclaw|clawdbot)(?:/|$)`, CTX_RECON],
|
|
1112
|
+
[String.raw`^/(?:default|inicio|indice|localstart)(?:\.|/|$|\?)`, CTX_RECON],
|
|
1113
|
+
[String.raw`(?:^|/)(?:\.streamlit|\.gpt-pilot|\.aider|\.cursor|\.windsurf|\.copilot|\.devcontainer)(?:/|$)`, CTX_RECON],
|
|
1114
|
+
[String.raw`(?:^|/)(?:docker-compose|Dockerfile|Makefile|Vagrantfile|Jenkinsfile|Procfile)(?:\.ya?ml)?(?:\?|$)`, CTX_RECON],
|
|
1115
|
+
[String.raw`(?:^|/)[\w./-]*(?:secrets?|credentials?)\.(?:py|json|yml|yaml|toml|txt|env|xml|conf|cfg)(?:\?|$)`, CTX_RECON],
|
|
1116
|
+
[String.raw`(?:^|/)autodiscover/`, CTX_RECON],
|
|
1117
|
+
[String.raw`^/dns-query(?:\?|$)`, CTX_RECON],
|
|
1118
|
+
[String.raw`(?:^|/)\.git/(?:refs|index|HEAD|objects|logs)(?:/|$)`, CTX_RECON]
|
|
1119
|
+
];
|
|
1120
|
+
SusPatternsManager = class {
|
|
1121
|
+
constructor(config, logger) {
|
|
1122
|
+
this.logger = logger;
|
|
1123
|
+
this.compiler = new PatternCompiler(config.detectionCompilerTimeout * 1e3, config.detectionMaxTrackedPatterns);
|
|
1124
|
+
this.preprocessor = new ContentPreprocessor(config.detectionMaxContentLength, config.detectionPreserveAttackPatterns);
|
|
1125
|
+
this.semantic = new SemanticAnalyzer();
|
|
1126
|
+
this.monitor = new PerformanceMonitor(
|
|
1127
|
+
config.detectionAnomalyThreshold,
|
|
1128
|
+
config.detectionSlowPatternThreshold,
|
|
1129
|
+
config.detectionMonitorHistorySize,
|
|
1130
|
+
config.detectionMaxTrackedPatterns
|
|
1131
|
+
);
|
|
1132
|
+
this.semanticThreshold = config.detectionSemanticThreshold;
|
|
1133
|
+
}
|
|
1134
|
+
compiler;
|
|
1135
|
+
preprocessor;
|
|
1136
|
+
semantic;
|
|
1137
|
+
monitor;
|
|
1138
|
+
customPatterns = /* @__PURE__ */ new Set();
|
|
1139
|
+
compiledCustomContexts = /* @__PURE__ */ new Map();
|
|
1140
|
+
redisHandler = null;
|
|
1141
|
+
agentHandler = null;
|
|
1142
|
+
semanticThreshold;
|
|
1143
|
+
async initializeRedis(redisHandler) {
|
|
1144
|
+
this.redisHandler = redisHandler;
|
|
1145
|
+
const cached = await redisHandler.getKey("patterns", "custom");
|
|
1146
|
+
if (typeof cached === "string" && cached.length > 0) {
|
|
1147
|
+
for (const p of cached.split(",")) {
|
|
1148
|
+
if (p.trim()) {
|
|
1149
|
+
this.customPatterns.add(p.trim());
|
|
1150
|
+
this.compiledCustomContexts.set(p.trim(), CTX_ALL);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async initializeAgent(agentHandler) {
|
|
1156
|
+
this.agentHandler = agentHandler;
|
|
1157
|
+
}
|
|
1158
|
+
normalizeContext(context) {
|
|
1159
|
+
const parts = context.split(":");
|
|
1160
|
+
const normalized = parts[0].toLowerCase();
|
|
1161
|
+
return KNOWN_CONTEXTS.has(normalized) ? normalized : "unknown";
|
|
1162
|
+
}
|
|
1163
|
+
async detect(content, ipAddress, context = "unknown", correlationId = null) {
|
|
1164
|
+
const startTime = performance.now();
|
|
1165
|
+
const originalLength = content.length;
|
|
1166
|
+
const processed = await this.preprocessor.preprocess(content);
|
|
1167
|
+
const normalizedCtx = this.normalizeContext(context);
|
|
1168
|
+
const threats = [];
|
|
1169
|
+
const timeouts = [];
|
|
1170
|
+
for (const [pattern, contexts] of PATTERN_DEFINITIONS) {
|
|
1171
|
+
if (!contexts.has(normalizedCtx)) continue;
|
|
1172
|
+
const patternStart = performance.now();
|
|
1173
|
+
try {
|
|
1174
|
+
const match = await this.compiler.safeMatch(pattern, processed);
|
|
1175
|
+
const elapsed = (performance.now() - patternStart) / 1e3;
|
|
1176
|
+
await this.monitor.recordMetric(pattern, elapsed, processed.length, match !== null, false, this.agentHandler, correlationId);
|
|
1177
|
+
if (match) {
|
|
1178
|
+
threats.push({
|
|
1179
|
+
pattern,
|
|
1180
|
+
context: normalizedCtx,
|
|
1181
|
+
matchedContent: String(match[0] ?? "").slice(0, 200),
|
|
1182
|
+
detectionMethod: "regex"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
} catch {
|
|
1186
|
+
timeouts.push(pattern);
|
|
1187
|
+
const elapsed = (performance.now() - patternStart) / 1e3;
|
|
1188
|
+
await this.monitor.recordMetric(pattern, elapsed, processed.length, false, true, this.agentHandler, correlationId);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
for (const customPattern of this.customPatterns) {
|
|
1192
|
+
const ctxSet = this.compiledCustomContexts.get(customPattern) ?? CTX_ALL;
|
|
1193
|
+
if (!ctxSet.has(normalizedCtx)) continue;
|
|
1194
|
+
try {
|
|
1195
|
+
const match = await this.compiler.safeMatch(customPattern, processed);
|
|
1196
|
+
if (match) {
|
|
1197
|
+
threats.push({
|
|
1198
|
+
pattern: customPattern,
|
|
1199
|
+
context: normalizedCtx,
|
|
1200
|
+
matchedContent: String(match[0] ?? "").slice(0, 200),
|
|
1201
|
+
detectionMethod: "regex_custom"
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
timeouts.push(customPattern);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const analysis = this.semantic.analyze(processed);
|
|
1209
|
+
const semanticScore = this.semantic.getThreatScore(analysis);
|
|
1210
|
+
if (semanticScore >= this.semanticThreshold) {
|
|
1211
|
+
const topAttack = Object.entries(analysis.attackProbabilities).sort(([, a], [, b]) => b - a)[0];
|
|
1212
|
+
if (topAttack) {
|
|
1213
|
+
threats.push({
|
|
1214
|
+
pattern: `semantic:${topAttack[0]}`,
|
|
1215
|
+
context: normalizedCtx,
|
|
1216
|
+
matchedContent: `score=${semanticScore.toFixed(3)}`,
|
|
1217
|
+
detectionMethod: "semantic"
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const regexScore = threats.some((t) => t.detectionMethod.startsWith("regex")) ? 1 : 0;
|
|
1222
|
+
const threatScore = Math.max(regexScore, semanticScore);
|
|
1223
|
+
const executionTime = (performance.now() - startTime) / 1e3;
|
|
1224
|
+
return {
|
|
1225
|
+
isThreat: threats.length > 0,
|
|
1226
|
+
threatScore,
|
|
1227
|
+
threats,
|
|
1228
|
+
executionTime,
|
|
1229
|
+
timeouts,
|
|
1230
|
+
correlationId,
|
|
1231
|
+
originalLength,
|
|
1232
|
+
processedLength: processed.length
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
async detectPatternMatch(content, ipAddress, context = "unknown", correlationId = null) {
|
|
1236
|
+
const result = await this.detect(content, ipAddress, context, correlationId);
|
|
1237
|
+
if (result.isThreat && result.threats.length > 0) {
|
|
1238
|
+
return [true, result.threats[0].pattern];
|
|
1239
|
+
}
|
|
1240
|
+
return [false, null];
|
|
1241
|
+
}
|
|
1242
|
+
async addPattern(pattern, custom = true) {
|
|
1243
|
+
this.customPatterns.add(pattern);
|
|
1244
|
+
this.compiledCustomContexts.set(pattern, CTX_ALL);
|
|
1245
|
+
if (custom && this.redisHandler) {
|
|
1246
|
+
await this.redisHandler.setKey("patterns", "custom", [...this.customPatterns].join(","));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async removePattern(pattern) {
|
|
1250
|
+
this.customPatterns.delete(pattern);
|
|
1251
|
+
this.compiledCustomContexts.delete(pattern);
|
|
1252
|
+
if (this.redisHandler) {
|
|
1253
|
+
await this.redisHandler.setKey("patterns", "custom", [...this.customPatterns].join(","));
|
|
1254
|
+
}
|
|
1255
|
+
await this.compiler.clearCache();
|
|
1256
|
+
await this.monitor.removePatternStats(pattern);
|
|
1257
|
+
}
|
|
1258
|
+
getDefaultPatterns() {
|
|
1259
|
+
return PATTERN_DEFINITIONS.map(([p]) => p);
|
|
1260
|
+
}
|
|
1261
|
+
getCustomPatterns() {
|
|
1262
|
+
return [...this.customPatterns];
|
|
1263
|
+
}
|
|
1264
|
+
getAllPatterns() {
|
|
1265
|
+
return [...this.getDefaultPatterns(), ...this.getCustomPatterns()];
|
|
1266
|
+
}
|
|
1267
|
+
async getPerformanceStats() {
|
|
1268
|
+
return {
|
|
1269
|
+
summary: this.monitor.getSummaryStats(),
|
|
1270
|
+
slowPatterns: this.monitor.getSlowPatterns(),
|
|
1271
|
+
problematicPatterns: this.monitor.getProblematicPatterns()
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
getComponentStatus() {
|
|
1275
|
+
return {
|
|
1276
|
+
compiler: true,
|
|
1277
|
+
preprocessor: true,
|
|
1278
|
+
semanticAnalyzer: true,
|
|
1279
|
+
performanceMonitor: true
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
async configureSemanticThreshold(threshold) {
|
|
1283
|
+
this.semanticThreshold = Math.max(0, Math.min(1, threshold));
|
|
1284
|
+
}
|
|
1285
|
+
async reset() {
|
|
1286
|
+
this.customPatterns.clear();
|
|
1287
|
+
this.compiledCustomContexts.clear();
|
|
1288
|
+
this.agentHandler = null;
|
|
1289
|
+
await this.compiler.clearCache();
|
|
1290
|
+
await this.monitor.clearStats();
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// src/utils.ts
|
|
1297
|
+
var utils_exports = {};
|
|
1298
|
+
__export(utils_exports, {
|
|
1299
|
+
checkIpCountry: () => checkIpCountry,
|
|
1300
|
+
detectPenetrationAttempt: () => detectPenetrationAttempt,
|
|
1301
|
+
extractClientIp: () => extractClientIp,
|
|
1302
|
+
isIpAllowed: () => isIpAllowed,
|
|
1303
|
+
isUserAgentAllowed: () => isUserAgentAllowed,
|
|
1304
|
+
logActivity: () => logActivity,
|
|
1305
|
+
sanitizeForLog: () => sanitizeForLog,
|
|
1306
|
+
sendAgentEvent: () => sendAgentEvent
|
|
1307
|
+
});
|
|
1308
|
+
function sanitizeForLog(value) {
|
|
1309
|
+
return value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(
|
|
1310
|
+
/[\x00-\x08\x0b\x0c\x0e-\x1f]/g,
|
|
1311
|
+
(ch) => `\\x${ch.charCodeAt(0).toString(16).padStart(2, "0")}`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
async function sendAgentEvent(agentHandler, eventType, ipAddress, actionTaken, reason, request, metadata) {
|
|
1315
|
+
if (!agentHandler) return;
|
|
1316
|
+
try {
|
|
1317
|
+
await agentHandler.sendEvent({
|
|
1318
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1319
|
+
eventType,
|
|
1320
|
+
ipAddress,
|
|
1321
|
+
actionTaken,
|
|
1322
|
+
reason,
|
|
1323
|
+
endpoint: request?.urlPath ?? null,
|
|
1324
|
+
method: request?.method ?? null,
|
|
1325
|
+
userAgent: request?.headers["user-agent"] ?? null,
|
|
1326
|
+
metadata: metadata ?? {}
|
|
1327
|
+
});
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function isTrustedProxy(connectingIp, trustedProxies) {
|
|
1332
|
+
for (const proxy of trustedProxies) {
|
|
1333
|
+
if (!proxy.includes("/")) {
|
|
1334
|
+
if (connectingIp === proxy) return true;
|
|
1335
|
+
} else {
|
|
1336
|
+
try {
|
|
1337
|
+
const parsed = ipaddr4.parse(connectingIp);
|
|
1338
|
+
const [addr, prefixLen] = ipaddr4.parseCIDR(proxy);
|
|
1339
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
1340
|
+
} catch {
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
function extractFromForwardedHeader(forwardedFor, proxyDepth) {
|
|
1348
|
+
const ips = forwardedFor.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
1349
|
+
if (ips.length === 0) return null;
|
|
1350
|
+
const targetIndex = ips.length - proxyDepth;
|
|
1351
|
+
if (targetIndex < 0) return ips[0];
|
|
1352
|
+
return ips[targetIndex];
|
|
1353
|
+
}
|
|
1354
|
+
async function extractClientIp(request, config, agentHandler) {
|
|
1355
|
+
const connectingIp = request.clientHost;
|
|
1356
|
+
if (!connectingIp) return "unknown";
|
|
1357
|
+
const forwardedFor = request.headers["x-forwarded-for"] ?? null;
|
|
1358
|
+
if (forwardedFor && config.trustedProxies.length === 0) {
|
|
1359
|
+
await sendAgentEvent(
|
|
1360
|
+
agentHandler ?? null,
|
|
1361
|
+
"suspicious_request",
|
|
1362
|
+
connectingIp,
|
|
1363
|
+
"spoofing_detected",
|
|
1364
|
+
"X-Forwarded-For received from untrusted source",
|
|
1365
|
+
request
|
|
1366
|
+
);
|
|
1367
|
+
return connectingIp;
|
|
1368
|
+
}
|
|
1369
|
+
if (forwardedFor && config.trustedProxies.length > 0 && isTrustedProxy(connectingIp, config.trustedProxies)) {
|
|
1370
|
+
const extracted = extractFromForwardedHeader(forwardedFor, config.trustedProxyDepth);
|
|
1371
|
+
if (extracted && ipaddr4.isValid(extracted)) return extracted;
|
|
1372
|
+
}
|
|
1373
|
+
return connectingIp;
|
|
1374
|
+
}
|
|
1375
|
+
async function isUserAgentAllowed(userAgent, config) {
|
|
1376
|
+
for (const pattern of config.blockedUserAgents) {
|
|
1377
|
+
if (new RegExp(pattern, "i").test(userAgent)) return false;
|
|
1378
|
+
}
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
async function checkIpCountry(ip, config, geoIpHandler) {
|
|
1382
|
+
if (config.blockedCountries.length === 0 && config.whitelistCountries.length === 0) {
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
if (!geoIpHandler.isInitialized) {
|
|
1386
|
+
await geoIpHandler.initialize();
|
|
1387
|
+
}
|
|
1388
|
+
const country = geoIpHandler.getCountry(ip);
|
|
1389
|
+
if (!country) return false;
|
|
1390
|
+
if (config.whitelistCountries.length > 0) {
|
|
1391
|
+
return !config.whitelistCountries.includes(country);
|
|
1392
|
+
}
|
|
1393
|
+
if (config.blockedCountries.length > 0) {
|
|
1394
|
+
return config.blockedCountries.includes(country);
|
|
1395
|
+
}
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
async function isIpAllowed(ip, config, geoIpHandler) {
|
|
1399
|
+
try {
|
|
1400
|
+
const parsed = ipaddr4.parse(ip);
|
|
1401
|
+
for (const blocked of config.blacklist) {
|
|
1402
|
+
if (blocked.includes("/")) {
|
|
1403
|
+
const [addr, prefixLen] = ipaddr4.parseCIDR(blocked);
|
|
1404
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return false;
|
|
1405
|
+
} else if (ip === blocked) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (config.whitelist && config.whitelist.length > 0) {
|
|
1410
|
+
let found = false;
|
|
1411
|
+
for (const allowed of config.whitelist) {
|
|
1412
|
+
if (allowed.includes("/")) {
|
|
1413
|
+
const [addr, prefixLen] = ipaddr4.parseCIDR(allowed);
|
|
1414
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) {
|
|
1415
|
+
found = true;
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
} else if (ip === allowed) {
|
|
1419
|
+
found = true;
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (!found) return false;
|
|
1424
|
+
}
|
|
1425
|
+
if (geoIpHandler) {
|
|
1426
|
+
const blocked = await checkIpCountry(ip, config, geoIpHandler);
|
|
1427
|
+
if (blocked) return false;
|
|
1428
|
+
}
|
|
1429
|
+
} catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
async function detectPenetrationAttempt(request) {
|
|
1435
|
+
const { SusPatternsManager: SusPatternsManager2 } = await Promise.resolve().then(() => (init_sus_patterns(), sus_patterns_exports));
|
|
1436
|
+
const clientIp = request.clientHost ?? "unknown";
|
|
1437
|
+
const correlationId = `${clientIp}:${Date.now()}`;
|
|
1438
|
+
const queryString = Object.entries(request.queryParams).map(([k, v]) => `${k}=${v}`).join("&");
|
|
1439
|
+
if (queryString) {
|
|
1440
|
+
const result = await checkRequestComponent(queryString, "query_param", "query_params", clientIp, correlationId);
|
|
1441
|
+
if (result[0]) return result;
|
|
1442
|
+
}
|
|
1443
|
+
const urlPath = request.urlPath;
|
|
1444
|
+
if (urlPath && urlPath !== "/") {
|
|
1445
|
+
const result = await checkRequestComponent(urlPath, "url_path", "url_path", clientIp, correlationId);
|
|
1446
|
+
if (result[0]) return result;
|
|
1447
|
+
}
|
|
1448
|
+
for (const [headerName, headerValue] of Object.entries(request.headers)) {
|
|
1449
|
+
if (EXCLUDED_HEADERS.has(headerName.toLowerCase())) continue;
|
|
1450
|
+
if (headerName.toLowerCase().startsWith("sec-")) continue;
|
|
1451
|
+
const result = await checkRequestComponent(headerValue, "header", `header:${headerName}`, clientIp, correlationId);
|
|
1452
|
+
if (result[0]) return result;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
const bodyBytes = await request.body();
|
|
1456
|
+
if (bodyBytes.length > 0) {
|
|
1457
|
+
const bodyText = new TextDecoder().decode(bodyBytes);
|
|
1458
|
+
if (bodyText.trim()) {
|
|
1459
|
+
const result = await checkRequestComponent(bodyText, "request_body", "body", clientIp, correlationId);
|
|
1460
|
+
if (result[0]) return result;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
} catch {
|
|
1464
|
+
}
|
|
1465
|
+
return [false, ""];
|
|
1466
|
+
}
|
|
1467
|
+
async function checkRequestComponent(value, context, componentName, clientIp, correlationId) {
|
|
1468
|
+
try {
|
|
1469
|
+
const result = await checkValueEnhanced(value, context, clientIp, correlationId);
|
|
1470
|
+
if (result[0]) {
|
|
1471
|
+
return [true, `Suspicious ${componentName}: ${result[1]}`];
|
|
1472
|
+
}
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
return [false, ""];
|
|
1476
|
+
}
|
|
1477
|
+
async function checkValueEnhanced(value, context, clientIp, correlationId) {
|
|
1478
|
+
const { SusPatternsManager: SusPatternsManager2 } = await Promise.resolve().then(() => (init_sus_patterns(), sus_patterns_exports));
|
|
1479
|
+
try {
|
|
1480
|
+
const jsonData = JSON.parse(value);
|
|
1481
|
+
if (typeof jsonData === "object" && jsonData !== null) {
|
|
1482
|
+
const result = await checkJsonFields(jsonData, context, clientIp, correlationId);
|
|
1483
|
+
if (result[0]) return result;
|
|
1484
|
+
}
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
const dangerousPatterns = [
|
|
1488
|
+
/<script/i,
|
|
1489
|
+
/javascript:/i,
|
|
1490
|
+
/UNION\s+SELECT/i,
|
|
1491
|
+
/\.\.\//,
|
|
1492
|
+
/eval\s*\(/i,
|
|
1493
|
+
/exec\s*\(/i,
|
|
1494
|
+
/system\s*\(/i
|
|
1495
|
+
];
|
|
1496
|
+
for (const pattern of dangerousPatterns) {
|
|
1497
|
+
if (pattern.test(value)) {
|
|
1498
|
+
return [true, `Pattern match: ${pattern.source}`];
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return [false, ""];
|
|
1502
|
+
}
|
|
1503
|
+
async function checkJsonFields(data, context, clientIp, correlationId) {
|
|
1504
|
+
for (const [key, val] of Object.entries(data)) {
|
|
1505
|
+
if (typeof val === "string") {
|
|
1506
|
+
const result = await checkValueEnhanced(val, context, clientIp, correlationId);
|
|
1507
|
+
if (result[0]) return [true, `JSON field '${key}': ${result[1]}`];
|
|
1508
|
+
} else if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
1509
|
+
const result = await checkJsonFields(val, context, clientIp, correlationId);
|
|
1510
|
+
if (result[0]) return result;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return [false, ""];
|
|
1514
|
+
}
|
|
1515
|
+
function logActivity(request, logger, logType = "request", reason = "", passiveMode = false, triggerInfo = "", level = "WARNING") {
|
|
1516
|
+
if (!level) return;
|
|
1517
|
+
const clientIp = request.clientHost ?? "unknown";
|
|
1518
|
+
const method = request.method;
|
|
1519
|
+
const url = sanitizeForLog(request.urlPath);
|
|
1520
|
+
const userAgent = sanitizeForLog(request.headers["user-agent"] ?? "");
|
|
1521
|
+
let message;
|
|
1522
|
+
if (logType === "request") {
|
|
1523
|
+
message = `Request from ${clientIp}: ${method} ${url} [UA: ${userAgent}]`;
|
|
1524
|
+
} else if (logType === "suspicious") {
|
|
1525
|
+
const prefix = passiveMode ? "[PASSIVE MODE]" : "";
|
|
1526
|
+
const trigger = triggerInfo ? ` | Trigger: ${triggerInfo}` : "";
|
|
1527
|
+
message = `${prefix} Suspicious request from ${clientIp}: ${method} ${url} - ${reason}${trigger}`;
|
|
1528
|
+
} else {
|
|
1529
|
+
message = `${logType} from ${clientIp}: ${method} ${url} - ${reason}`;
|
|
1530
|
+
}
|
|
1531
|
+
const levelLower = level.toLowerCase();
|
|
1532
|
+
const logMethod = levelLower === "warning" ? "warn" : levelLower === "critical" ? "error" : levelLower === "debug" ? "debug" : levelLower === "error" ? "error" : "info";
|
|
1533
|
+
logger[logMethod](message);
|
|
1534
|
+
}
|
|
1535
|
+
var ipaddr4, EXCLUDED_HEADERS;
|
|
1536
|
+
var init_utils = __esm({
|
|
1537
|
+
"src/utils.ts"() {
|
|
1538
|
+
"use strict";
|
|
1539
|
+
ipaddr4 = __toESM(require("ipaddr.js"), 1);
|
|
1540
|
+
EXCLUDED_HEADERS = /* @__PURE__ */ new Set([
|
|
1541
|
+
"host",
|
|
1542
|
+
"user-agent",
|
|
1543
|
+
"accept",
|
|
1544
|
+
"accept-encoding",
|
|
1545
|
+
"connection",
|
|
1546
|
+
"origin",
|
|
1547
|
+
"referer",
|
|
1548
|
+
"sec-fetch-site",
|
|
1549
|
+
"sec-fetch-mode",
|
|
1550
|
+
"sec-fetch-dest",
|
|
1551
|
+
"sec-ch-ua",
|
|
1552
|
+
"sec-ch-ua-mobile",
|
|
1553
|
+
"sec-ch-ua-platform"
|
|
1554
|
+
]);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// src/index.ts
|
|
1559
|
+
var index_exports = {};
|
|
1560
|
+
__export(index_exports, {
|
|
1561
|
+
BaseSecurityDecorator: () => BaseSecurityDecorator,
|
|
1562
|
+
BehaviorRule: () => BehaviorRule,
|
|
1563
|
+
BehavioralProcessor: () => BehavioralProcessor,
|
|
1564
|
+
BypassHandler: () => BypassHandler,
|
|
1565
|
+
ContentPreprocessor: () => ContentPreprocessor,
|
|
1566
|
+
DynamicRulesSchema: () => DynamicRulesSchema,
|
|
1567
|
+
ErrorResponseFactory: () => ErrorResponseFactory,
|
|
1568
|
+
MetricsCollector: () => MetricsCollector,
|
|
1569
|
+
PatternCompiler: () => PatternCompiler,
|
|
1570
|
+
PerformanceMonitor: () => PerformanceMonitor,
|
|
1571
|
+
RequestValidator: () => RequestValidator,
|
|
1572
|
+
RouteConfig: () => RouteConfig,
|
|
1573
|
+
RouteConfigResolver: () => RouteConfigResolver,
|
|
1574
|
+
SecurityCheckPipeline: () => SecurityCheckPipeline,
|
|
1575
|
+
SecurityConfigSchema: () => SecurityConfigSchema,
|
|
1576
|
+
SecurityDecorator: () => SecurityDecorator,
|
|
1577
|
+
SecurityEventBus: () => SecurityEventBus,
|
|
1578
|
+
SemanticAnalyzer: () => SemanticAnalyzer,
|
|
1579
|
+
checkIpCountry: () => checkIpCountry,
|
|
1580
|
+
defaultLogger: () => defaultLogger,
|
|
1581
|
+
detectPenetrationAttempt: () => detectPenetrationAttempt,
|
|
1582
|
+
extractClientIp: () => extractClientIp,
|
|
1583
|
+
getRouteDecoratorConfig: () => getRouteDecoratorConfig,
|
|
1584
|
+
initializeSecurityMiddleware: () => initializeSecurityMiddleware,
|
|
1585
|
+
isIpAllowed: () => isIpAllowed,
|
|
1586
|
+
isUserAgentAllowed: () => isUserAgentAllowed,
|
|
1587
|
+
logActivity: () => logActivity,
|
|
1588
|
+
sanitizeForLog: () => sanitizeForLog,
|
|
1589
|
+
sendAgentEvent: () => sendAgentEvent
|
|
1590
|
+
});
|
|
1591
|
+
module.exports = __toCommonJS(index_exports);
|
|
1592
|
+
|
|
1593
|
+
// src/models/behavior-rule.ts
|
|
1594
|
+
var BehaviorRule = class {
|
|
1595
|
+
ruleType;
|
|
1596
|
+
threshold;
|
|
1597
|
+
window;
|
|
1598
|
+
pattern;
|
|
1599
|
+
action;
|
|
1600
|
+
customAction;
|
|
1601
|
+
constructor(ruleType, threshold, window = 3600, pattern = null, action = "log", customAction = null) {
|
|
1602
|
+
this.ruleType = ruleType;
|
|
1603
|
+
this.threshold = threshold;
|
|
1604
|
+
this.window = window;
|
|
1605
|
+
this.pattern = pattern;
|
|
1606
|
+
this.action = action;
|
|
1607
|
+
this.customAction = customAction;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
// src/models/config.ts
|
|
1612
|
+
var ipaddr = __toESM(require("ipaddr.js"), 1);
|
|
1613
|
+
var import_zod = require("zod");
|
|
1614
|
+
function isValidIpOrCidr(value) {
|
|
1615
|
+
if (value.includes("/")) {
|
|
1616
|
+
try {
|
|
1617
|
+
ipaddr.parseCIDR(value);
|
|
1618
|
+
return true;
|
|
1619
|
+
} catch {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return ipaddr.isValid(value);
|
|
1624
|
+
}
|
|
1625
|
+
var VALID_CLOUD_PROVIDERS = ["AWS", "GCP", "Azure"];
|
|
1626
|
+
var IpOrCidrSchema = import_zod.z.string().refine(isValidIpOrCidr, "Invalid IP or CIDR");
|
|
1627
|
+
var LogLevel = import_zod.z.enum(["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]);
|
|
1628
|
+
var SecurityConfigSchema = import_zod.z.object({
|
|
1629
|
+
trustedProxies: import_zod.z.array(IpOrCidrSchema).default([]),
|
|
1630
|
+
trustedProxyDepth: import_zod.z.number().int().min(1).default(1),
|
|
1631
|
+
trustXForwardedProto: import_zod.z.boolean().default(false),
|
|
1632
|
+
passiveMode: import_zod.z.boolean().default(false),
|
|
1633
|
+
geoIpHandler: import_zod.z.custom().optional(),
|
|
1634
|
+
geoResolver: import_zod.z.custom().optional(),
|
|
1635
|
+
enableRedis: import_zod.z.boolean().default(true),
|
|
1636
|
+
redisUrl: import_zod.z.string().default("redis://localhost:6379"),
|
|
1637
|
+
redisPrefix: import_zod.z.string().default("guard_core:"),
|
|
1638
|
+
whitelist: import_zod.z.array(IpOrCidrSchema).nullable().default(null),
|
|
1639
|
+
blacklist: import_zod.z.array(IpOrCidrSchema).default([]),
|
|
1640
|
+
whitelistCountries: import_zod.z.array(import_zod.z.string().length(2)).default([]),
|
|
1641
|
+
blockedCountries: import_zod.z.array(import_zod.z.string().length(2)).default([]),
|
|
1642
|
+
blockedUserAgents: import_zod.z.array(import_zod.z.string()).default([]),
|
|
1643
|
+
autoBanThreshold: import_zod.z.number().int().positive().default(10),
|
|
1644
|
+
autoBanDuration: import_zod.z.number().int().positive().default(3600),
|
|
1645
|
+
logger: import_zod.z.custom().optional(),
|
|
1646
|
+
customLogFile: import_zod.z.string().nullable().default(null),
|
|
1647
|
+
logSuspiciousLevel: LogLevel.nullable().default("WARNING"),
|
|
1648
|
+
logRequestLevel: LogLevel.nullable().default(null),
|
|
1649
|
+
logFormat: import_zod.z.enum(["text", "json"]).default("text"),
|
|
1650
|
+
customErrorResponses: import_zod.z.record(import_zod.z.coerce.number(), import_zod.z.string()).default({}),
|
|
1651
|
+
rateLimit: import_zod.z.number().int().positive().default(10),
|
|
1652
|
+
rateLimitWindow: import_zod.z.number().int().positive().default(60),
|
|
1653
|
+
enforceHttps: import_zod.z.boolean().default(false),
|
|
1654
|
+
securityHeaders: import_zod.z.object({
|
|
1655
|
+
enabled: import_zod.z.boolean().default(true),
|
|
1656
|
+
hsts: import_zod.z.object({
|
|
1657
|
+
maxAge: import_zod.z.number().default(31536e3),
|
|
1658
|
+
includeSubdomains: import_zod.z.boolean().default(true),
|
|
1659
|
+
preload: import_zod.z.boolean().default(false)
|
|
1660
|
+
}).optional(),
|
|
1661
|
+
csp: import_zod.z.record(import_zod.z.string(), import_zod.z.array(import_zod.z.string())).nullable().default(null),
|
|
1662
|
+
frameOptions: import_zod.z.enum(["DENY", "SAMEORIGIN"]).default("SAMEORIGIN"),
|
|
1663
|
+
contentTypeOptions: import_zod.z.string().default("nosniff"),
|
|
1664
|
+
xssProtection: import_zod.z.string().default("1; mode=block"),
|
|
1665
|
+
referrerPolicy: import_zod.z.string().default("strict-origin-when-cross-origin"),
|
|
1666
|
+
permissionsPolicy: import_zod.z.string().default("geolocation=(), microphone=(), camera=()"),
|
|
1667
|
+
custom: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).nullable().default(null)
|
|
1668
|
+
}).nullable().default({
|
|
1669
|
+
enabled: true,
|
|
1670
|
+
hsts: { maxAge: 31536e3, includeSubdomains: true, preload: false },
|
|
1671
|
+
frameOptions: "SAMEORIGIN",
|
|
1672
|
+
contentTypeOptions: "nosniff",
|
|
1673
|
+
xssProtection: "1; mode=block",
|
|
1674
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
1675
|
+
permissionsPolicy: "geolocation=(), microphone=(), camera=()",
|
|
1676
|
+
csp: null,
|
|
1677
|
+
custom: null
|
|
1678
|
+
}),
|
|
1679
|
+
customRequestCheck: import_zod.z.custom().optional(),
|
|
1680
|
+
customResponseModifier: import_zod.z.custom().optional(),
|
|
1681
|
+
enableCors: import_zod.z.boolean().default(false),
|
|
1682
|
+
corsAllowOrigins: import_zod.z.array(import_zod.z.string()).default(["*"]),
|
|
1683
|
+
corsAllowMethods: import_zod.z.array(import_zod.z.string()).default(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]),
|
|
1684
|
+
corsAllowHeaders: import_zod.z.array(import_zod.z.string()).default(["*"]),
|
|
1685
|
+
corsAllowCredentials: import_zod.z.boolean().default(false),
|
|
1686
|
+
corsExposeHeaders: import_zod.z.array(import_zod.z.string()).default([]),
|
|
1687
|
+
corsMaxAge: import_zod.z.number().int().positive().default(600),
|
|
1688
|
+
blockCloudProviders: import_zod.z.array(import_zod.z.enum(VALID_CLOUD_PROVIDERS)).default([]).transform((arr) => new Set(arr)),
|
|
1689
|
+
cloudIpRefreshInterval: import_zod.z.number().int().min(60).max(86400).default(3600),
|
|
1690
|
+
excludePaths: import_zod.z.array(import_zod.z.string()).default([]),
|
|
1691
|
+
enableIpBanning: import_zod.z.boolean().default(true),
|
|
1692
|
+
enableRateLimiting: import_zod.z.boolean().default(true),
|
|
1693
|
+
enablePenetrationDetection: import_zod.z.boolean().default(true),
|
|
1694
|
+
emergencyMode: import_zod.z.boolean().default(false),
|
|
1695
|
+
emergencyWhitelist: import_zod.z.array(import_zod.z.string()).default([]),
|
|
1696
|
+
endpointRateLimits: import_zod.z.record(import_zod.z.string(), import_zod.z.tuple([import_zod.z.number(), import_zod.z.number()])).default({}),
|
|
1697
|
+
detectionCompilerTimeout: import_zod.z.number().min(0.1).max(10).default(2),
|
|
1698
|
+
detectionMaxContentLength: import_zod.z.number().int().min(1e3).max(1e5).default(1e4),
|
|
1699
|
+
detectionPreserveAttackPatterns: import_zod.z.boolean().default(true),
|
|
1700
|
+
detectionSemanticThreshold: import_zod.z.number().min(0).max(1).default(0.7),
|
|
1701
|
+
detectionAnomalyThreshold: import_zod.z.number().min(1).max(10).default(3),
|
|
1702
|
+
detectionSlowPatternThreshold: import_zod.z.number().min(0.01).max(1).default(0.1),
|
|
1703
|
+
detectionMonitorHistorySize: import_zod.z.number().int().min(100).max(1e4).default(1e3),
|
|
1704
|
+
detectionMaxTrackedPatterns: import_zod.z.number().int().min(100).max(5e3).default(1e3),
|
|
1705
|
+
enableAgent: import_zod.z.boolean().default(false),
|
|
1706
|
+
agentApiKey: import_zod.z.string().nullable().default(null),
|
|
1707
|
+
agentEndpoint: import_zod.z.string().url().default("https://api.fastapi-guard.com"),
|
|
1708
|
+
agentProjectId: import_zod.z.string().nullable().default(null),
|
|
1709
|
+
agentBufferSize: import_zod.z.number().int().positive().default(100),
|
|
1710
|
+
agentFlushInterval: import_zod.z.number().int().positive().default(30),
|
|
1711
|
+
agentEnableEvents: import_zod.z.boolean().default(true),
|
|
1712
|
+
agentEnableMetrics: import_zod.z.boolean().default(true),
|
|
1713
|
+
agentTimeout: import_zod.z.number().int().positive().default(30),
|
|
1714
|
+
agentRetryAttempts: import_zod.z.number().int().nonnegative().default(3),
|
|
1715
|
+
enableDynamicRules: import_zod.z.boolean().default(false),
|
|
1716
|
+
dynamicRuleInterval: import_zod.z.number().int().positive().default(300)
|
|
1717
|
+
}).superRefine((data, ctx) => {
|
|
1718
|
+
if (data.enableAgent && !data.agentApiKey) {
|
|
1719
|
+
ctx.addIssue({
|
|
1720
|
+
code: "custom",
|
|
1721
|
+
message: "agentApiKey is required when enableAgent is true",
|
|
1722
|
+
path: ["agentApiKey"]
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
if (data.enableDynamicRules && !data.enableAgent) {
|
|
1726
|
+
ctx.addIssue({
|
|
1727
|
+
code: "custom",
|
|
1728
|
+
message: "enableAgent must be true when enableDynamicRules is true",
|
|
1729
|
+
path: ["enableDynamicRules"]
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
if ((data.blockedCountries.length > 0 || data.whitelistCountries.length > 0) && !data.geoIpHandler && !data.geoResolver) {
|
|
1733
|
+
ctx.addIssue({
|
|
1734
|
+
code: "custom",
|
|
1735
|
+
message: "geoIpHandler or geoResolver is required when using country filtering",
|
|
1736
|
+
path: ["geoIpHandler"]
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
// src/models/dynamic-rules.ts
|
|
1742
|
+
var import_zod2 = require("zod");
|
|
1743
|
+
var VALID_CLOUD_PROVIDERS2 = ["AWS", "GCP", "Azure"];
|
|
1744
|
+
var DynamicRulesSchema = import_zod2.z.object({
|
|
1745
|
+
ruleId: import_zod2.z.string(),
|
|
1746
|
+
version: import_zod2.z.number().int(),
|
|
1747
|
+
timestamp: import_zod2.z.string().datetime(),
|
|
1748
|
+
expiresAt: import_zod2.z.string().datetime().nullable().default(null),
|
|
1749
|
+
ttl: import_zod2.z.number().int().default(300),
|
|
1750
|
+
ipBlacklist: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
1751
|
+
ipWhitelist: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
1752
|
+
ipBanDuration: import_zod2.z.number().int().default(3600),
|
|
1753
|
+
blockedCountries: import_zod2.z.array(import_zod2.z.string().length(2)).default([]),
|
|
1754
|
+
whitelistCountries: import_zod2.z.array(import_zod2.z.string().length(2)).default([]),
|
|
1755
|
+
globalRateLimit: import_zod2.z.number().int().nullable().default(null),
|
|
1756
|
+
globalRateWindow: import_zod2.z.number().int().nullable().default(null),
|
|
1757
|
+
endpointRateLimits: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.tuple([import_zod2.z.number(), import_zod2.z.number()])).default({}),
|
|
1758
|
+
blockedCloudProviders: import_zod2.z.array(import_zod2.z.enum(VALID_CLOUD_PROVIDERS2)).default([]).transform((arr) => new Set(arr)),
|
|
1759
|
+
blockedUserAgents: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
1760
|
+
suspiciousPatterns: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
1761
|
+
enablePenetrationDetection: import_zod2.z.boolean().nullable().default(null),
|
|
1762
|
+
enableIpBanning: import_zod2.z.boolean().nullable().default(null),
|
|
1763
|
+
enableRateLimiting: import_zod2.z.boolean().nullable().default(null),
|
|
1764
|
+
emergencyMode: import_zod2.z.boolean().default(false),
|
|
1765
|
+
emergencyWhitelist: import_zod2.z.array(import_zod2.z.string()).default([])
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
// src/models/logger.ts
|
|
1769
|
+
var defaultLogger = {
|
|
1770
|
+
info: (msg, ...args) => console.info(`[guard-core] ${msg}`, ...args),
|
|
1771
|
+
warn: (msg, ...args) => console.warn(`[guard-core] ${msg}`, ...args),
|
|
1772
|
+
error: (msg, ...args) => console.error(`[guard-core] ${msg}`, ...args),
|
|
1773
|
+
debug: (msg, ...args) => console.debug(`[guard-core] ${msg}`, ...args)
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
// src/models/route-config.ts
|
|
1777
|
+
var RouteConfig = class {
|
|
1778
|
+
rateLimit = null;
|
|
1779
|
+
rateLimitWindow = null;
|
|
1780
|
+
ipWhitelist = null;
|
|
1781
|
+
ipBlacklist = null;
|
|
1782
|
+
blockedCountries = null;
|
|
1783
|
+
whitelistCountries = null;
|
|
1784
|
+
bypassedChecks = /* @__PURE__ */ new Set();
|
|
1785
|
+
requireHttps = false;
|
|
1786
|
+
authRequired = null;
|
|
1787
|
+
customValidators = [];
|
|
1788
|
+
blockedUserAgents = [];
|
|
1789
|
+
requiredHeaders = {};
|
|
1790
|
+
behaviorRules = [];
|
|
1791
|
+
blockCloudProviders = /* @__PURE__ */ new Set();
|
|
1792
|
+
maxRequestSize = null;
|
|
1793
|
+
allowedContentTypes = null;
|
|
1794
|
+
timeRestrictions = null;
|
|
1795
|
+
enableSuspiciousDetection = true;
|
|
1796
|
+
requireReferrer = null;
|
|
1797
|
+
apiKeyRequired = false;
|
|
1798
|
+
sessionLimits = null;
|
|
1799
|
+
geoRateLimits = null;
|
|
1800
|
+
};
|
|
1801
|
+
|
|
1802
|
+
// src/detection-engine/index.ts
|
|
1803
|
+
init_compiler();
|
|
1804
|
+
init_preprocessor();
|
|
1805
|
+
init_monitor();
|
|
1806
|
+
init_semantic();
|
|
1807
|
+
|
|
1808
|
+
// src/core/events/event-bus.ts
|
|
1809
|
+
var SecurityEventBus = class {
|
|
1810
|
+
constructor(agentHandler, config, logger, geoIpHandler = null) {
|
|
1811
|
+
this.agentHandler = agentHandler;
|
|
1812
|
+
this.config = config;
|
|
1813
|
+
this.logger = logger;
|
|
1814
|
+
this.geoIpHandler = geoIpHandler;
|
|
1815
|
+
}
|
|
1816
|
+
async sendMiddlewareEvent(eventType, request, actionTaken, reason, metadata) {
|
|
1817
|
+
if (!this.agentHandler || !this.config.agentEnableEvents) return;
|
|
1818
|
+
try {
|
|
1819
|
+
const clientIp = request.clientHost ?? "unknown";
|
|
1820
|
+
let country = null;
|
|
1821
|
+
if (this.geoIpHandler) {
|
|
1822
|
+
try {
|
|
1823
|
+
country = this.geoIpHandler.getCountry(clientIp);
|
|
1824
|
+
} catch {
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
await this.agentHandler.sendEvent({
|
|
1828
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1829
|
+
eventType,
|
|
1830
|
+
ipAddress: clientIp,
|
|
1831
|
+
country,
|
|
1832
|
+
userAgent: request.headers["user-agent"] ?? null,
|
|
1833
|
+
actionTaken,
|
|
1834
|
+
reason,
|
|
1835
|
+
endpoint: request.urlPath,
|
|
1836
|
+
method: request.method,
|
|
1837
|
+
metadata: metadata ?? {}
|
|
1838
|
+
});
|
|
1839
|
+
} catch (e) {
|
|
1840
|
+
this.logger.error(`Failed to send security event: ${e}`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async sendHttpsViolationEvent(request, isRouteSpecific) {
|
|
1844
|
+
const httpsUrl = request.urlReplaceScheme("https");
|
|
1845
|
+
if (isRouteSpecific) {
|
|
1846
|
+
await this.sendMiddlewareEvent(
|
|
1847
|
+
"decorator_violation",
|
|
1848
|
+
request,
|
|
1849
|
+
"https_redirect",
|
|
1850
|
+
"Route requires HTTPS but request was HTTP",
|
|
1851
|
+
{ decoratorType: "authentication", violationType: "require_https", redirectUrl: httpsUrl }
|
|
1852
|
+
);
|
|
1853
|
+
} else {
|
|
1854
|
+
await this.sendMiddlewareEvent(
|
|
1855
|
+
"https_enforced",
|
|
1856
|
+
request,
|
|
1857
|
+
"https_redirect",
|
|
1858
|
+
"HTTP request redirected to HTTPS for security",
|
|
1859
|
+
{ originalScheme: request.urlScheme, redirectUrl: httpsUrl }
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async sendCloudDetectionEvents(request, clientIp, providers, passiveMode) {
|
|
1864
|
+
await this.sendMiddlewareEvent(
|
|
1865
|
+
"cloud_detection",
|
|
1866
|
+
request,
|
|
1867
|
+
/* v8 ignore next -- V8 cannot track ternary branch coverage inside string template literal */
|
|
1868
|
+
passiveMode ? "logged_only" : "request_blocked",
|
|
1869
|
+
`Cloud provider IP ${clientIp} detected`,
|
|
1870
|
+
{ blockedProviders: providers }
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
// src/core/events/metrics.ts
|
|
1876
|
+
var MetricsCollector = class {
|
|
1877
|
+
constructor(agentHandler, config, logger) {
|
|
1878
|
+
this.agentHandler = agentHandler;
|
|
1879
|
+
this.config = config;
|
|
1880
|
+
this.logger = logger;
|
|
1881
|
+
}
|
|
1882
|
+
async sendMetric(metricType, value, tags) {
|
|
1883
|
+
if (!this.agentHandler || !this.config.agentEnableMetrics) return;
|
|
1884
|
+
try {
|
|
1885
|
+
await this.agentHandler.sendMetric({
|
|
1886
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1887
|
+
metricType,
|
|
1888
|
+
value,
|
|
1889
|
+
tags: tags ?? {}
|
|
1890
|
+
});
|
|
1891
|
+
} catch (e) {
|
|
1892
|
+
this.logger.error(`Failed to send metric: ${e}`);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
async collectRequestMetrics(request, responseTime, statusCode) {
|
|
1896
|
+
if (!this.agentHandler || !this.config.agentEnableMetrics) return;
|
|
1897
|
+
const endpoint = request.urlPath;
|
|
1898
|
+
const method = request.method;
|
|
1899
|
+
const tags = { endpoint, method, status: String(statusCode) };
|
|
1900
|
+
await this.sendMetric("response_time", responseTime, tags);
|
|
1901
|
+
await this.sendMetric("request_count", 1, { endpoint, method });
|
|
1902
|
+
if (statusCode >= 400) {
|
|
1903
|
+
await this.sendMetric("error_rate", 1, tags);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
// src/handlers/behavior.ts
|
|
1909
|
+
var BehaviorTracker = class {
|
|
1910
|
+
constructor(config, logger) {
|
|
1911
|
+
this.config = config;
|
|
1912
|
+
this.logger = logger;
|
|
1913
|
+
}
|
|
1914
|
+
usageCounts = /* @__PURE__ */ new Map();
|
|
1915
|
+
returnPatterns = /* @__PURE__ */ new Map();
|
|
1916
|
+
redisHandler = null;
|
|
1917
|
+
agentHandler = null;
|
|
1918
|
+
async initializeRedis(redisHandler) {
|
|
1919
|
+
this.redisHandler = redisHandler;
|
|
1920
|
+
}
|
|
1921
|
+
async initializeAgent(agentHandler) {
|
|
1922
|
+
this.agentHandler = agentHandler;
|
|
1923
|
+
}
|
|
1924
|
+
async trackEndpointUsage(endpointId, clientIp, rule) {
|
|
1925
|
+
const now = Date.now() / 1e3;
|
|
1926
|
+
const windowStart = now - rule.window;
|
|
1927
|
+
if (!this.usageCounts.has(endpointId)) {
|
|
1928
|
+
this.usageCounts.set(endpointId, /* @__PURE__ */ new Map());
|
|
1929
|
+
}
|
|
1930
|
+
const endpointMap = this.usageCounts.get(endpointId);
|
|
1931
|
+
if (!endpointMap.has(clientIp)) {
|
|
1932
|
+
endpointMap.set(clientIp, []);
|
|
1933
|
+
}
|
|
1934
|
+
const timestamps = endpointMap.get(clientIp);
|
|
1935
|
+
const validIdx = timestamps.findIndex((t) => t > windowStart);
|
|
1936
|
+
if (validIdx > 0) timestamps.splice(0, validIdx);
|
|
1937
|
+
else if (validIdx === -1) timestamps.length = 0;
|
|
1938
|
+
timestamps.push(now);
|
|
1939
|
+
return timestamps.length > rule.threshold;
|
|
1940
|
+
}
|
|
1941
|
+
async trackReturnPattern(endpointId, clientIp, response, rule) {
|
|
1942
|
+
if (!rule.pattern) return false;
|
|
1943
|
+
const matched = this.checkResponsePattern(response, rule.pattern);
|
|
1944
|
+
if (!matched) return false;
|
|
1945
|
+
const now = Date.now() / 1e3;
|
|
1946
|
+
const windowStart = now - rule.window;
|
|
1947
|
+
const key = `${endpointId}:${rule.pattern}`;
|
|
1948
|
+
if (!this.returnPatterns.has(key)) {
|
|
1949
|
+
this.returnPatterns.set(key, /* @__PURE__ */ new Map());
|
|
1950
|
+
}
|
|
1951
|
+
const patternMap = this.returnPatterns.get(key);
|
|
1952
|
+
if (!patternMap.has(clientIp)) {
|
|
1953
|
+
patternMap.set(clientIp, []);
|
|
1954
|
+
}
|
|
1955
|
+
const timestamps = patternMap.get(clientIp);
|
|
1956
|
+
const validIdx = timestamps.findIndex((t) => t > windowStart);
|
|
1957
|
+
if (validIdx > 0) timestamps.splice(0, validIdx);
|
|
1958
|
+
else if (validIdx === -1) timestamps.length = 0;
|
|
1959
|
+
timestamps.push(now);
|
|
1960
|
+
return timestamps.length > rule.threshold;
|
|
1961
|
+
}
|
|
1962
|
+
checkResponsePattern(response, pattern) {
|
|
1963
|
+
if (pattern.startsWith("status:")) {
|
|
1964
|
+
const code = parseInt(pattern.slice(7), 10);
|
|
1965
|
+
return response.statusCode === code;
|
|
1966
|
+
}
|
|
1967
|
+
if (pattern.startsWith("regex:")) {
|
|
1968
|
+
const re = new RegExp(pattern.slice(6), "i");
|
|
1969
|
+
return response.bodyText ? re.test(response.bodyText) : false;
|
|
1970
|
+
}
|
|
1971
|
+
if (pattern.startsWith("json:")) {
|
|
1972
|
+
if (!response.bodyText) return false;
|
|
1973
|
+
try {
|
|
1974
|
+
const data = JSON.parse(response.bodyText);
|
|
1975
|
+
const path = pattern.slice(5);
|
|
1976
|
+
const parts = path.split(".");
|
|
1977
|
+
let current = data;
|
|
1978
|
+
for (const part of parts) {
|
|
1979
|
+
if (current === null || current === void 0) return false;
|
|
1980
|
+
current = current[part];
|
|
1981
|
+
}
|
|
1982
|
+
return current !== void 0 && current !== null;
|
|
1983
|
+
} catch {
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return response.bodyText ? response.bodyText.includes(pattern) : false;
|
|
1988
|
+
}
|
|
1989
|
+
async applyAction(rule, clientIp, endpointId, details) {
|
|
1990
|
+
if (this.config.passiveMode) {
|
|
1991
|
+
this.logger.info(`[PASSIVE] Would ${rule.action} ${clientIp} for ${details}`);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
switch (rule.action) {
|
|
1995
|
+
case "ban":
|
|
1996
|
+
this.logger.warn(`Behavioral ban: ${clientIp} - ${details}`);
|
|
1997
|
+
break;
|
|
1998
|
+
case "log":
|
|
1999
|
+
this.logger.info(`Behavioral log: ${clientIp} - ${details}`);
|
|
2000
|
+
break;
|
|
2001
|
+
case "throttle":
|
|
2002
|
+
this.logger.info(`Behavioral throttle: ${clientIp} - ${details}`);
|
|
2003
|
+
break;
|
|
2004
|
+
case "alert":
|
|
2005
|
+
this.logger.warn(`Behavioral alert: ${clientIp} - ${details}`);
|
|
2006
|
+
break;
|
|
2007
|
+
}
|
|
2008
|
+
if (rule.customAction) {
|
|
2009
|
+
try {
|
|
2010
|
+
rule.customAction(rule.action, clientIp, endpointId, details);
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (this.agentHandler) {
|
|
2015
|
+
try {
|
|
2016
|
+
await this.agentHandler.sendEvent({
|
|
2017
|
+
eventType: "behavioral_action",
|
|
2018
|
+
ipAddress: clientIp,
|
|
2019
|
+
actionTaken: rule.action,
|
|
2020
|
+
reason: details,
|
|
2021
|
+
metadata: { endpointId, ruleType: rule.ruleType, threshold: rule.threshold }
|
|
2022
|
+
});
|
|
2023
|
+
} catch {
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
async reset() {
|
|
2028
|
+
this.usageCounts.clear();
|
|
2029
|
+
this.returnPatterns.clear();
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
|
|
2033
|
+
// src/core/initialization/handler-initializer.ts
|
|
2034
|
+
init_cloud();
|
|
2035
|
+
|
|
2036
|
+
// src/handlers/dynamic-rules.ts
|
|
2037
|
+
var DynamicRuleManager = class {
|
|
2038
|
+
constructor(config, logger) {
|
|
2039
|
+
this.config = config;
|
|
2040
|
+
this.logger = logger;
|
|
2041
|
+
}
|
|
2042
|
+
currentRules = null;
|
|
2043
|
+
updateTimer = null;
|
|
2044
|
+
lastUpdate = 0;
|
|
2045
|
+
agentHandler = null;
|
|
2046
|
+
redisHandler = null;
|
|
2047
|
+
async initializeAgent(agentHandler) {
|
|
2048
|
+
this.agentHandler = agentHandler;
|
|
2049
|
+
if (this.config.enableDynamicRules) {
|
|
2050
|
+
this.startUpdateLoop();
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async initializeRedis(redisHandler) {
|
|
2054
|
+
this.redisHandler = redisHandler;
|
|
2055
|
+
}
|
|
2056
|
+
startUpdateLoop() {
|
|
2057
|
+
if (this.updateTimer) return;
|
|
2058
|
+
this.updateTimer = setInterval(
|
|
2059
|
+
() => {
|
|
2060
|
+
this.updateRules().catch((e) => this.logger.error(`Rule update failed: ${e}`));
|
|
2061
|
+
},
|
|
2062
|
+
this.config.dynamicRuleInterval * 1e3
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
async updateRules() {
|
|
2066
|
+
if (!this.agentHandler) return;
|
|
2067
|
+
try {
|
|
2068
|
+
const rawRules = await this.agentHandler.getDynamicRules();
|
|
2069
|
+
if (!rawRules) return;
|
|
2070
|
+
const parsed = DynamicRulesSchema.safeParse(rawRules);
|
|
2071
|
+
if (!parsed.success) {
|
|
2072
|
+
this.logger.warn(`Invalid dynamic rules: ${parsed.error.message}`);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const rules = parsed.data;
|
|
2076
|
+
if (this.currentRules && this.currentRules.ruleId === rules.ruleId && this.currentRules.version >= rules.version) {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
this.currentRules = rules;
|
|
2080
|
+
this.lastUpdate = Date.now() / 1e3;
|
|
2081
|
+
this.logger.info(`Applied dynamic rules: ${rules.ruleId} v${rules.version}`);
|
|
2082
|
+
if (this.agentHandler) {
|
|
2083
|
+
try {
|
|
2084
|
+
await this.agentHandler.sendEvent({
|
|
2085
|
+
eventType: "dynamic_rule_applied",
|
|
2086
|
+
ipAddress: "system",
|
|
2087
|
+
actionTaken: "rules_updated",
|
|
2088
|
+
reason: `Applied rules ${rules.ruleId} v${rules.version}`
|
|
2089
|
+
});
|
|
2090
|
+
} catch {
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
} catch (e) {
|
|
2094
|
+
this.logger.error(`Failed to fetch dynamic rules: ${e}`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
getCurrentRules() {
|
|
2098
|
+
return this.currentRules;
|
|
2099
|
+
}
|
|
2100
|
+
async forceUpdate() {
|
|
2101
|
+
await this.updateRules();
|
|
2102
|
+
}
|
|
2103
|
+
async stop() {
|
|
2104
|
+
if (this.updateTimer) {
|
|
2105
|
+
clearInterval(this.updateTimer);
|
|
2106
|
+
this.updateTimer = null;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
// src/handlers/ip-ban.ts
|
|
2112
|
+
var IPBanManager = class {
|
|
2113
|
+
constructor(logger) {
|
|
2114
|
+
this.logger = logger;
|
|
2115
|
+
}
|
|
2116
|
+
bannedIps = /* @__PURE__ */ new Map();
|
|
2117
|
+
redisHandler = null;
|
|
2118
|
+
agentHandler = null;
|
|
2119
|
+
maxSize = 1e4;
|
|
2120
|
+
async initializeRedis(redisHandler) {
|
|
2121
|
+
this.redisHandler = redisHandler;
|
|
2122
|
+
}
|
|
2123
|
+
async initializeAgent(agentHandler) {
|
|
2124
|
+
this.agentHandler = agentHandler;
|
|
2125
|
+
}
|
|
2126
|
+
async banIp(ip, duration, reason) {
|
|
2127
|
+
const now = Date.now() / 1e3;
|
|
2128
|
+
const expiresAt = now + duration;
|
|
2129
|
+
if (this.bannedIps.size >= this.maxSize) {
|
|
2130
|
+
const oldestKey = this.bannedIps.keys().next().value;
|
|
2131
|
+
if (oldestKey) this.bannedIps.delete(oldestKey);
|
|
2132
|
+
}
|
|
2133
|
+
this.bannedIps.set(ip, { expiresAt, reason, bannedAt: now });
|
|
2134
|
+
if (this.redisHandler) {
|
|
2135
|
+
await this.redisHandler.setKey("banned_ips", ip, String(expiresAt), duration);
|
|
2136
|
+
}
|
|
2137
|
+
if (this.agentHandler) {
|
|
2138
|
+
try {
|
|
2139
|
+
await this.agentHandler.sendEvent({
|
|
2140
|
+
eventType: "ip_banned",
|
|
2141
|
+
ipAddress: ip,
|
|
2142
|
+
actionTaken: "ip_banned",
|
|
2143
|
+
reason,
|
|
2144
|
+
metadata: { duration, expiresAt }
|
|
2145
|
+
});
|
|
2146
|
+
} catch {
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
this.logger.info(`IP banned: ${ip} for ${duration}s - ${reason}`);
|
|
2150
|
+
}
|
|
2151
|
+
async isIpBanned(ip) {
|
|
2152
|
+
const now = Date.now() / 1e3;
|
|
2153
|
+
const entry = this.bannedIps.get(ip);
|
|
2154
|
+
if (entry) {
|
|
2155
|
+
if (now <= entry.expiresAt) return true;
|
|
2156
|
+
this.bannedIps.delete(ip);
|
|
2157
|
+
}
|
|
2158
|
+
if (this.redisHandler) {
|
|
2159
|
+
const expiryStr = await this.redisHandler.getKey("banned_ips", ip);
|
|
2160
|
+
if (typeof expiryStr === "string") {
|
|
2161
|
+
const expiresAt = parseFloat(expiryStr);
|
|
2162
|
+
if (now <= expiresAt) {
|
|
2163
|
+
this.bannedIps.set(ip, {
|
|
2164
|
+
expiresAt,
|
|
2165
|
+
reason: "restored_from_redis",
|
|
2166
|
+
bannedAt: now
|
|
2167
|
+
});
|
|
2168
|
+
return true;
|
|
2169
|
+
}
|
|
2170
|
+
await this.redisHandler.delete("banned_ips", ip);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return false;
|
|
2174
|
+
}
|
|
2175
|
+
async unbanIp(ip) {
|
|
2176
|
+
this.bannedIps.delete(ip);
|
|
2177
|
+
if (this.redisHandler) {
|
|
2178
|
+
await this.redisHandler.delete("banned_ips", ip);
|
|
2179
|
+
}
|
|
2180
|
+
if (this.agentHandler) {
|
|
2181
|
+
try {
|
|
2182
|
+
await this.agentHandler.sendEvent({
|
|
2183
|
+
eventType: "ip_unbanned",
|
|
2184
|
+
ipAddress: ip,
|
|
2185
|
+
actionTaken: "ip_unbanned",
|
|
2186
|
+
reason: "Manual unban"
|
|
2187
|
+
});
|
|
2188
|
+
} catch {
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
this.logger.info(`IP unbanned: ${ip}`);
|
|
2192
|
+
}
|
|
2193
|
+
async reset() {
|
|
2194
|
+
this.bannedIps.clear();
|
|
2195
|
+
if (this.redisHandler) {
|
|
2196
|
+
await this.redisHandler.deletePattern("banned_ips:*");
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
|
|
2201
|
+
// src/handlers/rate-limit.ts
|
|
2202
|
+
var RATE_LIMIT_SCRIPT = `
|
|
2203
|
+
local key = KEYS[1]
|
|
2204
|
+
local now = tonumber(ARGV[1])
|
|
2205
|
+
local window = tonumber(ARGV[2])
|
|
2206
|
+
local limit = tonumber(ARGV[3])
|
|
2207
|
+
local window_start = now - window
|
|
2208
|
+
|
|
2209
|
+
redis.call('ZADD', key, now, now)
|
|
2210
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
|
|
2211
|
+
local count = redis.call('ZCARD', key)
|
|
2212
|
+
redis.call('EXPIRE', key, window * 2)
|
|
2213
|
+
|
|
2214
|
+
return count
|
|
2215
|
+
`;
|
|
2216
|
+
var RateLimitManager = class {
|
|
2217
|
+
constructor(logger) {
|
|
2218
|
+
this.logger = logger;
|
|
2219
|
+
}
|
|
2220
|
+
requestTimestamps = /* @__PURE__ */ new Map();
|
|
2221
|
+
redisHandler = null;
|
|
2222
|
+
agentHandler = null;
|
|
2223
|
+
rateLimitScriptSha = null;
|
|
2224
|
+
async initializeRedis(redisHandler) {
|
|
2225
|
+
this.redisHandler = redisHandler;
|
|
2226
|
+
const client = redisHandler.getRawClient();
|
|
2227
|
+
if (client) {
|
|
2228
|
+
try {
|
|
2229
|
+
this.rateLimitScriptSha = await client.script("load", RATE_LIMIT_SCRIPT);
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
this.logger.warn(`Failed to load rate limit Lua script: ${e}`);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
async initializeAgent(agentHandler) {
|
|
2236
|
+
this.agentHandler = agentHandler;
|
|
2237
|
+
}
|
|
2238
|
+
async checkRateLimit(request, clientIp, createErrorResponse, endpointPath = null, rateLimit = 10, rateLimitWindow = 60) {
|
|
2239
|
+
const key = endpointPath ? `${clientIp}:${endpointPath}` : clientIp;
|
|
2240
|
+
const now = Date.now() / 1e3;
|
|
2241
|
+
let count = null;
|
|
2242
|
+
if (this.redisHandler) {
|
|
2243
|
+
count = await this.getRedisRequestCount(key, now, rateLimitWindow, rateLimit);
|
|
2244
|
+
}
|
|
2245
|
+
if (count === null) {
|
|
2246
|
+
count = this.getInMemoryRequestCount(key, now, rateLimitWindow);
|
|
2247
|
+
}
|
|
2248
|
+
if (count > rateLimit) {
|
|
2249
|
+
return this.handleRateLimitExceeded(
|
|
2250
|
+
request,
|
|
2251
|
+
clientIp,
|
|
2252
|
+
count,
|
|
2253
|
+
createErrorResponse,
|
|
2254
|
+
rateLimitWindow
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
return null;
|
|
2258
|
+
}
|
|
2259
|
+
async getRedisRequestCount(key, now, window, _limit) {
|
|
2260
|
+
const client = this.redisHandler?.getRawClient();
|
|
2261
|
+
if (!client) return null;
|
|
2262
|
+
const redisKey = `rate_limit:rate:${key}`;
|
|
2263
|
+
const prefix = this.redisHandler["prefix"];
|
|
2264
|
+
const fullKey = `${prefix}${redisKey}`;
|
|
2265
|
+
try {
|
|
2266
|
+
if (this.rateLimitScriptSha) {
|
|
2267
|
+
const count2 = await client.evalsha(
|
|
2268
|
+
this.rateLimitScriptSha,
|
|
2269
|
+
1,
|
|
2270
|
+
fullKey,
|
|
2271
|
+
now,
|
|
2272
|
+
window,
|
|
2273
|
+
_limit
|
|
2274
|
+
);
|
|
2275
|
+
return Number(count2);
|
|
2276
|
+
}
|
|
2277
|
+
await client.zadd(fullKey, now, String(now));
|
|
2278
|
+
await client.zremrangebyscore(fullKey, 0, now - window);
|
|
2279
|
+
const count = await client.zcard(fullKey);
|
|
2280
|
+
await client.eval('redis.call("EXPIRE", KEYS[1], ARGV[1])', 1, fullKey, window * 2);
|
|
2281
|
+
return count;
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
this.logger.warn(`Redis rate limit check failed, falling back to in-memory: ${e}`);
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
getInMemoryRequestCount(key, now, window) {
|
|
2288
|
+
let timestamps = this.requestTimestamps.get(key);
|
|
2289
|
+
if (!timestamps) {
|
|
2290
|
+
timestamps = [];
|
|
2291
|
+
this.requestTimestamps.set(key, timestamps);
|
|
2292
|
+
}
|
|
2293
|
+
const windowStart = now - window;
|
|
2294
|
+
const validIndex = timestamps.findIndex((t) => t > windowStart);
|
|
2295
|
+
if (validIndex > 0) {
|
|
2296
|
+
timestamps.splice(0, validIndex);
|
|
2297
|
+
} else if (validIndex === -1) {
|
|
2298
|
+
timestamps.length = 0;
|
|
2299
|
+
}
|
|
2300
|
+
timestamps.push(now);
|
|
2301
|
+
return timestamps.length;
|
|
2302
|
+
}
|
|
2303
|
+
async handleRateLimitExceeded(request, clientIp, count, createErrorResponse, window) {
|
|
2304
|
+
this.logger.warn(`Rate limit exceeded for ${clientIp}: ${count} requests`);
|
|
2305
|
+
if (this.agentHandler) {
|
|
2306
|
+
try {
|
|
2307
|
+
await this.agentHandler.sendEvent({
|
|
2308
|
+
eventType: "rate_limit_exceeded",
|
|
2309
|
+
ipAddress: clientIp,
|
|
2310
|
+
actionTaken: "request_blocked",
|
|
2311
|
+
reason: `Rate limit exceeded: ${count} requests in ${window}s window`,
|
|
2312
|
+
metadata: {
|
|
2313
|
+
endpoint: request.urlPath,
|
|
2314
|
+
method: request.method,
|
|
2315
|
+
requestCount: count,
|
|
2316
|
+
window
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
} catch {
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return createErrorResponse(429, "Rate limit exceeded");
|
|
2323
|
+
}
|
|
2324
|
+
async reset() {
|
|
2325
|
+
this.requestTimestamps.clear();
|
|
2326
|
+
if (this.redisHandler) {
|
|
2327
|
+
await this.redisHandler.deletePattern("rate_limit:rate:*");
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
// src/handlers/redis.ts
|
|
2333
|
+
var RedisManager = class {
|
|
2334
|
+
constructor(config, logger) {
|
|
2335
|
+
this.config = config;
|
|
2336
|
+
this.logger = logger;
|
|
2337
|
+
this.prefix = config.redisPrefix;
|
|
2338
|
+
}
|
|
2339
|
+
client = null;
|
|
2340
|
+
closed = false;
|
|
2341
|
+
agentHandler = null;
|
|
2342
|
+
prefix;
|
|
2343
|
+
async initialize() {
|
|
2344
|
+
if (!this.config.enableRedis || this.closed) return;
|
|
2345
|
+
try {
|
|
2346
|
+
const { default: Redis } = await import("ioredis");
|
|
2347
|
+
this.client = new Redis(this.config.redisUrl);
|
|
2348
|
+
await this.client.ping();
|
|
2349
|
+
this.logger.info("Redis connection established");
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
this.logger.error(`Redis connection failed: ${e}`);
|
|
2352
|
+
this.client = null;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async close() {
|
|
2356
|
+
this.closed = true;
|
|
2357
|
+
if (this.client) {
|
|
2358
|
+
try {
|
|
2359
|
+
await this.client.quit();
|
|
2360
|
+
} catch {
|
|
2361
|
+
}
|
|
2362
|
+
this.client = null;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
async initializeAgent(agentHandler) {
|
|
2366
|
+
this.agentHandler = agentHandler;
|
|
2367
|
+
}
|
|
2368
|
+
/* v8 ignore start -- getConnection returns pooled disposable; V8 cannot track inline Symbol.asyncDispose */
|
|
2369
|
+
getConnection() {
|
|
2370
|
+
const client = this.client;
|
|
2371
|
+
return {
|
|
2372
|
+
[Symbol.asyncDispose]: async () => {
|
|
2373
|
+
},
|
|
2374
|
+
get client() {
|
|
2375
|
+
return client;
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
/* v8 ignore stop */
|
|
2380
|
+
formatKey(namespace, key) {
|
|
2381
|
+
return `${this.prefix}${namespace}:${key}`;
|
|
2382
|
+
}
|
|
2383
|
+
async getKey(namespace, key) {
|
|
2384
|
+
if (!this.client) return null;
|
|
2385
|
+
try {
|
|
2386
|
+
return await this.client.get(this.formatKey(namespace, key));
|
|
2387
|
+
} catch (e) {
|
|
2388
|
+
this.logger.error(`Redis get failed: ${e}`);
|
|
2389
|
+
return null;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
async setKey(namespace, key, value, ttl) {
|
|
2393
|
+
if (!this.client) return null;
|
|
2394
|
+
try {
|
|
2395
|
+
const fullKey = this.formatKey(namespace, key);
|
|
2396
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
2397
|
+
if (ttl && ttl > 0) {
|
|
2398
|
+
await this.client.setex(fullKey, ttl, strValue);
|
|
2399
|
+
} else {
|
|
2400
|
+
await this.client.set(fullKey, strValue);
|
|
2401
|
+
}
|
|
2402
|
+
return true;
|
|
2403
|
+
} catch (e) {
|
|
2404
|
+
this.logger.error(`Redis set failed: ${e}`);
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
async incr(namespace, key, ttl) {
|
|
2409
|
+
if (!this.client) return null;
|
|
2410
|
+
try {
|
|
2411
|
+
const fullKey = this.formatKey(namespace, key);
|
|
2412
|
+
const count = await this.client.incr(fullKey);
|
|
2413
|
+
if (ttl && ttl > 0) {
|
|
2414
|
+
await this.client.expire(fullKey, ttl);
|
|
2415
|
+
}
|
|
2416
|
+
return count;
|
|
2417
|
+
} catch (e) {
|
|
2418
|
+
this.logger.error(`Redis incr failed: ${e}`);
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
async exists(namespace, key) {
|
|
2423
|
+
if (!this.client) return null;
|
|
2424
|
+
try {
|
|
2425
|
+
const result = await this.client.exists(this.formatKey(namespace, key));
|
|
2426
|
+
return result > 0;
|
|
2427
|
+
} catch (e) {
|
|
2428
|
+
this.logger.error(`Redis exists failed: ${e}`);
|
|
2429
|
+
return null;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
async delete(namespace, key) {
|
|
2433
|
+
if (!this.client) return null;
|
|
2434
|
+
try {
|
|
2435
|
+
return await this.client.del(this.formatKey(namespace, key));
|
|
2436
|
+
} catch (e) {
|
|
2437
|
+
this.logger.error(`Redis delete failed: ${e}`);
|
|
2438
|
+
return null;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
async keys(pattern) {
|
|
2442
|
+
if (!this.client) return null;
|
|
2443
|
+
try {
|
|
2444
|
+
return await this.client.keys(`${this.prefix}${pattern}`);
|
|
2445
|
+
} catch (e) {
|
|
2446
|
+
this.logger.error(`Redis keys failed: ${e}`);
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
async deletePattern(pattern) {
|
|
2451
|
+
if (!this.client) return null;
|
|
2452
|
+
try {
|
|
2453
|
+
const matchedKeys = await this.client.keys(`${this.prefix}${pattern}`);
|
|
2454
|
+
if (matchedKeys.length === 0) return 0;
|
|
2455
|
+
return await this.client.del(...matchedKeys);
|
|
2456
|
+
} catch (e) {
|
|
2457
|
+
this.logger.error(`Redis deletePattern failed: ${e}`);
|
|
2458
|
+
return null;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
getRawClient() {
|
|
2462
|
+
return this.client;
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
|
|
2466
|
+
// src/handlers/security-headers.ts
|
|
2467
|
+
var DEFAULT_HEADERS = {
|
|
2468
|
+
"X-Content-Type-Options": "nosniff",
|
|
2469
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
2470
|
+
"X-XSS-Protection": "1; mode=block",
|
|
2471
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
2472
|
+
"Permissions-Policy": "geolocation=(), microphone=(), camera=()",
|
|
2473
|
+
"X-Permitted-Cross-Domain-Policies": "none",
|
|
2474
|
+
"X-Download-Options": "noopen",
|
|
2475
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
2476
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
2477
|
+
"Cross-Origin-Resource-Policy": "same-origin"
|
|
2478
|
+
};
|
|
2479
|
+
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
2480
|
+
function validateHeaderValue(value) {
|
|
2481
|
+
if (value.includes("\r") || value.includes("\n")) {
|
|
2482
|
+
throw new Error("Header value must not contain CR or LF characters");
|
|
2483
|
+
}
|
|
2484
|
+
if (value.length > MAX_HEADER_VALUE_LENGTH) {
|
|
2485
|
+
throw new Error(`Header value exceeds maximum length of ${MAX_HEADER_VALUE_LENGTH}`);
|
|
2486
|
+
}
|
|
2487
|
+
return value.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
2488
|
+
}
|
|
2489
|
+
function generateCacheKey(requestPath) {
|
|
2490
|
+
const normalized = requestPath.toLowerCase().replace(/\/+$/, "");
|
|
2491
|
+
let hash = 0;
|
|
2492
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
2493
|
+
hash = (hash << 5) - hash + normalized.charCodeAt(i);
|
|
2494
|
+
hash |= 0;
|
|
2495
|
+
}
|
|
2496
|
+
return String(Math.abs(hash)).padStart(16, "0").slice(0, 16);
|
|
2497
|
+
}
|
|
2498
|
+
var SecurityHeadersManager = class {
|
|
2499
|
+
constructor(logger) {
|
|
2500
|
+
this.logger = logger;
|
|
2501
|
+
}
|
|
2502
|
+
headersCache = /* @__PURE__ */ new Map();
|
|
2503
|
+
defaultHeaders = { ...DEFAULT_HEADERS };
|
|
2504
|
+
customHeaders = {};
|
|
2505
|
+
cspConfig = null;
|
|
2506
|
+
hstsConfig = null;
|
|
2507
|
+
corsConfig = null;
|
|
2508
|
+
redisHandler = null;
|
|
2509
|
+
agentHandler = null;
|
|
2510
|
+
cacheMaxSize = 1e3;
|
|
2511
|
+
cacheTtlMs = 3e5;
|
|
2512
|
+
cacheTimestamps = /* @__PURE__ */ new Map();
|
|
2513
|
+
async initializeRedis(redisHandler) {
|
|
2514
|
+
this.redisHandler = redisHandler;
|
|
2515
|
+
await this.loadCachedConfig();
|
|
2516
|
+
}
|
|
2517
|
+
/* v8 ignore start -- initializeAgent assignment; tested via handler tests but V8 misses when called from mock */
|
|
2518
|
+
async initializeAgent(agentHandler) {
|
|
2519
|
+
this.agentHandler = agentHandler;
|
|
2520
|
+
}
|
|
2521
|
+
/* v8 ignore stop */
|
|
2522
|
+
async loadCachedConfig() {
|
|
2523
|
+
if (!this.redisHandler) return;
|
|
2524
|
+
const cspJson = await this.redisHandler.getKey("security_headers", "csp_config");
|
|
2525
|
+
if (typeof cspJson === "string") {
|
|
2526
|
+
try {
|
|
2527
|
+
this.cspConfig = JSON.parse(cspJson);
|
|
2528
|
+
} catch {
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const hstsJson = await this.redisHandler.getKey("security_headers", "hsts_config");
|
|
2532
|
+
if (typeof hstsJson === "string") {
|
|
2533
|
+
try {
|
|
2534
|
+
this.hstsConfig = JSON.parse(hstsJson);
|
|
2535
|
+
} catch {
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
const customJson = await this.redisHandler.getKey("security_headers", "custom_headers");
|
|
2539
|
+
if (typeof customJson === "string") {
|
|
2540
|
+
try {
|
|
2541
|
+
this.customHeaders = JSON.parse(customJson);
|
|
2542
|
+
} catch {
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
configure(options) {
|
|
2547
|
+
if (options.enabled === false) {
|
|
2548
|
+
this.defaultHeaders = {};
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
if (options.csp) this.cspConfig = options.csp;
|
|
2552
|
+
if (options.hstsMaxAge !== void 0) {
|
|
2553
|
+
this.hstsConfig = {
|
|
2554
|
+
maxAge: options.hstsMaxAge,
|
|
2555
|
+
includeSubdomains: options.hstsIncludeSubdomains ?? true,
|
|
2556
|
+
preload: options.hstsPreload ?? false
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
if (options.frameOptions) this.defaultHeaders["X-Frame-Options"] = validateHeaderValue(options.frameOptions);
|
|
2560
|
+
if (options.contentTypeOptions) this.defaultHeaders["X-Content-Type-Options"] = validateHeaderValue(options.contentTypeOptions);
|
|
2561
|
+
if (options.xssProtection) this.defaultHeaders["X-XSS-Protection"] = validateHeaderValue(options.xssProtection);
|
|
2562
|
+
if (options.referrerPolicy) this.defaultHeaders["Referrer-Policy"] = validateHeaderValue(options.referrerPolicy);
|
|
2563
|
+
if (options.permissionsPolicy) this.defaultHeaders["Permissions-Policy"] = validateHeaderValue(options.permissionsPolicy);
|
|
2564
|
+
if (options.customHeaders) {
|
|
2565
|
+
for (const [key, value] of Object.entries(options.customHeaders)) {
|
|
2566
|
+
this.customHeaders[key] = validateHeaderValue(value);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
if (options.corsOrigins) {
|
|
2570
|
+
this.corsConfig = {
|
|
2571
|
+
origins: options.corsOrigins,
|
|
2572
|
+
allowCredentials: options.corsAllowCredentials ?? false,
|
|
2573
|
+
allowMethods: options.corsAllowMethods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
2574
|
+
allowHeaders: options.corsAllowHeaders ?? ["*"]
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
this.cacheConfiguration();
|
|
2578
|
+
}
|
|
2579
|
+
async cacheConfiguration() {
|
|
2580
|
+
if (!this.redisHandler) return;
|
|
2581
|
+
const ttl = 86400;
|
|
2582
|
+
if (this.cspConfig) await this.redisHandler.setKey("security_headers", "csp_config", JSON.stringify(this.cspConfig), ttl);
|
|
2583
|
+
if (this.hstsConfig) await this.redisHandler.setKey("security_headers", "hsts_config", JSON.stringify(this.hstsConfig), ttl);
|
|
2584
|
+
if (Object.keys(this.customHeaders).length > 0) {
|
|
2585
|
+
await this.redisHandler.setKey("security_headers", "custom_headers", JSON.stringify(this.customHeaders), ttl);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
buildCsp() {
|
|
2589
|
+
if (!this.cspConfig) return null;
|
|
2590
|
+
return Object.entries(this.cspConfig).map(([directive, values]) => `${directive} ${values.join(" ")}`).join("; ");
|
|
2591
|
+
}
|
|
2592
|
+
buildHsts() {
|
|
2593
|
+
if (!this.hstsConfig) return null;
|
|
2594
|
+
let header = `max-age=${this.hstsConfig.maxAge}`;
|
|
2595
|
+
if (this.hstsConfig.includeSubdomains) header += "; includeSubDomains";
|
|
2596
|
+
if (this.hstsConfig.preload) header += "; preload";
|
|
2597
|
+
return header;
|
|
2598
|
+
}
|
|
2599
|
+
async getHeaders(requestPath) {
|
|
2600
|
+
const cacheKey = generateCacheKey(requestPath);
|
|
2601
|
+
const now = Date.now();
|
|
2602
|
+
const cachedTimestamp = this.cacheTimestamps.get(cacheKey);
|
|
2603
|
+
if (cachedTimestamp && now - cachedTimestamp < this.cacheTtlMs) {
|
|
2604
|
+
const cached = this.headersCache.get(cacheKey);
|
|
2605
|
+
if (cached) return { ...cached };
|
|
2606
|
+
}
|
|
2607
|
+
const headers = { ...this.defaultHeaders };
|
|
2608
|
+
const csp = this.buildCsp();
|
|
2609
|
+
if (csp) headers["Content-Security-Policy"] = csp;
|
|
2610
|
+
const hsts = this.buildHsts();
|
|
2611
|
+
if (hsts) headers["Strict-Transport-Security"] = hsts;
|
|
2612
|
+
for (const [key, value] of Object.entries(this.customHeaders)) {
|
|
2613
|
+
headers[key] = value;
|
|
2614
|
+
}
|
|
2615
|
+
if (this.headersCache.size >= this.cacheMaxSize) {
|
|
2616
|
+
const oldestKey = this.headersCache.keys().next().value;
|
|
2617
|
+
if (oldestKey) {
|
|
2618
|
+
this.headersCache.delete(oldestKey);
|
|
2619
|
+
this.cacheTimestamps.delete(oldestKey);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
this.headersCache.set(cacheKey, headers);
|
|
2623
|
+
this.cacheTimestamps.set(cacheKey, now);
|
|
2624
|
+
return { ...headers };
|
|
2625
|
+
}
|
|
2626
|
+
getCorsHeaders(origin) {
|
|
2627
|
+
if (!this.corsConfig) return {};
|
|
2628
|
+
const isAllowed = this.corsConfig.origins.includes("*") || this.corsConfig.origins.includes(origin);
|
|
2629
|
+
if (!isAllowed) return {};
|
|
2630
|
+
const headers = {
|
|
2631
|
+
"Access-Control-Allow-Origin": this.corsConfig.origins.includes("*") ? "*" : origin,
|
|
2632
|
+
"Access-Control-Allow-Methods": this.corsConfig.allowMethods.join(", "),
|
|
2633
|
+
"Access-Control-Allow-Headers": this.corsConfig.allowHeaders.join(", ")
|
|
2634
|
+
};
|
|
2635
|
+
if (this.corsConfig.allowCredentials) {
|
|
2636
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
2637
|
+
}
|
|
2638
|
+
return headers;
|
|
2639
|
+
}
|
|
2640
|
+
async reset() {
|
|
2641
|
+
this.headersCache.clear();
|
|
2642
|
+
this.cacheTimestamps.clear();
|
|
2643
|
+
this.defaultHeaders = { ...DEFAULT_HEADERS };
|
|
2644
|
+
this.customHeaders = {};
|
|
2645
|
+
this.cspConfig = null;
|
|
2646
|
+
this.hstsConfig = null;
|
|
2647
|
+
this.corsConfig = null;
|
|
2648
|
+
if (this.redisHandler) {
|
|
2649
|
+
await this.redisHandler.deletePattern("security_headers:*");
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
// src/core/initialization/handler-initializer.ts
|
|
2655
|
+
init_sus_patterns();
|
|
2656
|
+
var HandlerInitializer = class {
|
|
2657
|
+
constructor(config, logger, agentHandler = null, geoIpHandler = null, guardDecorator = null) {
|
|
2658
|
+
this.config = config;
|
|
2659
|
+
this.logger = logger;
|
|
2660
|
+
this.agentHandler = agentHandler;
|
|
2661
|
+
this.geoIpHandler = geoIpHandler;
|
|
2662
|
+
this.guardDecorator = guardDecorator;
|
|
2663
|
+
}
|
|
2664
|
+
async initialize() {
|
|
2665
|
+
const ipBanHandler = new IPBanManager(this.logger);
|
|
2666
|
+
const rateLimitHandler = new RateLimitManager(this.logger);
|
|
2667
|
+
const cloudHandler = new CloudHandler(this.logger);
|
|
2668
|
+
const susPatternsHandler = new SusPatternsManager(this.config, this.logger);
|
|
2669
|
+
const securityHeadersHandler = new SecurityHeadersManager(this.logger);
|
|
2670
|
+
const behaviorTracker = new BehaviorTracker(this.config, this.logger);
|
|
2671
|
+
const dynamicRuleHandler = new DynamicRuleManager(this.config, this.logger);
|
|
2672
|
+
let redisHandler = null;
|
|
2673
|
+
if (this.config.enableRedis) {
|
|
2674
|
+
try {
|
|
2675
|
+
redisHandler = new RedisManager(this.config, this.logger);
|
|
2676
|
+
await redisHandler.initialize();
|
|
2677
|
+
await ipBanHandler.initializeRedis(redisHandler);
|
|
2678
|
+
await rateLimitHandler.initializeRedis(redisHandler);
|
|
2679
|
+
await susPatternsHandler.initializeRedis(redisHandler);
|
|
2680
|
+
await securityHeadersHandler.initializeRedis(redisHandler);
|
|
2681
|
+
await behaviorTracker.initializeRedis(redisHandler);
|
|
2682
|
+
await dynamicRuleHandler.initializeRedis(redisHandler);
|
|
2683
|
+
if (this.config.blockCloudProviders.size > 0) {
|
|
2684
|
+
await cloudHandler.initializeRedis(
|
|
2685
|
+
redisHandler,
|
|
2686
|
+
this.config.blockCloudProviders,
|
|
2687
|
+
this.config.cloudIpRefreshInterval
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
if (this.geoIpHandler) {
|
|
2691
|
+
await this.geoIpHandler.initializeRedis(redisHandler);
|
|
2692
|
+
}
|
|
2693
|
+
} catch (e) {
|
|
2694
|
+
this.logger.warn(`Redis initialization failed, falling back to in-memory: ${e}`);
|
|
2695
|
+
redisHandler = null;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (this.geoIpHandler && !this.geoIpHandler.isInitialized) {
|
|
2699
|
+
await this.geoIpHandler.initialize();
|
|
2700
|
+
}
|
|
2701
|
+
if (this.agentHandler) {
|
|
2702
|
+
await this.initializeAgentIntegrations(
|
|
2703
|
+
ipBanHandler,
|
|
2704
|
+
rateLimitHandler,
|
|
2705
|
+
cloudHandler,
|
|
2706
|
+
susPatternsHandler,
|
|
2707
|
+
dynamicRuleHandler,
|
|
2708
|
+
redisHandler
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
this.configureSecurityHeaders(securityHeadersHandler);
|
|
2712
|
+
return {
|
|
2713
|
+
redisHandler,
|
|
2714
|
+
ipBanHandler,
|
|
2715
|
+
rateLimitHandler,
|
|
2716
|
+
cloudHandler,
|
|
2717
|
+
susPatternsHandler,
|
|
2718
|
+
securityHeadersHandler,
|
|
2719
|
+
behaviorTracker,
|
|
2720
|
+
dynamicRuleHandler,
|
|
2721
|
+
geoIpHandler: this.geoIpHandler
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
async initializeAgentIntegrations(ipBanHandler, rateLimitHandler, cloudHandler, susPatternsHandler, dynamicRuleHandler, redisHandler) {
|
|
2725
|
+
if (!this.agentHandler) return;
|
|
2726
|
+
await this.agentHandler.start();
|
|
2727
|
+
if (redisHandler) {
|
|
2728
|
+
await this.agentHandler.initializeRedis(redisHandler);
|
|
2729
|
+
await redisHandler.initializeAgent(this.agentHandler);
|
|
2730
|
+
}
|
|
2731
|
+
await ipBanHandler.initializeAgent(this.agentHandler);
|
|
2732
|
+
await rateLimitHandler.initializeAgent(this.agentHandler);
|
|
2733
|
+
await susPatternsHandler.initializeAgent(this.agentHandler);
|
|
2734
|
+
if (this.config.blockCloudProviders.size > 0) {
|
|
2735
|
+
await cloudHandler.initializeAgent(this.agentHandler);
|
|
2736
|
+
}
|
|
2737
|
+
if (this.geoIpHandler) {
|
|
2738
|
+
await this.geoIpHandler.initializeAgent(this.agentHandler);
|
|
2739
|
+
}
|
|
2740
|
+
if (this.config.enableDynamicRules) {
|
|
2741
|
+
await dynamicRuleHandler.initializeAgent(this.agentHandler);
|
|
2742
|
+
}
|
|
2743
|
+
if (this.guardDecorator && typeof this.guardDecorator["initializeAgent"] === "function") {
|
|
2744
|
+
await this.guardDecorator.initializeAgent(this.agentHandler);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
configureSecurityHeaders(manager) {
|
|
2748
|
+
const headers = this.config.securityHeaders;
|
|
2749
|
+
if (!headers) return;
|
|
2750
|
+
manager.configure({
|
|
2751
|
+
enabled: headers.enabled,
|
|
2752
|
+
csp: headers.csp,
|
|
2753
|
+
hstsMaxAge: headers.hsts?.maxAge,
|
|
2754
|
+
hstsIncludeSubdomains: headers.hsts?.includeSubdomains,
|
|
2755
|
+
hstsPreload: headers.hsts?.preload,
|
|
2756
|
+
frameOptions: headers.frameOptions,
|
|
2757
|
+
contentTypeOptions: headers.contentTypeOptions,
|
|
2758
|
+
xssProtection: headers.xssProtection,
|
|
2759
|
+
referrerPolicy: headers.referrerPolicy,
|
|
2760
|
+
permissionsPolicy: headers.permissionsPolicy,
|
|
2761
|
+
customHeaders: headers.custom ?? void 0,
|
|
2762
|
+
corsOrigins: this.config.enableCors ? this.config.corsAllowOrigins : void 0,
|
|
2763
|
+
corsAllowCredentials: this.config.corsAllowCredentials,
|
|
2764
|
+
corsAllowMethods: this.config.corsAllowMethods,
|
|
2765
|
+
corsAllowHeaders: this.config.corsAllowHeaders
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
|
|
2770
|
+
// src/core/validation/validator.ts
|
|
2771
|
+
var ipaddr3 = __toESM(require("ipaddr.js"), 1);
|
|
2772
|
+
var RequestValidator = class {
|
|
2773
|
+
constructor(config, logger, eventBus) {
|
|
2774
|
+
this.config = config;
|
|
2775
|
+
this.logger = logger;
|
|
2776
|
+
this.eventBus = eventBus;
|
|
2777
|
+
}
|
|
2778
|
+
isRequestHttps(request) {
|
|
2779
|
+
let isHttps = request.urlScheme === "https";
|
|
2780
|
+
if (this.config.trustXForwardedProto && this.config.trustedProxies.length > 0 && request.clientHost) {
|
|
2781
|
+
if (this.isTrustedProxy(request.clientHost)) {
|
|
2782
|
+
const forwardedProto = request.headers["x-forwarded-proto"] ?? "";
|
|
2783
|
+
isHttps = isHttps || forwardedProto.toLowerCase() === "https";
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return isHttps;
|
|
2787
|
+
}
|
|
2788
|
+
isTrustedProxy(connectingIp) {
|
|
2789
|
+
for (const proxy of this.config.trustedProxies) {
|
|
2790
|
+
if (!proxy.includes("/")) {
|
|
2791
|
+
if (connectingIp === proxy) return true;
|
|
2792
|
+
} else {
|
|
2793
|
+
try {
|
|
2794
|
+
const parsed = ipaddr3.parse(connectingIp);
|
|
2795
|
+
const [addr, prefixLen] = ipaddr3.parseCIDR(proxy);
|
|
2796
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
2797
|
+
} catch {
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
async checkTimeWindow(timeRestrictions) {
|
|
2805
|
+
try {
|
|
2806
|
+
const { start, end } = timeRestrictions;
|
|
2807
|
+
const now = /* @__PURE__ */ new Date();
|
|
2808
|
+
const currentTime = now.toISOString().slice(11, 16);
|
|
2809
|
+
if (start > end) {
|
|
2810
|
+
return currentTime >= start || currentTime <= end;
|
|
2811
|
+
}
|
|
2812
|
+
return currentTime >= start && currentTime <= end;
|
|
2813
|
+
} catch (e) {
|
|
2814
|
+
this.logger.error(`Error checking time window: ${e}`);
|
|
2815
|
+
return true;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
async isPathExcluded(request) {
|
|
2819
|
+
const excluded = this.config.excludePaths.some(
|
|
2820
|
+
(path) => request.urlPath.startsWith(path)
|
|
2821
|
+
);
|
|
2822
|
+
if (excluded) {
|
|
2823
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
2824
|
+
"path_excluded",
|
|
2825
|
+
request,
|
|
2826
|
+
"security_checks_bypassed",
|
|
2827
|
+
`Path ${request.urlPath} excluded from security checks`,
|
|
2828
|
+
{ excludedPath: request.urlPath, configuredExclusions: this.config.excludePaths }
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
return excluded;
|
|
2832
|
+
}
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
// src/core/routing/resolver.ts
|
|
2836
|
+
var RouteConfigResolver = class {
|
|
2837
|
+
constructor(config) {
|
|
2838
|
+
this.config = config;
|
|
2839
|
+
}
|
|
2840
|
+
guardDecorator = null;
|
|
2841
|
+
setGuardDecorator(decorator) {
|
|
2842
|
+
this.guardDecorator = decorator;
|
|
2843
|
+
}
|
|
2844
|
+
getRouteConfig(request) {
|
|
2845
|
+
const decorator = this.guardDecorator ?? request.state.guardDecorator;
|
|
2846
|
+
if (!decorator) return null;
|
|
2847
|
+
const routeId = request.state.guardRouteId;
|
|
2848
|
+
if (!routeId) return null;
|
|
2849
|
+
const getConfig = decorator.getRouteConfig;
|
|
2850
|
+
if (typeof getConfig !== "function") return null;
|
|
2851
|
+
return getConfig.call(decorator, routeId) ?? null;
|
|
2852
|
+
}
|
|
2853
|
+
shouldBypassCheck(checkName, routeConfig) {
|
|
2854
|
+
if (!routeConfig) return false;
|
|
2855
|
+
return routeConfig.bypassedChecks.has(checkName) || routeConfig.bypassedChecks.has("all");
|
|
2856
|
+
}
|
|
2857
|
+
getCloudProvidersToCheck(routeConfig) {
|
|
2858
|
+
if (routeConfig && routeConfig.blockCloudProviders.size > 0) {
|
|
2859
|
+
return [...routeConfig.blockCloudProviders];
|
|
2860
|
+
}
|
|
2861
|
+
if (this.config.blockCloudProviders.size > 0) {
|
|
2862
|
+
return [...this.config.blockCloudProviders];
|
|
2863
|
+
}
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
// src/core/bypass/handler.ts
|
|
2869
|
+
var BypassHandler = class {
|
|
2870
|
+
constructor(config, eventBus, routeResolver, responseFactory, validator) {
|
|
2871
|
+
this.config = config;
|
|
2872
|
+
this.eventBus = eventBus;
|
|
2873
|
+
this.routeResolver = routeResolver;
|
|
2874
|
+
this.responseFactory = responseFactory;
|
|
2875
|
+
this.validator = validator;
|
|
2876
|
+
}
|
|
2877
|
+
async handlePassthrough(request, callNext) {
|
|
2878
|
+
if (!request.clientHost) {
|
|
2879
|
+
const response = await callNext(request);
|
|
2880
|
+
return this.responseFactory.applyModifier(response);
|
|
2881
|
+
}
|
|
2882
|
+
if (await this.validator.isPathExcluded(request)) {
|
|
2883
|
+
const response = await callNext(request);
|
|
2884
|
+
return this.responseFactory.applyModifier(response);
|
|
2885
|
+
}
|
|
2886
|
+
return null;
|
|
2887
|
+
}
|
|
2888
|
+
async handleSecurityBypass(request, callNext, routeConfig) {
|
|
2889
|
+
if (!routeConfig || !this.routeResolver.shouldBypassCheck("all", routeConfig)) {
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
2893
|
+
"security_bypass",
|
|
2894
|
+
request,
|
|
2895
|
+
"all_checks_bypassed",
|
|
2896
|
+
"Route configured to bypass all security checks",
|
|
2897
|
+
{ bypassedChecks: [...routeConfig.bypassedChecks], endpoint: request.urlPath }
|
|
2898
|
+
);
|
|
2899
|
+
if (!this.config.passiveMode) {
|
|
2900
|
+
const response = await callNext(request);
|
|
2901
|
+
return this.responseFactory.applyModifier(response);
|
|
2902
|
+
}
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
// src/core/responses/factory.ts
|
|
2908
|
+
var ErrorResponseFactory = class {
|
|
2909
|
+
constructor(config, logger, metricsCollector, guardResponseFactory, securityHeadersManager, agentHandler = null) {
|
|
2910
|
+
this.config = config;
|
|
2911
|
+
this.logger = logger;
|
|
2912
|
+
this.metricsCollector = metricsCollector;
|
|
2913
|
+
this.guardResponseFactory = guardResponseFactory;
|
|
2914
|
+
this.securityHeadersManager = securityHeadersManager;
|
|
2915
|
+
this.agentHandler = agentHandler;
|
|
2916
|
+
}
|
|
2917
|
+
async createErrorResponse(statusCode, defaultMessage) {
|
|
2918
|
+
const message = this.config.customErrorResponses[statusCode] ?? defaultMessage;
|
|
2919
|
+
const response = this.guardResponseFactory.createResponse(message, statusCode);
|
|
2920
|
+
await this.applySecurityHeaders(response, void 0);
|
|
2921
|
+
return this.applyModifier(response);
|
|
2922
|
+
}
|
|
2923
|
+
async createHttpsRedirect(request) {
|
|
2924
|
+
const httpsUrl = request.urlReplaceScheme("https");
|
|
2925
|
+
const response = this.guardResponseFactory.createRedirectResponse(httpsUrl, 301);
|
|
2926
|
+
return this.applyModifier(response);
|
|
2927
|
+
}
|
|
2928
|
+
async applySecurityHeaders(response, requestPath) {
|
|
2929
|
+
const headersConfig = this.config.securityHeaders;
|
|
2930
|
+
if (headersConfig && headersConfig.enabled) {
|
|
2931
|
+
const securityHeaders = await this.securityHeadersManager.getHeaders(requestPath ?? "/");
|
|
2932
|
+
for (const [name, value] of Object.entries(securityHeaders)) {
|
|
2933
|
+
response.setHeader(name, value);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
return response;
|
|
2937
|
+
}
|
|
2938
|
+
async applyCorsHeaders(response, origin) {
|
|
2939
|
+
const corsHeaders = this.securityHeadersManager.getCorsHeaders(origin);
|
|
2940
|
+
for (const [name, value] of Object.entries(corsHeaders)) {
|
|
2941
|
+
response.setHeader(name, value);
|
|
2942
|
+
}
|
|
2943
|
+
return response;
|
|
2944
|
+
}
|
|
2945
|
+
async applyModifier(response) {
|
|
2946
|
+
if (this.config.customResponseModifier) {
|
|
2947
|
+
return this.config.customResponseModifier(response);
|
|
2948
|
+
}
|
|
2949
|
+
return response;
|
|
2950
|
+
}
|
|
2951
|
+
async processResponse(request, response, responseTime, routeConfig, processBehavioralRules) {
|
|
2952
|
+
if (routeConfig && routeConfig.behaviorRules.length > 0 && processBehavioralRules) {
|
|
2953
|
+
const clientIp = request.clientHost ?? "unknown";
|
|
2954
|
+
await processBehavioralRules(request, response, clientIp, routeConfig);
|
|
2955
|
+
}
|
|
2956
|
+
await this.metricsCollector.collectRequestMetrics(request, responseTime, response.statusCode);
|
|
2957
|
+
await this.applySecurityHeaders(response, request.urlPath);
|
|
2958
|
+
const origin = request.headers["origin"];
|
|
2959
|
+
if (origin) {
|
|
2960
|
+
await this.applyCorsHeaders(response, origin);
|
|
2961
|
+
}
|
|
2962
|
+
return this.applyModifier(response);
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
// src/core/behavioral/processor.ts
|
|
2967
|
+
var BehavioralProcessor = class {
|
|
2968
|
+
constructor(logger, eventBus) {
|
|
2969
|
+
this.logger = logger;
|
|
2970
|
+
this.eventBus = eventBus;
|
|
2971
|
+
}
|
|
2972
|
+
guardDecorator = null;
|
|
2973
|
+
setGuardDecorator(decorator) {
|
|
2974
|
+
this.guardDecorator = decorator;
|
|
2975
|
+
}
|
|
2976
|
+
async processUsageRules(request, clientIp, routeConfig) {
|
|
2977
|
+
if (!this.guardDecorator) return;
|
|
2978
|
+
const endpointId = this.getEndpointId(request);
|
|
2979
|
+
const tracker = this.guardDecorator.behaviorTracker;
|
|
2980
|
+
for (const rule of routeConfig.behaviorRules) {
|
|
2981
|
+
if (rule.ruleType === "usage" || rule.ruleType === "frequency") {
|
|
2982
|
+
const exceeded = await tracker.trackEndpointUsage(endpointId, clientIp, rule);
|
|
2983
|
+
if (exceeded) {
|
|
2984
|
+
const details = `${rule.threshold} calls in ${rule.window}s`;
|
|
2985
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
2986
|
+
"decorator_violation",
|
|
2987
|
+
request,
|
|
2988
|
+
"behavioral_action_triggered",
|
|
2989
|
+
`Behavioral ${rule.ruleType} threshold exceeded: ${details}`,
|
|
2990
|
+
{
|
|
2991
|
+
decoratorType: "behavioral",
|
|
2992
|
+
violationType: rule.ruleType,
|
|
2993
|
+
threshold: rule.threshold,
|
|
2994
|
+
window: rule.window,
|
|
2995
|
+
action: rule.action,
|
|
2996
|
+
endpointId
|
|
2997
|
+
}
|
|
2998
|
+
);
|
|
2999
|
+
await tracker.applyAction(rule, clientIp, endpointId, `Usage threshold exceeded: ${details}`);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
async processReturnRules(request, response, clientIp, routeConfig) {
|
|
3005
|
+
if (!this.guardDecorator) return;
|
|
3006
|
+
const endpointId = this.getEndpointId(request);
|
|
3007
|
+
const tracker = this.guardDecorator.behaviorTracker;
|
|
3008
|
+
for (const rule of routeConfig.behaviorRules) {
|
|
3009
|
+
if (rule.ruleType === "return_pattern") {
|
|
3010
|
+
const detected = await tracker.trackReturnPattern(endpointId, clientIp, response, rule);
|
|
3011
|
+
if (detected) {
|
|
3012
|
+
const details = `${rule.threshold} for '${rule.pattern}' in ${rule.window}s`;
|
|
3013
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
3014
|
+
"decorator_violation",
|
|
3015
|
+
request,
|
|
3016
|
+
"behavioral_action_triggered",
|
|
3017
|
+
`Return pattern threshold exceeded: ${details}`,
|
|
3018
|
+
{
|
|
3019
|
+
decoratorType: "behavioral",
|
|
3020
|
+
violationType: "return_pattern",
|
|
3021
|
+
threshold: rule.threshold,
|
|
3022
|
+
window: rule.window,
|
|
3023
|
+
pattern: rule.pattern,
|
|
3024
|
+
action: rule.action,
|
|
3025
|
+
endpointId
|
|
3026
|
+
}
|
|
3027
|
+
);
|
|
3028
|
+
await tracker.applyAction(rule, clientIp, endpointId, `Return pattern threshold exceeded: ${details}`);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
getEndpointId(request) {
|
|
3034
|
+
const endpointId = request.state.guardEndpointId;
|
|
3035
|
+
if (typeof endpointId === "string") return endpointId;
|
|
3036
|
+
return `${request.method}:${request.urlPath}`;
|
|
3037
|
+
}
|
|
3038
|
+
};
|
|
3039
|
+
|
|
3040
|
+
// src/core/checks/pipeline.ts
|
|
3041
|
+
var SecurityCheckPipeline = class {
|
|
3042
|
+
constructor(checks, logger) {
|
|
3043
|
+
this.checks = checks;
|
|
3044
|
+
this.logger = logger;
|
|
3045
|
+
}
|
|
3046
|
+
async execute(request) {
|
|
3047
|
+
for (const check of this.checks) {
|
|
3048
|
+
try {
|
|
3049
|
+
const response = await check.check(request);
|
|
3050
|
+
if (response !== null) return response;
|
|
3051
|
+
} catch (e) {
|
|
3052
|
+
this.logger.error(`Security check '${check.checkName}' failed: ${e}`);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
3057
|
+
add(check) {
|
|
3058
|
+
this.checks.push(check);
|
|
3059
|
+
}
|
|
3060
|
+
insert(index, check) {
|
|
3061
|
+
this.checks.splice(index, 0, check);
|
|
3062
|
+
}
|
|
3063
|
+
remove(name) {
|
|
3064
|
+
const idx = this.checks.findIndex((c) => c.checkName === name);
|
|
3065
|
+
if (idx === -1) return false;
|
|
3066
|
+
this.checks.splice(idx, 1);
|
|
3067
|
+
return true;
|
|
3068
|
+
}
|
|
3069
|
+
getCheckNames() {
|
|
3070
|
+
return this.checks.map((c) => c.checkName);
|
|
3071
|
+
}
|
|
3072
|
+
get length() {
|
|
3073
|
+
return this.checks.length;
|
|
3074
|
+
}
|
|
3075
|
+
};
|
|
3076
|
+
|
|
3077
|
+
// src/core/checks/base.ts
|
|
3078
|
+
var SecurityCheck = class {
|
|
3079
|
+
constructor(middleware) {
|
|
3080
|
+
this.middleware = middleware;
|
|
3081
|
+
}
|
|
3082
|
+
get config() {
|
|
3083
|
+
return this.middleware.config;
|
|
3084
|
+
}
|
|
3085
|
+
get logger() {
|
|
3086
|
+
return this.middleware.logger;
|
|
3087
|
+
}
|
|
3088
|
+
async sendEvent(type, request, action, reason, meta) {
|
|
3089
|
+
const eventBus = this.middleware.eventBus;
|
|
3090
|
+
await eventBus.sendMiddlewareEvent(type, request, action, reason, meta);
|
|
3091
|
+
}
|
|
3092
|
+
async createErrorResponse(statusCode, message) {
|
|
3093
|
+
return this.middleware.createErrorResponse(statusCode, message);
|
|
3094
|
+
}
|
|
3095
|
+
isPassiveMode() {
|
|
3096
|
+
return this.config.passiveMode;
|
|
3097
|
+
}
|
|
3098
|
+
};
|
|
3099
|
+
|
|
3100
|
+
// src/core/checks/implementations/route-config.ts
|
|
3101
|
+
var RouteConfigCheck = class extends SecurityCheck {
|
|
3102
|
+
get checkName() {
|
|
3103
|
+
return "route_config";
|
|
3104
|
+
}
|
|
3105
|
+
async check(request) {
|
|
3106
|
+
const routeResolver = this.middleware.routeResolver;
|
|
3107
|
+
const routeConfig = routeResolver.getRouteConfig(request);
|
|
3108
|
+
if (routeConfig) {
|
|
3109
|
+
request.state["_routeConfig"] = routeConfig;
|
|
3110
|
+
}
|
|
3111
|
+
return null;
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
|
|
3115
|
+
// src/core/checks/implementations/emergency-mode.ts
|
|
3116
|
+
var EmergencyModeCheck = class extends SecurityCheck {
|
|
3117
|
+
get checkName() {
|
|
3118
|
+
return "emergency_mode";
|
|
3119
|
+
}
|
|
3120
|
+
async check(request) {
|
|
3121
|
+
if (!this.config.emergencyMode) return null;
|
|
3122
|
+
const clientIp = request.clientHost ?? "";
|
|
3123
|
+
if (this.config.emergencyWhitelist.includes(clientIp)) return null;
|
|
3124
|
+
await this.sendEvent("emergency_mode", request, "request_blocked", "Emergency mode active");
|
|
3125
|
+
return this.createErrorResponse(503, "Service temporarily unavailable");
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
|
|
3129
|
+
// src/core/checks/implementations/https-enforcement.ts
|
|
3130
|
+
var HttpsEnforcementCheck = class extends SecurityCheck {
|
|
3131
|
+
validator;
|
|
3132
|
+
responseFactory;
|
|
3133
|
+
constructor(middleware, validator, responseFactory) {
|
|
3134
|
+
super(middleware);
|
|
3135
|
+
this.validator = validator;
|
|
3136
|
+
this.responseFactory = responseFactory;
|
|
3137
|
+
}
|
|
3138
|
+
get checkName() {
|
|
3139
|
+
return "https_enforcement";
|
|
3140
|
+
}
|
|
3141
|
+
async check(request) {
|
|
3142
|
+
if (!this.config.enforceHttps) return null;
|
|
3143
|
+
if (this.validator.isRequestHttps(request)) return null;
|
|
3144
|
+
if (this.isPassiveMode()) {
|
|
3145
|
+
this.logger.info(`[PASSIVE] Would redirect to HTTPS: ${request.urlPath}`);
|
|
3146
|
+
return null;
|
|
3147
|
+
}
|
|
3148
|
+
return this.responseFactory.createHttpsRedirect(request);
|
|
3149
|
+
}
|
|
3150
|
+
};
|
|
3151
|
+
|
|
3152
|
+
// src/core/checks/implementations/request-logging.ts
|
|
3153
|
+
init_utils();
|
|
3154
|
+
var RequestLoggingCheck = class extends SecurityCheck {
|
|
3155
|
+
get checkName() {
|
|
3156
|
+
return "request_logging";
|
|
3157
|
+
}
|
|
3158
|
+
async check(request) {
|
|
3159
|
+
if (this.config.logRequestLevel) {
|
|
3160
|
+
logActivity(request, this.logger, "request", "", false, "", this.config.logRequestLevel);
|
|
3161
|
+
await this.sendEvent("request_logged", request, "logged", "Request logged");
|
|
3162
|
+
}
|
|
3163
|
+
return null;
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
|
|
3167
|
+
// src/core/checks/implementations/request-size-content.ts
|
|
3168
|
+
var RequestSizeContentCheck = class extends SecurityCheck {
|
|
3169
|
+
get checkName() {
|
|
3170
|
+
return "request_size_content";
|
|
3171
|
+
}
|
|
3172
|
+
async check(request) {
|
|
3173
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3174
|
+
if (!routeConfig) return null;
|
|
3175
|
+
if (routeConfig.maxRequestSize !== null) {
|
|
3176
|
+
const contentLength = parseInt(request.headers["content-length"] ?? "0", 10);
|
|
3177
|
+
if (contentLength > routeConfig.maxRequestSize) {
|
|
3178
|
+
if (this.isPassiveMode()) {
|
|
3179
|
+
this.logger.info(`[PASSIVE] Request too large: ${contentLength} > ${routeConfig.maxRequestSize}`);
|
|
3180
|
+
return null;
|
|
3181
|
+
}
|
|
3182
|
+
return this.createErrorResponse(413, "Request entity too large");
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
if (routeConfig.allowedContentTypes !== null) {
|
|
3186
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
3187
|
+
if (contentType && !routeConfig.allowedContentTypes.some((t) => contentType.includes(t))) {
|
|
3188
|
+
if (this.isPassiveMode()) {
|
|
3189
|
+
this.logger.info(`[PASSIVE] Invalid content type: ${contentType}`);
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
return this.createErrorResponse(415, "Unsupported media type");
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
return null;
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
|
|
3199
|
+
// src/core/checks/implementations/required-headers.ts
|
|
3200
|
+
var RequiredHeadersCheck = class extends SecurityCheck {
|
|
3201
|
+
get checkName() {
|
|
3202
|
+
return "required_headers";
|
|
3203
|
+
}
|
|
3204
|
+
async check(request) {
|
|
3205
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3206
|
+
if (!routeConfig || Object.keys(routeConfig.requiredHeaders).length === 0) return null;
|
|
3207
|
+
for (const [headerName, expectedValue] of Object.entries(routeConfig.requiredHeaders)) {
|
|
3208
|
+
const actualValue = request.headers[headerName.toLowerCase()];
|
|
3209
|
+
if (!actualValue || expectedValue && actualValue !== expectedValue) {
|
|
3210
|
+
if (this.isPassiveMode()) {
|
|
3211
|
+
this.logger.info(`[PASSIVE] Missing required header: ${headerName}`);
|
|
3212
|
+
return null;
|
|
3213
|
+
}
|
|
3214
|
+
return this.createErrorResponse(400, `Missing or invalid required header: ${headerName}`);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
return null;
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
|
|
3221
|
+
// src/core/checks/helpers.ts
|
|
3222
|
+
var ipaddr5 = __toESM(require("ipaddr.js"), 1);
|
|
3223
|
+
function isIpInBlacklist(clientIp, blacklist) {
|
|
3224
|
+
for (const blocked of blacklist) {
|
|
3225
|
+
if (blocked.includes("/")) {
|
|
3226
|
+
try {
|
|
3227
|
+
const parsed = ipaddr5.parse(clientIp);
|
|
3228
|
+
const [addr, prefixLen] = ipaddr5.parseCIDR(blocked);
|
|
3229
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
3230
|
+
} catch {
|
|
3231
|
+
continue;
|
|
3232
|
+
}
|
|
3233
|
+
} else if (clientIp === blocked) {
|
|
3234
|
+
return true;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
function isIpInWhitelist(clientIp, whitelist) {
|
|
3240
|
+
if (whitelist.length === 0) return null;
|
|
3241
|
+
for (const allowed of whitelist) {
|
|
3242
|
+
if (allowed.includes("/")) {
|
|
3243
|
+
try {
|
|
3244
|
+
const parsed = ipaddr5.parse(clientIp);
|
|
3245
|
+
const [addr, prefixLen] = ipaddr5.parseCIDR(allowed);
|
|
3246
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
3247
|
+
} catch {
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
} else if (clientIp === allowed) {
|
|
3251
|
+
return true;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
return false;
|
|
3255
|
+
}
|
|
3256
|
+
function checkCountryAccess(clientIp, routeConfig, geoIpHandler) {
|
|
3257
|
+
if (!geoIpHandler) return null;
|
|
3258
|
+
let country = null;
|
|
3259
|
+
if (routeConfig.blockedCountries && routeConfig.blockedCountries.length > 0) {
|
|
3260
|
+
country = geoIpHandler.getCountry(clientIp);
|
|
3261
|
+
if (country && routeConfig.blockedCountries.includes(country)) return false;
|
|
3262
|
+
}
|
|
3263
|
+
if (routeConfig.whitelistCountries && routeConfig.whitelistCountries.length > 0) {
|
|
3264
|
+
if (country === null) country = geoIpHandler.getCountry(clientIp);
|
|
3265
|
+
if (country) return routeConfig.whitelistCountries.includes(country);
|
|
3266
|
+
return false;
|
|
3267
|
+
}
|
|
3268
|
+
return null;
|
|
3269
|
+
}
|
|
3270
|
+
async function checkRouteIpAccess(clientIp, routeConfig, middleware) {
|
|
3271
|
+
try {
|
|
3272
|
+
if (routeConfig.ipBlacklist && routeConfig.ipBlacklist.length > 0) {
|
|
3273
|
+
if (isIpInBlacklist(clientIp, routeConfig.ipBlacklist)) return false;
|
|
3274
|
+
}
|
|
3275
|
+
if (routeConfig.ipWhitelist && routeConfig.ipWhitelist.length > 0) {
|
|
3276
|
+
const whitelistResult = isIpInWhitelist(clientIp, routeConfig.ipWhitelist);
|
|
3277
|
+
if (whitelistResult !== null) return whitelistResult;
|
|
3278
|
+
}
|
|
3279
|
+
const countryResult = checkCountryAccess(clientIp, routeConfig, middleware.geoIpHandler);
|
|
3280
|
+
if (countryResult !== null) return countryResult;
|
|
3281
|
+
return null;
|
|
3282
|
+
} catch {
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
async function checkUserAgentAllowed(userAgent, routeConfig, config) {
|
|
3287
|
+
if (routeConfig && routeConfig.blockedUserAgents.length > 0) {
|
|
3288
|
+
for (const pattern of routeConfig.blockedUserAgents) {
|
|
3289
|
+
if (new RegExp(pattern, "i").test(userAgent)) return false;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
for (const pattern of config.blockedUserAgents) {
|
|
3293
|
+
if (new RegExp(pattern, "i").test(userAgent)) return false;
|
|
3294
|
+
}
|
|
3295
|
+
return true;
|
|
3296
|
+
}
|
|
3297
|
+
function validateAuthHeader(authHeader, authType) {
|
|
3298
|
+
if (authType === "bearer") {
|
|
3299
|
+
if (!authHeader.startsWith("Bearer ")) return [false, "Missing or invalid Bearer token"];
|
|
3300
|
+
} else if (authType === "basic") {
|
|
3301
|
+
if (!authHeader.startsWith("Basic ")) return [false, "Missing or invalid Basic authentication"];
|
|
3302
|
+
} else {
|
|
3303
|
+
if (!authHeader) return [false, `Missing ${authType} authentication`];
|
|
3304
|
+
}
|
|
3305
|
+
return [true, ""];
|
|
3306
|
+
}
|
|
3307
|
+
function isReferrerDomainAllowed(referrer, allowedDomains) {
|
|
3308
|
+
try {
|
|
3309
|
+
const url = new URL(referrer);
|
|
3310
|
+
const referrerDomain = url.hostname.toLowerCase();
|
|
3311
|
+
for (const allowed of allowedDomains) {
|
|
3312
|
+
const lowerAllowed = allowed.toLowerCase();
|
|
3313
|
+
if (referrerDomain === lowerAllowed || referrerDomain.endsWith(`.${lowerAllowed}`)) {
|
|
3314
|
+
return true;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
return false;
|
|
3318
|
+
} catch {
|
|
3319
|
+
return false;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
async function detectPenetrationPatterns(request, routeConfig, config, shouldBypassCheckFn) {
|
|
3323
|
+
let penetrationEnabled = config.enablePenetrationDetection;
|
|
3324
|
+
let routeSpecificDetection = null;
|
|
3325
|
+
if (routeConfig) {
|
|
3326
|
+
routeSpecificDetection = routeConfig.enableSuspiciousDetection;
|
|
3327
|
+
penetrationEnabled = routeSpecificDetection;
|
|
3328
|
+
}
|
|
3329
|
+
if (penetrationEnabled && !shouldBypassCheckFn("penetration", routeConfig)) {
|
|
3330
|
+
const { detectPenetrationAttempt: detectPenetrationAttempt2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3331
|
+
return detectPenetrationAttempt2(request);
|
|
3332
|
+
}
|
|
3333
|
+
const reason = routeSpecificDetection === false && config.enablePenetrationDetection ? "disabled_by_decorator" : "not_enabled";
|
|
3334
|
+
return [false, reason];
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// src/core/checks/implementations/authentication.ts
|
|
3338
|
+
var AuthenticationCheck = class extends SecurityCheck {
|
|
3339
|
+
get checkName() {
|
|
3340
|
+
return "authentication";
|
|
3341
|
+
}
|
|
3342
|
+
async check(request) {
|
|
3343
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3344
|
+
if (!routeConfig) return null;
|
|
3345
|
+
if (routeConfig.authRequired) {
|
|
3346
|
+
const authHeader = request.headers["authorization"] ?? "";
|
|
3347
|
+
const [isValid3, message] = validateAuthHeader(authHeader, routeConfig.authRequired);
|
|
3348
|
+
if (!isValid3) {
|
|
3349
|
+
if (this.isPassiveMode()) {
|
|
3350
|
+
this.logger.info(`[PASSIVE] Auth failed: ${message}`);
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
await this.sendEvent("authentication_failed", request, "request_blocked", message);
|
|
3354
|
+
return this.createErrorResponse(401, message);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (routeConfig.apiKeyRequired) {
|
|
3358
|
+
const apiKey = request.headers["x-api-key"] ?? "";
|
|
3359
|
+
if (!apiKey) {
|
|
3360
|
+
if (this.isPassiveMode()) {
|
|
3361
|
+
this.logger.info("[PASSIVE] Missing API key");
|
|
3362
|
+
return null;
|
|
3363
|
+
}
|
|
3364
|
+
return this.createErrorResponse(401, "API key required");
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
return null;
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
|
|
3371
|
+
// src/core/checks/implementations/referrer.ts
|
|
3372
|
+
var ReferrerCheck = class extends SecurityCheck {
|
|
3373
|
+
get checkName() {
|
|
3374
|
+
return "referrer";
|
|
3375
|
+
}
|
|
3376
|
+
async check(request) {
|
|
3377
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3378
|
+
if (!routeConfig?.requireReferrer || routeConfig.requireReferrer.length === 0) return null;
|
|
3379
|
+
const referrer = request.headers["referer"] ?? request.headers["referrer"] ?? "";
|
|
3380
|
+
if (!referrer || !isReferrerDomainAllowed(referrer, routeConfig.requireReferrer)) {
|
|
3381
|
+
if (this.isPassiveMode()) {
|
|
3382
|
+
this.logger.info(`[PASSIVE] Invalid referrer: ${referrer}`);
|
|
3383
|
+
return null;
|
|
3384
|
+
}
|
|
3385
|
+
return this.createErrorResponse(403, "Invalid referrer");
|
|
3386
|
+
}
|
|
3387
|
+
return null;
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
|
|
3391
|
+
// src/core/checks/implementations/custom-validators.ts
|
|
3392
|
+
var CustomValidatorsCheck = class extends SecurityCheck {
|
|
3393
|
+
get checkName() {
|
|
3394
|
+
return "custom_validators";
|
|
3395
|
+
}
|
|
3396
|
+
async check(request) {
|
|
3397
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3398
|
+
if (!routeConfig || routeConfig.customValidators.length === 0) return null;
|
|
3399
|
+
for (const validator of routeConfig.customValidators) {
|
|
3400
|
+
const response = await validator(request);
|
|
3401
|
+
if (response !== null) return response;
|
|
3402
|
+
}
|
|
3403
|
+
return null;
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3407
|
+
// src/core/checks/implementations/time-window.ts
|
|
3408
|
+
var TimeWindowCheck = class extends SecurityCheck {
|
|
3409
|
+
validator;
|
|
3410
|
+
constructor(middleware, validator) {
|
|
3411
|
+
super(middleware);
|
|
3412
|
+
this.validator = validator;
|
|
3413
|
+
}
|
|
3414
|
+
get checkName() {
|
|
3415
|
+
return "time_window";
|
|
3416
|
+
}
|
|
3417
|
+
async check(request) {
|
|
3418
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3419
|
+
if (!routeConfig?.timeRestrictions) return null;
|
|
3420
|
+
const withinWindow = await this.validator.checkTimeWindow(routeConfig.timeRestrictions);
|
|
3421
|
+
if (!withinWindow) {
|
|
3422
|
+
if (this.isPassiveMode()) {
|
|
3423
|
+
this.logger.info("[PASSIVE] Request outside time window");
|
|
3424
|
+
return null;
|
|
3425
|
+
}
|
|
3426
|
+
return this.createErrorResponse(403, "Access denied: outside allowed time window");
|
|
3427
|
+
}
|
|
3428
|
+
return null;
|
|
3429
|
+
}
|
|
3430
|
+
};
|
|
3431
|
+
|
|
3432
|
+
// src/core/checks/implementations/cloud-ip-refresh.ts
|
|
3433
|
+
var CloudIpRefreshCheck = class extends SecurityCheck {
|
|
3434
|
+
get checkName() {
|
|
3435
|
+
return "cloud_ip_refresh";
|
|
3436
|
+
}
|
|
3437
|
+
async check(_request) {
|
|
3438
|
+
if (this.config.blockCloudProviders.size === 0) return null;
|
|
3439
|
+
const now = Date.now() / 1e3;
|
|
3440
|
+
const elapsed = now - this.middleware.lastCloudIpRefresh;
|
|
3441
|
+
if (elapsed >= this.config.cloudIpRefreshInterval) {
|
|
3442
|
+
this.middleware.lastCloudIpRefresh = now;
|
|
3443
|
+
try {
|
|
3444
|
+
await this.middleware.refreshCloudIpRanges();
|
|
3445
|
+
} catch (e) {
|
|
3446
|
+
this.logger.error(`Cloud IP refresh failed: ${e}`);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return null;
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3452
|
+
|
|
3453
|
+
// src/core/checks/implementations/ip-security.ts
|
|
3454
|
+
init_utils();
|
|
3455
|
+
var IpSecurityCheck = class extends SecurityCheck {
|
|
3456
|
+
get checkName() {
|
|
3457
|
+
return "ip_security";
|
|
3458
|
+
}
|
|
3459
|
+
async check(request) {
|
|
3460
|
+
const clientIp = request.clientHost;
|
|
3461
|
+
if (!clientIp) return null;
|
|
3462
|
+
const ipBanHandler = this.middleware.rateLimitHandler;
|
|
3463
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3464
|
+
if (routeConfig) {
|
|
3465
|
+
const routeResult = await checkRouteIpAccess(clientIp, routeConfig, this.middleware);
|
|
3466
|
+
if (routeResult === false) {
|
|
3467
|
+
if (this.isPassiveMode()) {
|
|
3468
|
+
this.logger.info(`[PASSIVE] IP blocked by route config: ${clientIp}`);
|
|
3469
|
+
return null;
|
|
3470
|
+
}
|
|
3471
|
+
await this.sendEvent("ip_blocked", request, "request_blocked", `IP ${clientIp} blocked by route config`);
|
|
3472
|
+
return this.createErrorResponse(403, "Access denied");
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
const allowed = await isIpAllowed(clientIp, this.config, this.middleware.geoIpHandler);
|
|
3476
|
+
if (!allowed) {
|
|
3477
|
+
if (this.isPassiveMode()) {
|
|
3478
|
+
this.logger.info(`[PASSIVE] IP not allowed: ${clientIp}`);
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
await this.sendEvent("ip_blocked", request, "request_blocked", `IP ${clientIp} not allowed`);
|
|
3482
|
+
return this.createErrorResponse(403, "Access denied");
|
|
3483
|
+
}
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
};
|
|
3487
|
+
|
|
3488
|
+
// src/core/checks/implementations/cloud-provider.ts
|
|
3489
|
+
var CloudProviderCheck = class extends SecurityCheck {
|
|
3490
|
+
get checkName() {
|
|
3491
|
+
return "cloud_provider";
|
|
3492
|
+
}
|
|
3493
|
+
async check(request) {
|
|
3494
|
+
const clientIp = request.clientHost;
|
|
3495
|
+
if (!clientIp) return null;
|
|
3496
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3497
|
+
const resolver = this.middleware.routeResolver;
|
|
3498
|
+
const providers = resolver.getCloudProvidersToCheck(routeConfig ?? null);
|
|
3499
|
+
if (!providers || providers.length === 0) return null;
|
|
3500
|
+
const { CloudHandler: CloudHandler2 } = await Promise.resolve().then(() => (init_cloud(), cloud_exports));
|
|
3501
|
+
return null;
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
|
|
3505
|
+
// src/core/checks/implementations/user-agent.ts
|
|
3506
|
+
var UserAgentCheck = class extends SecurityCheck {
|
|
3507
|
+
get checkName() {
|
|
3508
|
+
return "user_agent";
|
|
3509
|
+
}
|
|
3510
|
+
async check(request) {
|
|
3511
|
+
const userAgent = request.headers["user-agent"] ?? "";
|
|
3512
|
+
if (!userAgent) return null;
|
|
3513
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3514
|
+
const allowed = await checkUserAgentAllowed(userAgent, routeConfig ?? null, this.config);
|
|
3515
|
+
if (!allowed) {
|
|
3516
|
+
if (this.isPassiveMode()) {
|
|
3517
|
+
this.logger.info(`[PASSIVE] Blocked user agent: ${userAgent}`);
|
|
3518
|
+
return null;
|
|
3519
|
+
}
|
|
3520
|
+
await this.sendEvent("ua_blocked", request, "request_blocked", `Blocked user agent: ${userAgent}`);
|
|
3521
|
+
return this.createErrorResponse(403, "Access denied");
|
|
3522
|
+
}
|
|
3523
|
+
return null;
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
|
|
3527
|
+
// src/core/checks/implementations/rate-limit.ts
|
|
3528
|
+
var RateLimitCheck = class extends SecurityCheck {
|
|
3529
|
+
get checkName() {
|
|
3530
|
+
return "rate_limit";
|
|
3531
|
+
}
|
|
3532
|
+
async check(request) {
|
|
3533
|
+
if (!this.config.enableRateLimiting) return null;
|
|
3534
|
+
const clientIp = request.clientHost;
|
|
3535
|
+
if (!clientIp) return null;
|
|
3536
|
+
const rateLimitHandler = this.middleware.rateLimitHandler;
|
|
3537
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3538
|
+
const createError = this.createErrorResponse.bind(this);
|
|
3539
|
+
if (routeConfig?.rateLimit !== null && routeConfig?.rateLimit !== void 0) {
|
|
3540
|
+
const response2 = await rateLimitHandler.checkRateLimit(
|
|
3541
|
+
request,
|
|
3542
|
+
clientIp,
|
|
3543
|
+
createError,
|
|
3544
|
+
request.urlPath,
|
|
3545
|
+
routeConfig.rateLimit,
|
|
3546
|
+
routeConfig.rateLimitWindow ?? this.config.rateLimitWindow
|
|
3547
|
+
);
|
|
3548
|
+
if (response2) {
|
|
3549
|
+
if (this.isPassiveMode()) {
|
|
3550
|
+
this.logger.info(`[PASSIVE] Route rate limit exceeded for ${clientIp}`);
|
|
3551
|
+
return null;
|
|
3552
|
+
}
|
|
3553
|
+
return response2;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
const endpointLimit = this.config.endpointRateLimits[request.urlPath];
|
|
3557
|
+
if (endpointLimit) {
|
|
3558
|
+
const [limit, window] = endpointLimit;
|
|
3559
|
+
const response2 = await rateLimitHandler.checkRateLimit(
|
|
3560
|
+
request,
|
|
3561
|
+
clientIp,
|
|
3562
|
+
createError,
|
|
3563
|
+
request.urlPath,
|
|
3564
|
+
limit,
|
|
3565
|
+
window
|
|
3566
|
+
);
|
|
3567
|
+
if (response2) {
|
|
3568
|
+
if (this.isPassiveMode()) {
|
|
3569
|
+
this.logger.info(`[PASSIVE] Endpoint rate limit exceeded for ${clientIp}`);
|
|
3570
|
+
return null;
|
|
3571
|
+
}
|
|
3572
|
+
return response2;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
const response = await rateLimitHandler.checkRateLimit(
|
|
3576
|
+
request,
|
|
3577
|
+
clientIp,
|
|
3578
|
+
createError,
|
|
3579
|
+
null,
|
|
3580
|
+
this.config.rateLimit,
|
|
3581
|
+
this.config.rateLimitWindow
|
|
3582
|
+
);
|
|
3583
|
+
if (response) {
|
|
3584
|
+
if (this.isPassiveMode()) {
|
|
3585
|
+
this.logger.info(`[PASSIVE] Global rate limit exceeded for ${clientIp}`);
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
3588
|
+
return response;
|
|
3589
|
+
}
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3592
|
+
};
|
|
3593
|
+
|
|
3594
|
+
// src/core/checks/implementations/suspicious-activity.ts
|
|
3595
|
+
init_utils();
|
|
3596
|
+
var SuspiciousActivityCheck = class extends SecurityCheck {
|
|
3597
|
+
get checkName() {
|
|
3598
|
+
return "suspicious_activity";
|
|
3599
|
+
}
|
|
3600
|
+
async check(request) {
|
|
3601
|
+
if (!this.config.enablePenetrationDetection) return null;
|
|
3602
|
+
const clientIp = request.clientHost;
|
|
3603
|
+
if (!clientIp) return null;
|
|
3604
|
+
const routeConfig = request.state["_routeConfig"];
|
|
3605
|
+
const resolver = this.middleware.routeResolver;
|
|
3606
|
+
const [isThreat, triggerInfo] = await detectPenetrationPatterns(
|
|
3607
|
+
request,
|
|
3608
|
+
routeConfig ?? null,
|
|
3609
|
+
this.config,
|
|
3610
|
+
(check, rc) => resolver.shouldBypassCheck(check, rc)
|
|
3611
|
+
);
|
|
3612
|
+
if (!isThreat) return null;
|
|
3613
|
+
const counts = this.middleware.suspiciousRequestCounts;
|
|
3614
|
+
const currentCount = (counts.get(clientIp) ?? 0) + 1;
|
|
3615
|
+
counts.set(clientIp, currentCount);
|
|
3616
|
+
logActivity(
|
|
3617
|
+
request,
|
|
3618
|
+
this.logger,
|
|
3619
|
+
"suspicious",
|
|
3620
|
+
"Suspicious activity detected",
|
|
3621
|
+
this.config.passiveMode,
|
|
3622
|
+
triggerInfo,
|
|
3623
|
+
this.config.logSuspiciousLevel
|
|
3624
|
+
);
|
|
3625
|
+
await this.sendEvent(
|
|
3626
|
+
"penetration_attempt",
|
|
3627
|
+
request,
|
|
3628
|
+
"request_blocked",
|
|
3629
|
+
`Suspicious activity: ${triggerInfo}`,
|
|
3630
|
+
{ triggerInfo, requestCount: currentCount }
|
|
3631
|
+
);
|
|
3632
|
+
if (this.isPassiveMode()) return null;
|
|
3633
|
+
return this.createErrorResponse(403, "Suspicious activity detected");
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
|
|
3637
|
+
// src/core/checks/implementations/custom-request.ts
|
|
3638
|
+
var CustomRequestCheck = class extends SecurityCheck {
|
|
3639
|
+
get checkName() {
|
|
3640
|
+
return "custom_request";
|
|
3641
|
+
}
|
|
3642
|
+
async check(request) {
|
|
3643
|
+
if (!this.config.customRequestCheck) return null;
|
|
3644
|
+
return this.config.customRequestCheck(request);
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
|
|
3648
|
+
// src/middleware-support.ts
|
|
3649
|
+
async function initializeSecurityMiddleware(config, logger, guardResponseFactory, agentHandler, geoIpHandler, guardDecorator) {
|
|
3650
|
+
const initializer = new HandlerInitializer(
|
|
3651
|
+
config,
|
|
3652
|
+
logger,
|
|
3653
|
+
agentHandler ?? null,
|
|
3654
|
+
geoIpHandler ?? null,
|
|
3655
|
+
guardDecorator ?? null
|
|
3656
|
+
);
|
|
3657
|
+
const registry = await initializer.initialize();
|
|
3658
|
+
const eventBus = new SecurityEventBus(
|
|
3659
|
+
agentHandler ?? null,
|
|
3660
|
+
config,
|
|
3661
|
+
logger,
|
|
3662
|
+
registry.geoIpHandler
|
|
3663
|
+
);
|
|
3664
|
+
const metricsCollector = new MetricsCollector(
|
|
3665
|
+
agentHandler ?? null,
|
|
3666
|
+
config,
|
|
3667
|
+
logger
|
|
3668
|
+
);
|
|
3669
|
+
const validator = new RequestValidator(config, logger, eventBus);
|
|
3670
|
+
const routeResolver = new RouteConfigResolver(config);
|
|
3671
|
+
if (guardDecorator) routeResolver.setGuardDecorator(guardDecorator);
|
|
3672
|
+
const errorResponseFactory = new ErrorResponseFactory(
|
|
3673
|
+
config,
|
|
3674
|
+
logger,
|
|
3675
|
+
metricsCollector,
|
|
3676
|
+
guardResponseFactory,
|
|
3677
|
+
registry.securityHeadersHandler,
|
|
3678
|
+
agentHandler ?? null
|
|
3679
|
+
);
|
|
3680
|
+
const bypassHandler = new BypassHandler(
|
|
3681
|
+
config,
|
|
3682
|
+
eventBus,
|
|
3683
|
+
routeResolver,
|
|
3684
|
+
errorResponseFactory,
|
|
3685
|
+
validator
|
|
3686
|
+
);
|
|
3687
|
+
const behavioralProcessor = new BehavioralProcessor(logger, eventBus);
|
|
3688
|
+
if (guardDecorator) {
|
|
3689
|
+
behavioralProcessor.setGuardDecorator(
|
|
3690
|
+
guardDecorator
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
const middlewareProtocol = {
|
|
3694
|
+
get config() {
|
|
3695
|
+
return config;
|
|
3696
|
+
},
|
|
3697
|
+
get logger() {
|
|
3698
|
+
return logger;
|
|
3699
|
+
},
|
|
3700
|
+
lastCloudIpRefresh: 0,
|
|
3701
|
+
suspiciousRequestCounts: /* @__PURE__ */ new Map(),
|
|
3702
|
+
get eventBus() {
|
|
3703
|
+
return eventBus;
|
|
3704
|
+
},
|
|
3705
|
+
get routeResolver() {
|
|
3706
|
+
return routeResolver;
|
|
3707
|
+
},
|
|
3708
|
+
get responseFactory() {
|
|
3709
|
+
return errorResponseFactory;
|
|
3710
|
+
},
|
|
3711
|
+
get rateLimitHandler() {
|
|
3712
|
+
return registry.rateLimitHandler;
|
|
3713
|
+
},
|
|
3714
|
+
get agentHandler() {
|
|
3715
|
+
return agentHandler ?? null;
|
|
3716
|
+
},
|
|
3717
|
+
get geoIpHandler() {
|
|
3718
|
+
return registry.geoIpHandler ?? null;
|
|
3719
|
+
},
|
|
3720
|
+
get guardResponseFactory() {
|
|
3721
|
+
return guardResponseFactory;
|
|
3722
|
+
},
|
|
3723
|
+
async createErrorResponse(statusCode, message) {
|
|
3724
|
+
return errorResponseFactory.createErrorResponse(statusCode, message);
|
|
3725
|
+
},
|
|
3726
|
+
async refreshCloudIpRanges() {
|
|
3727
|
+
if (registry.cloudHandler && config.blockCloudProviders.size > 0) {
|
|
3728
|
+
await registry.cloudHandler.refreshAsync(config.blockCloudProviders);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3732
|
+
const pipeline = new SecurityCheckPipeline([
|
|
3733
|
+
new RouteConfigCheck(middlewareProtocol),
|
|
3734
|
+
new EmergencyModeCheck(middlewareProtocol),
|
|
3735
|
+
new HttpsEnforcementCheck(middlewareProtocol, validator, errorResponseFactory),
|
|
3736
|
+
new RequestLoggingCheck(middlewareProtocol),
|
|
3737
|
+
new RequestSizeContentCheck(middlewareProtocol),
|
|
3738
|
+
new RequiredHeadersCheck(middlewareProtocol),
|
|
3739
|
+
new AuthenticationCheck(middlewareProtocol),
|
|
3740
|
+
new ReferrerCheck(middlewareProtocol),
|
|
3741
|
+
new CustomValidatorsCheck(middlewareProtocol),
|
|
3742
|
+
new TimeWindowCheck(middlewareProtocol, validator),
|
|
3743
|
+
new CloudIpRefreshCheck(middlewareProtocol),
|
|
3744
|
+
new IpSecurityCheck(middlewareProtocol),
|
|
3745
|
+
new CloudProviderCheck(middlewareProtocol),
|
|
3746
|
+
new UserAgentCheck(middlewareProtocol),
|
|
3747
|
+
new RateLimitCheck(middlewareProtocol),
|
|
3748
|
+
new SuspiciousActivityCheck(middlewareProtocol),
|
|
3749
|
+
new CustomRequestCheck(middlewareProtocol)
|
|
3750
|
+
], logger);
|
|
3751
|
+
return {
|
|
3752
|
+
registry,
|
|
3753
|
+
pipeline,
|
|
3754
|
+
eventBus,
|
|
3755
|
+
metricsCollector,
|
|
3756
|
+
validator,
|
|
3757
|
+
routeResolver,
|
|
3758
|
+
bypassHandler,
|
|
3759
|
+
errorResponseFactory,
|
|
3760
|
+
behavioralProcessor,
|
|
3761
|
+
middlewareProtocol
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/decorators/base.ts
|
|
3766
|
+
var routeIdMap = /* @__PURE__ */ new WeakMap();
|
|
3767
|
+
var routeIdCounter = 0;
|
|
3768
|
+
var BaseSecurityDecorator = class {
|
|
3769
|
+
routeConfigs = /* @__PURE__ */ new Map();
|
|
3770
|
+
behaviorTracker;
|
|
3771
|
+
agentHandler = null;
|
|
3772
|
+
config;
|
|
3773
|
+
logger;
|
|
3774
|
+
constructor(config, logger) {
|
|
3775
|
+
this.config = config;
|
|
3776
|
+
this.logger = logger ?? defaultLogger;
|
|
3777
|
+
this.behaviorTracker = new BehaviorTracker(config, this.logger);
|
|
3778
|
+
}
|
|
3779
|
+
getRouteConfig(routeId) {
|
|
3780
|
+
return this.routeConfigs.get(routeId);
|
|
3781
|
+
}
|
|
3782
|
+
ensureRouteConfig(fn) {
|
|
3783
|
+
const id = this.getRouteId(fn);
|
|
3784
|
+
if (!this.routeConfigs.has(id)) {
|
|
3785
|
+
const rc = new RouteConfig();
|
|
3786
|
+
rc.enableSuspiciousDetection = this.config.enablePenetrationDetection;
|
|
3787
|
+
this.routeConfigs.set(id, rc);
|
|
3788
|
+
}
|
|
3789
|
+
return this.routeConfigs.get(id);
|
|
3790
|
+
}
|
|
3791
|
+
applyRouteConfig(fn) {
|
|
3792
|
+
fn["_guardRouteId"] = this.getRouteId(fn);
|
|
3793
|
+
return fn;
|
|
3794
|
+
}
|
|
3795
|
+
getRouteId(fn) {
|
|
3796
|
+
if (!routeIdMap.has(fn)) {
|
|
3797
|
+
routeIdMap.set(fn, `guard_route_${++routeIdCounter}`);
|
|
3798
|
+
}
|
|
3799
|
+
return routeIdMap.get(fn);
|
|
3800
|
+
}
|
|
3801
|
+
async initializeBehaviorTracking(redisHandler) {
|
|
3802
|
+
if (redisHandler) await this.behaviorTracker.initializeRedis(redisHandler);
|
|
3803
|
+
}
|
|
3804
|
+
async initializeAgent(agentHandler) {
|
|
3805
|
+
this.agentHandler = agentHandler;
|
|
3806
|
+
await this.behaviorTracker.initializeAgent(agentHandler);
|
|
3807
|
+
}
|
|
3808
|
+
async sendDecoratorEvent(eventType, _request, actionTaken, reason, decoratorType, meta) {
|
|
3809
|
+
if (!this.agentHandler) return;
|
|
3810
|
+
try {
|
|
3811
|
+
await this.agentHandler.sendEvent({
|
|
3812
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3813
|
+
eventType,
|
|
3814
|
+
actionTaken,
|
|
3815
|
+
reason,
|
|
3816
|
+
decoratorType,
|
|
3817
|
+
metadata: meta ?? {}
|
|
3818
|
+
});
|
|
3819
|
+
} catch {
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
async sendAccessDeniedEvent(request, reason, decoratorType, meta) {
|
|
3823
|
+
await this.sendDecoratorEvent("access_denied", request, "request_blocked", reason, decoratorType, meta);
|
|
3824
|
+
}
|
|
3825
|
+
async sendAuthenticationFailedEvent(request, reason, authType, meta) {
|
|
3826
|
+
await this.sendDecoratorEvent("authentication_failed", request, "request_blocked", reason, "authentication", { authType, ...meta });
|
|
3827
|
+
}
|
|
3828
|
+
async sendRateLimitEvent(request, limit, window, meta) {
|
|
3829
|
+
await this.sendDecoratorEvent("rate_limit_exceeded", request, "request_blocked", `Rate limit ${limit}/${window}s exceeded`, "rate_limit", { limit, window, ...meta });
|
|
3830
|
+
}
|
|
3831
|
+
async sendDecoratorViolationEvent(request, violationType, reason, meta) {
|
|
3832
|
+
await this.sendDecoratorEvent("decorator_violation", request, "request_blocked", reason, violationType, meta);
|
|
3833
|
+
}
|
|
3834
|
+
};
|
|
3835
|
+
function getRouteDecoratorConfig(request, decoratorHandler) {
|
|
3836
|
+
const routeId = request.state.guardRouteId;
|
|
3837
|
+
if (!routeId || typeof routeId !== "string") return void 0;
|
|
3838
|
+
return decoratorHandler.getRouteConfig(routeId);
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
// src/decorators/access-control.ts
|
|
3842
|
+
function AccessControl(Base) {
|
|
3843
|
+
return class extends Base {
|
|
3844
|
+
requireIp(whitelist, blacklist) {
|
|
3845
|
+
return (fn) => {
|
|
3846
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3847
|
+
if (whitelist) rc.ipWhitelist = whitelist;
|
|
3848
|
+
if (blacklist) rc.ipBlacklist = blacklist;
|
|
3849
|
+
return this.applyRouteConfig(fn);
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
blockCountries(countries) {
|
|
3853
|
+
return (fn) => {
|
|
3854
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3855
|
+
rc.blockedCountries = countries;
|
|
3856
|
+
return this.applyRouteConfig(fn);
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
allowCountries(countries) {
|
|
3860
|
+
return (fn) => {
|
|
3861
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3862
|
+
rc.whitelistCountries = countries;
|
|
3863
|
+
return this.applyRouteConfig(fn);
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
blockClouds(providers) {
|
|
3867
|
+
return (fn) => {
|
|
3868
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3869
|
+
rc.blockCloudProviders = new Set(providers ?? ["AWS", "GCP", "Azure"]);
|
|
3870
|
+
return this.applyRouteConfig(fn);
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
bypass(checks) {
|
|
3874
|
+
return (fn) => {
|
|
3875
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3876
|
+
for (const check of checks) rc.bypassedChecks.add(check);
|
|
3877
|
+
return this.applyRouteConfig(fn);
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// src/decorators/rate-limiting.ts
|
|
3884
|
+
function RateLimiting(Base) {
|
|
3885
|
+
return class extends Base {
|
|
3886
|
+
rateLimit(requests, window = 60) {
|
|
3887
|
+
return (fn) => {
|
|
3888
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3889
|
+
rc.rateLimit = requests;
|
|
3890
|
+
rc.rateLimitWindow = window;
|
|
3891
|
+
return this.applyRouteConfig(fn);
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
geoRateLimit(limits) {
|
|
3895
|
+
return (fn) => {
|
|
3896
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3897
|
+
rc.geoRateLimits = limits;
|
|
3898
|
+
return this.applyRouteConfig(fn);
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
// src/decorators/authentication.ts
|
|
3905
|
+
function Authentication(Base) {
|
|
3906
|
+
return class extends Base {
|
|
3907
|
+
requireHttps() {
|
|
3908
|
+
return (fn) => {
|
|
3909
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3910
|
+
rc.requireHttps = true;
|
|
3911
|
+
return this.applyRouteConfig(fn);
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
requireAuth(type = "bearer") {
|
|
3915
|
+
return (fn) => {
|
|
3916
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3917
|
+
rc.authRequired = type;
|
|
3918
|
+
return this.applyRouteConfig(fn);
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
apiKeyAuth(headerName = "X-API-Key") {
|
|
3922
|
+
return (fn) => {
|
|
3923
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3924
|
+
rc.apiKeyRequired = true;
|
|
3925
|
+
rc.requiredHeaders[headerName] = "";
|
|
3926
|
+
return this.applyRouteConfig(fn);
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
requireHeaders(headers) {
|
|
3930
|
+
return (fn) => {
|
|
3931
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3932
|
+
Object.assign(rc.requiredHeaders, headers);
|
|
3933
|
+
return this.applyRouteConfig(fn);
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// src/decorators/content-filtering.ts
|
|
3940
|
+
function ContentFiltering(Base) {
|
|
3941
|
+
return class extends Base {
|
|
3942
|
+
blockUserAgents(patterns) {
|
|
3943
|
+
return (fn) => {
|
|
3944
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3945
|
+
rc.blockedUserAgents.push(...patterns);
|
|
3946
|
+
return this.applyRouteConfig(fn);
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
contentTypeFilter(allowedTypes) {
|
|
3950
|
+
return (fn) => {
|
|
3951
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3952
|
+
rc.allowedContentTypes = allowedTypes;
|
|
3953
|
+
return this.applyRouteConfig(fn);
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
maxRequestSize(sizeBytes) {
|
|
3957
|
+
return (fn) => {
|
|
3958
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3959
|
+
rc.maxRequestSize = sizeBytes;
|
|
3960
|
+
return this.applyRouteConfig(fn);
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
requireReferrer(allowedDomains) {
|
|
3964
|
+
return (fn) => {
|
|
3965
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3966
|
+
rc.requireReferrer = allowedDomains;
|
|
3967
|
+
return this.applyRouteConfig(fn);
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
customValidation(validator) {
|
|
3971
|
+
return (fn) => {
|
|
3972
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3973
|
+
rc.customValidators.push(validator);
|
|
3974
|
+
return this.applyRouteConfig(fn);
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
};
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
// src/decorators/behavioral.ts
|
|
3981
|
+
function Behavioral(Base) {
|
|
3982
|
+
return class extends Base {
|
|
3983
|
+
usageMonitor(maxCalls, window = 3600, action = "ban") {
|
|
3984
|
+
return (fn) => {
|
|
3985
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3986
|
+
rc.behaviorRules.push(new BehaviorRule("usage", maxCalls, window, null, action));
|
|
3987
|
+
return this.applyRouteConfig(fn);
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3990
|
+
returnMonitor(pattern, maxOccurrences, window = 86400, action = "ban") {
|
|
3991
|
+
return (fn) => {
|
|
3992
|
+
const rc = this.ensureRouteConfig(fn);
|
|
3993
|
+
rc.behaviorRules.push(new BehaviorRule("return_pattern", maxOccurrences, window, pattern, action));
|
|
3994
|
+
return this.applyRouteConfig(fn);
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
behaviorAnalysis(rules) {
|
|
3998
|
+
return (fn) => {
|
|
3999
|
+
const rc = this.ensureRouteConfig(fn);
|
|
4000
|
+
rc.behaviorRules.push(...rules);
|
|
4001
|
+
return this.applyRouteConfig(fn);
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
suspiciousFrequency(maxFrequency, window = 300, action = "ban") {
|
|
4005
|
+
return (fn) => {
|
|
4006
|
+
const rc = this.ensureRouteConfig(fn);
|
|
4007
|
+
rc.behaviorRules.push(new BehaviorRule("frequency", maxFrequency, window, null, action));
|
|
4008
|
+
return this.applyRouteConfig(fn);
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
// src/decorators/advanced.ts
|
|
4015
|
+
function Advanced(Base) {
|
|
4016
|
+
return class extends Base {
|
|
4017
|
+
timeWindow(startTime, endTime, _timezone = "UTC") {
|
|
4018
|
+
return (fn) => {
|
|
4019
|
+
const rc = this.ensureRouteConfig(fn);
|
|
4020
|
+
rc.timeRestrictions = { start: startTime, end: endTime };
|
|
4021
|
+
return this.applyRouteConfig(fn);
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
suspiciousDetection(enabled = true) {
|
|
4025
|
+
return (fn) => {
|
|
4026
|
+
const rc = this.ensureRouteConfig(fn);
|
|
4027
|
+
rc.enableSuspiciousDetection = enabled;
|
|
4028
|
+
return this.applyRouteConfig(fn);
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
honeypotDetection(trapFields) {
|
|
4032
|
+
return (fn) => {
|
|
4033
|
+
const rc = this.ensureRouteConfig(fn);
|
|
4034
|
+
rc.customValidators.push(async (request) => {
|
|
4035
|
+
try {
|
|
4036
|
+
const bodyBytes = await request.body();
|
|
4037
|
+
if (bodyBytes.length === 0) return null;
|
|
4038
|
+
const bodyText = new TextDecoder().decode(bodyBytes);
|
|
4039
|
+
let data = {};
|
|
4040
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
4041
|
+
if (contentType.includes("json")) {
|
|
4042
|
+
data = JSON.parse(bodyText);
|
|
4043
|
+
} else if (contentType.includes("form")) {
|
|
4044
|
+
for (const pair of bodyText.split("&")) {
|
|
4045
|
+
const [key, value] = pair.split("=");
|
|
4046
|
+
if (key && value) data[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
for (const field of trapFields) {
|
|
4050
|
+
if (data[field] !== void 0 && data[field] !== "" && data[field] !== null) {
|
|
4051
|
+
return {
|
|
4052
|
+
statusCode: 403,
|
|
4053
|
+
headers: {},
|
|
4054
|
+
setHeader() {
|
|
4055
|
+
},
|
|
4056
|
+
body: new TextEncoder().encode("Forbidden"),
|
|
4057
|
+
bodyText: "Forbidden"
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
} catch {
|
|
4062
|
+
}
|
|
4063
|
+
return null;
|
|
4064
|
+
});
|
|
4065
|
+
return this.applyRouteConfig(fn);
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
};
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
// src/decorators/index.ts
|
|
4072
|
+
var SecurityDecorator = Advanced(
|
|
4073
|
+
ContentFiltering(
|
|
4074
|
+
Behavioral(
|
|
4075
|
+
Authentication(
|
|
4076
|
+
RateLimiting(
|
|
4077
|
+
AccessControl(
|
|
4078
|
+
BaseSecurityDecorator
|
|
4079
|
+
)
|
|
4080
|
+
)
|
|
4081
|
+
)
|
|
4082
|
+
)
|
|
4083
|
+
)
|
|
4084
|
+
);
|
|
4085
|
+
|
|
4086
|
+
// src/index.ts
|
|
4087
|
+
init_utils();
|
|
4088
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4089
|
+
0 && (module.exports = {
|
|
4090
|
+
BaseSecurityDecorator,
|
|
4091
|
+
BehaviorRule,
|
|
4092
|
+
BehavioralProcessor,
|
|
4093
|
+
BypassHandler,
|
|
4094
|
+
ContentPreprocessor,
|
|
4095
|
+
DynamicRulesSchema,
|
|
4096
|
+
ErrorResponseFactory,
|
|
4097
|
+
MetricsCollector,
|
|
4098
|
+
PatternCompiler,
|
|
4099
|
+
PerformanceMonitor,
|
|
4100
|
+
RequestValidator,
|
|
4101
|
+
RouteConfig,
|
|
4102
|
+
RouteConfigResolver,
|
|
4103
|
+
SecurityCheckPipeline,
|
|
4104
|
+
SecurityConfigSchema,
|
|
4105
|
+
SecurityDecorator,
|
|
4106
|
+
SecurityEventBus,
|
|
4107
|
+
SemanticAnalyzer,
|
|
4108
|
+
checkIpCountry,
|
|
4109
|
+
defaultLogger,
|
|
4110
|
+
detectPenetrationAttempt,
|
|
4111
|
+
extractClientIp,
|
|
4112
|
+
getRouteDecoratorConfig,
|
|
4113
|
+
initializeSecurityMiddleware,
|
|
4114
|
+
isIpAllowed,
|
|
4115
|
+
isUserAgentAllowed,
|
|
4116
|
+
logActivity,
|
|
4117
|
+
sanitizeForLog,
|
|
4118
|
+
sendAgentEvent
|
|
4119
|
+
});
|
|
4120
|
+
//# sourceMappingURL=index.cjs.map
|