@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.js
ADDED
|
@@ -0,0 +1,2533 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloudHandler
|
|
3
|
+
} from "./chunk-LK7N5CAQ.js";
|
|
4
|
+
import {
|
|
5
|
+
ContentPreprocessor,
|
|
6
|
+
PatternCompiler,
|
|
7
|
+
PerformanceMonitor,
|
|
8
|
+
SemanticAnalyzer,
|
|
9
|
+
SusPatternsManager
|
|
10
|
+
} from "./chunk-I634N6VV.js";
|
|
11
|
+
import {
|
|
12
|
+
checkIpCountry,
|
|
13
|
+
detectPenetrationAttempt,
|
|
14
|
+
extractClientIp,
|
|
15
|
+
isIpAllowed,
|
|
16
|
+
isUserAgentAllowed,
|
|
17
|
+
logActivity,
|
|
18
|
+
sanitizeForLog,
|
|
19
|
+
sendAgentEvent
|
|
20
|
+
} from "./chunk-4HBVN5N7.js";
|
|
21
|
+
import "./chunk-DGUM43GV.js";
|
|
22
|
+
|
|
23
|
+
// src/models/behavior-rule.ts
|
|
24
|
+
var BehaviorRule = class {
|
|
25
|
+
ruleType;
|
|
26
|
+
threshold;
|
|
27
|
+
window;
|
|
28
|
+
pattern;
|
|
29
|
+
action;
|
|
30
|
+
customAction;
|
|
31
|
+
constructor(ruleType, threshold, window = 3600, pattern = null, action = "log", customAction = null) {
|
|
32
|
+
this.ruleType = ruleType;
|
|
33
|
+
this.threshold = threshold;
|
|
34
|
+
this.window = window;
|
|
35
|
+
this.pattern = pattern;
|
|
36
|
+
this.action = action;
|
|
37
|
+
this.customAction = customAction;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/models/config.ts
|
|
42
|
+
import * as ipaddr from "ipaddr.js";
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
function isValidIpOrCidr(value) {
|
|
45
|
+
if (value.includes("/")) {
|
|
46
|
+
try {
|
|
47
|
+
ipaddr.parseCIDR(value);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return ipaddr.isValid(value);
|
|
54
|
+
}
|
|
55
|
+
var VALID_CLOUD_PROVIDERS = ["AWS", "GCP", "Azure"];
|
|
56
|
+
var IpOrCidrSchema = z.string().refine(isValidIpOrCidr, "Invalid IP or CIDR");
|
|
57
|
+
var LogLevel = z.enum(["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]);
|
|
58
|
+
var SecurityConfigSchema = z.object({
|
|
59
|
+
trustedProxies: z.array(IpOrCidrSchema).default([]),
|
|
60
|
+
trustedProxyDepth: z.number().int().min(1).default(1),
|
|
61
|
+
trustXForwardedProto: z.boolean().default(false),
|
|
62
|
+
passiveMode: z.boolean().default(false),
|
|
63
|
+
geoIpHandler: z.custom().optional(),
|
|
64
|
+
geoResolver: z.custom().optional(),
|
|
65
|
+
enableRedis: z.boolean().default(true),
|
|
66
|
+
redisUrl: z.string().default("redis://localhost:6379"),
|
|
67
|
+
redisPrefix: z.string().default("guard_core:"),
|
|
68
|
+
whitelist: z.array(IpOrCidrSchema).nullable().default(null),
|
|
69
|
+
blacklist: z.array(IpOrCidrSchema).default([]),
|
|
70
|
+
whitelistCountries: z.array(z.string().length(2)).default([]),
|
|
71
|
+
blockedCountries: z.array(z.string().length(2)).default([]),
|
|
72
|
+
blockedUserAgents: z.array(z.string()).default([]),
|
|
73
|
+
autoBanThreshold: z.number().int().positive().default(10),
|
|
74
|
+
autoBanDuration: z.number().int().positive().default(3600),
|
|
75
|
+
logger: z.custom().optional(),
|
|
76
|
+
customLogFile: z.string().nullable().default(null),
|
|
77
|
+
logSuspiciousLevel: LogLevel.nullable().default("WARNING"),
|
|
78
|
+
logRequestLevel: LogLevel.nullable().default(null),
|
|
79
|
+
logFormat: z.enum(["text", "json"]).default("text"),
|
|
80
|
+
customErrorResponses: z.record(z.coerce.number(), z.string()).default({}),
|
|
81
|
+
rateLimit: z.number().int().positive().default(10),
|
|
82
|
+
rateLimitWindow: z.number().int().positive().default(60),
|
|
83
|
+
enforceHttps: z.boolean().default(false),
|
|
84
|
+
securityHeaders: z.object({
|
|
85
|
+
enabled: z.boolean().default(true),
|
|
86
|
+
hsts: z.object({
|
|
87
|
+
maxAge: z.number().default(31536e3),
|
|
88
|
+
includeSubdomains: z.boolean().default(true),
|
|
89
|
+
preload: z.boolean().default(false)
|
|
90
|
+
}).optional(),
|
|
91
|
+
csp: z.record(z.string(), z.array(z.string())).nullable().default(null),
|
|
92
|
+
frameOptions: z.enum(["DENY", "SAMEORIGIN"]).default("SAMEORIGIN"),
|
|
93
|
+
contentTypeOptions: z.string().default("nosniff"),
|
|
94
|
+
xssProtection: z.string().default("1; mode=block"),
|
|
95
|
+
referrerPolicy: z.string().default("strict-origin-when-cross-origin"),
|
|
96
|
+
permissionsPolicy: z.string().default("geolocation=(), microphone=(), camera=()"),
|
|
97
|
+
custom: z.record(z.string(), z.string()).nullable().default(null)
|
|
98
|
+
}).nullable().default({
|
|
99
|
+
enabled: true,
|
|
100
|
+
hsts: { maxAge: 31536e3, includeSubdomains: true, preload: false },
|
|
101
|
+
frameOptions: "SAMEORIGIN",
|
|
102
|
+
contentTypeOptions: "nosniff",
|
|
103
|
+
xssProtection: "1; mode=block",
|
|
104
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
105
|
+
permissionsPolicy: "geolocation=(), microphone=(), camera=()",
|
|
106
|
+
csp: null,
|
|
107
|
+
custom: null
|
|
108
|
+
}),
|
|
109
|
+
customRequestCheck: z.custom().optional(),
|
|
110
|
+
customResponseModifier: z.custom().optional(),
|
|
111
|
+
enableCors: z.boolean().default(false),
|
|
112
|
+
corsAllowOrigins: z.array(z.string()).default(["*"]),
|
|
113
|
+
corsAllowMethods: z.array(z.string()).default(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]),
|
|
114
|
+
corsAllowHeaders: z.array(z.string()).default(["*"]),
|
|
115
|
+
corsAllowCredentials: z.boolean().default(false),
|
|
116
|
+
corsExposeHeaders: z.array(z.string()).default([]),
|
|
117
|
+
corsMaxAge: z.number().int().positive().default(600),
|
|
118
|
+
blockCloudProviders: z.array(z.enum(VALID_CLOUD_PROVIDERS)).default([]).transform((arr) => new Set(arr)),
|
|
119
|
+
cloudIpRefreshInterval: z.number().int().min(60).max(86400).default(3600),
|
|
120
|
+
excludePaths: z.array(z.string()).default([]),
|
|
121
|
+
enableIpBanning: z.boolean().default(true),
|
|
122
|
+
enableRateLimiting: z.boolean().default(true),
|
|
123
|
+
enablePenetrationDetection: z.boolean().default(true),
|
|
124
|
+
emergencyMode: z.boolean().default(false),
|
|
125
|
+
emergencyWhitelist: z.array(z.string()).default([]),
|
|
126
|
+
endpointRateLimits: z.record(z.string(), z.tuple([z.number(), z.number()])).default({}),
|
|
127
|
+
detectionCompilerTimeout: z.number().min(0.1).max(10).default(2),
|
|
128
|
+
detectionMaxContentLength: z.number().int().min(1e3).max(1e5).default(1e4),
|
|
129
|
+
detectionPreserveAttackPatterns: z.boolean().default(true),
|
|
130
|
+
detectionSemanticThreshold: z.number().min(0).max(1).default(0.7),
|
|
131
|
+
detectionAnomalyThreshold: z.number().min(1).max(10).default(3),
|
|
132
|
+
detectionSlowPatternThreshold: z.number().min(0.01).max(1).default(0.1),
|
|
133
|
+
detectionMonitorHistorySize: z.number().int().min(100).max(1e4).default(1e3),
|
|
134
|
+
detectionMaxTrackedPatterns: z.number().int().min(100).max(5e3).default(1e3),
|
|
135
|
+
enableAgent: z.boolean().default(false),
|
|
136
|
+
agentApiKey: z.string().nullable().default(null),
|
|
137
|
+
agentEndpoint: z.string().url().default("https://api.fastapi-guard.com"),
|
|
138
|
+
agentProjectId: z.string().nullable().default(null),
|
|
139
|
+
agentBufferSize: z.number().int().positive().default(100),
|
|
140
|
+
agentFlushInterval: z.number().int().positive().default(30),
|
|
141
|
+
agentEnableEvents: z.boolean().default(true),
|
|
142
|
+
agentEnableMetrics: z.boolean().default(true),
|
|
143
|
+
agentTimeout: z.number().int().positive().default(30),
|
|
144
|
+
agentRetryAttempts: z.number().int().nonnegative().default(3),
|
|
145
|
+
enableDynamicRules: z.boolean().default(false),
|
|
146
|
+
dynamicRuleInterval: z.number().int().positive().default(300)
|
|
147
|
+
}).superRefine((data, ctx) => {
|
|
148
|
+
if (data.enableAgent && !data.agentApiKey) {
|
|
149
|
+
ctx.addIssue({
|
|
150
|
+
code: "custom",
|
|
151
|
+
message: "agentApiKey is required when enableAgent is true",
|
|
152
|
+
path: ["agentApiKey"]
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (data.enableDynamicRules && !data.enableAgent) {
|
|
156
|
+
ctx.addIssue({
|
|
157
|
+
code: "custom",
|
|
158
|
+
message: "enableAgent must be true when enableDynamicRules is true",
|
|
159
|
+
path: ["enableDynamicRules"]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if ((data.blockedCountries.length > 0 || data.whitelistCountries.length > 0) && !data.geoIpHandler && !data.geoResolver) {
|
|
163
|
+
ctx.addIssue({
|
|
164
|
+
code: "custom",
|
|
165
|
+
message: "geoIpHandler or geoResolver is required when using country filtering",
|
|
166
|
+
path: ["geoIpHandler"]
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// src/models/dynamic-rules.ts
|
|
172
|
+
import { z as z2 } from "zod";
|
|
173
|
+
var VALID_CLOUD_PROVIDERS2 = ["AWS", "GCP", "Azure"];
|
|
174
|
+
var DynamicRulesSchema = z2.object({
|
|
175
|
+
ruleId: z2.string(),
|
|
176
|
+
version: z2.number().int(),
|
|
177
|
+
timestamp: z2.string().datetime(),
|
|
178
|
+
expiresAt: z2.string().datetime().nullable().default(null),
|
|
179
|
+
ttl: z2.number().int().default(300),
|
|
180
|
+
ipBlacklist: z2.array(z2.string()).default([]),
|
|
181
|
+
ipWhitelist: z2.array(z2.string()).default([]),
|
|
182
|
+
ipBanDuration: z2.number().int().default(3600),
|
|
183
|
+
blockedCountries: z2.array(z2.string().length(2)).default([]),
|
|
184
|
+
whitelistCountries: z2.array(z2.string().length(2)).default([]),
|
|
185
|
+
globalRateLimit: z2.number().int().nullable().default(null),
|
|
186
|
+
globalRateWindow: z2.number().int().nullable().default(null),
|
|
187
|
+
endpointRateLimits: z2.record(z2.string(), z2.tuple([z2.number(), z2.number()])).default({}),
|
|
188
|
+
blockedCloudProviders: z2.array(z2.enum(VALID_CLOUD_PROVIDERS2)).default([]).transform((arr) => new Set(arr)),
|
|
189
|
+
blockedUserAgents: z2.array(z2.string()).default([]),
|
|
190
|
+
suspiciousPatterns: z2.array(z2.string()).default([]),
|
|
191
|
+
enablePenetrationDetection: z2.boolean().nullable().default(null),
|
|
192
|
+
enableIpBanning: z2.boolean().nullable().default(null),
|
|
193
|
+
enableRateLimiting: z2.boolean().nullable().default(null),
|
|
194
|
+
emergencyMode: z2.boolean().default(false),
|
|
195
|
+
emergencyWhitelist: z2.array(z2.string()).default([])
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// src/models/logger.ts
|
|
199
|
+
var defaultLogger = {
|
|
200
|
+
info: (msg, ...args) => console.info(`[guard-core] ${msg}`, ...args),
|
|
201
|
+
warn: (msg, ...args) => console.warn(`[guard-core] ${msg}`, ...args),
|
|
202
|
+
error: (msg, ...args) => console.error(`[guard-core] ${msg}`, ...args),
|
|
203
|
+
debug: (msg, ...args) => console.debug(`[guard-core] ${msg}`, ...args)
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/models/route-config.ts
|
|
207
|
+
var RouteConfig = class {
|
|
208
|
+
rateLimit = null;
|
|
209
|
+
rateLimitWindow = null;
|
|
210
|
+
ipWhitelist = null;
|
|
211
|
+
ipBlacklist = null;
|
|
212
|
+
blockedCountries = null;
|
|
213
|
+
whitelistCountries = null;
|
|
214
|
+
bypassedChecks = /* @__PURE__ */ new Set();
|
|
215
|
+
requireHttps = false;
|
|
216
|
+
authRequired = null;
|
|
217
|
+
customValidators = [];
|
|
218
|
+
blockedUserAgents = [];
|
|
219
|
+
requiredHeaders = {};
|
|
220
|
+
behaviorRules = [];
|
|
221
|
+
blockCloudProviders = /* @__PURE__ */ new Set();
|
|
222
|
+
maxRequestSize = null;
|
|
223
|
+
allowedContentTypes = null;
|
|
224
|
+
timeRestrictions = null;
|
|
225
|
+
enableSuspiciousDetection = true;
|
|
226
|
+
requireReferrer = null;
|
|
227
|
+
apiKeyRequired = false;
|
|
228
|
+
sessionLimits = null;
|
|
229
|
+
geoRateLimits = null;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/core/events/event-bus.ts
|
|
233
|
+
var SecurityEventBus = class {
|
|
234
|
+
constructor(agentHandler, config, logger, geoIpHandler = null) {
|
|
235
|
+
this.agentHandler = agentHandler;
|
|
236
|
+
this.config = config;
|
|
237
|
+
this.logger = logger;
|
|
238
|
+
this.geoIpHandler = geoIpHandler;
|
|
239
|
+
}
|
|
240
|
+
async sendMiddlewareEvent(eventType, request, actionTaken, reason, metadata) {
|
|
241
|
+
if (!this.agentHandler || !this.config.agentEnableEvents) return;
|
|
242
|
+
try {
|
|
243
|
+
const clientIp = request.clientHost ?? "unknown";
|
|
244
|
+
let country = null;
|
|
245
|
+
if (this.geoIpHandler) {
|
|
246
|
+
try {
|
|
247
|
+
country = this.geoIpHandler.getCountry(clientIp);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await this.agentHandler.sendEvent({
|
|
252
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
253
|
+
eventType,
|
|
254
|
+
ipAddress: clientIp,
|
|
255
|
+
country,
|
|
256
|
+
userAgent: request.headers["user-agent"] ?? null,
|
|
257
|
+
actionTaken,
|
|
258
|
+
reason,
|
|
259
|
+
endpoint: request.urlPath,
|
|
260
|
+
method: request.method,
|
|
261
|
+
metadata: metadata ?? {}
|
|
262
|
+
});
|
|
263
|
+
} catch (e) {
|
|
264
|
+
this.logger.error(`Failed to send security event: ${e}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async sendHttpsViolationEvent(request, isRouteSpecific) {
|
|
268
|
+
const httpsUrl = request.urlReplaceScheme("https");
|
|
269
|
+
if (isRouteSpecific) {
|
|
270
|
+
await this.sendMiddlewareEvent(
|
|
271
|
+
"decorator_violation",
|
|
272
|
+
request,
|
|
273
|
+
"https_redirect",
|
|
274
|
+
"Route requires HTTPS but request was HTTP",
|
|
275
|
+
{ decoratorType: "authentication", violationType: "require_https", redirectUrl: httpsUrl }
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
await this.sendMiddlewareEvent(
|
|
279
|
+
"https_enforced",
|
|
280
|
+
request,
|
|
281
|
+
"https_redirect",
|
|
282
|
+
"HTTP request redirected to HTTPS for security",
|
|
283
|
+
{ originalScheme: request.urlScheme, redirectUrl: httpsUrl }
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async sendCloudDetectionEvents(request, clientIp, providers, passiveMode) {
|
|
288
|
+
await this.sendMiddlewareEvent(
|
|
289
|
+
"cloud_detection",
|
|
290
|
+
request,
|
|
291
|
+
/* v8 ignore next -- V8 cannot track ternary branch coverage inside string template literal */
|
|
292
|
+
passiveMode ? "logged_only" : "request_blocked",
|
|
293
|
+
`Cloud provider IP ${clientIp} detected`,
|
|
294
|
+
{ blockedProviders: providers }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/core/events/metrics.ts
|
|
300
|
+
var MetricsCollector = class {
|
|
301
|
+
constructor(agentHandler, config, logger) {
|
|
302
|
+
this.agentHandler = agentHandler;
|
|
303
|
+
this.config = config;
|
|
304
|
+
this.logger = logger;
|
|
305
|
+
}
|
|
306
|
+
async sendMetric(metricType, value, tags) {
|
|
307
|
+
if (!this.agentHandler || !this.config.agentEnableMetrics) return;
|
|
308
|
+
try {
|
|
309
|
+
await this.agentHandler.sendMetric({
|
|
310
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
311
|
+
metricType,
|
|
312
|
+
value,
|
|
313
|
+
tags: tags ?? {}
|
|
314
|
+
});
|
|
315
|
+
} catch (e) {
|
|
316
|
+
this.logger.error(`Failed to send metric: ${e}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async collectRequestMetrics(request, responseTime, statusCode) {
|
|
320
|
+
if (!this.agentHandler || !this.config.agentEnableMetrics) return;
|
|
321
|
+
const endpoint = request.urlPath;
|
|
322
|
+
const method = request.method;
|
|
323
|
+
const tags = { endpoint, method, status: String(statusCode) };
|
|
324
|
+
await this.sendMetric("response_time", responseTime, tags);
|
|
325
|
+
await this.sendMetric("request_count", 1, { endpoint, method });
|
|
326
|
+
if (statusCode >= 400) {
|
|
327
|
+
await this.sendMetric("error_rate", 1, tags);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/handlers/behavior.ts
|
|
333
|
+
var BehaviorTracker = class {
|
|
334
|
+
constructor(config, logger) {
|
|
335
|
+
this.config = config;
|
|
336
|
+
this.logger = logger;
|
|
337
|
+
}
|
|
338
|
+
usageCounts = /* @__PURE__ */ new Map();
|
|
339
|
+
returnPatterns = /* @__PURE__ */ new Map();
|
|
340
|
+
redisHandler = null;
|
|
341
|
+
agentHandler = null;
|
|
342
|
+
async initializeRedis(redisHandler) {
|
|
343
|
+
this.redisHandler = redisHandler;
|
|
344
|
+
}
|
|
345
|
+
async initializeAgent(agentHandler) {
|
|
346
|
+
this.agentHandler = agentHandler;
|
|
347
|
+
}
|
|
348
|
+
async trackEndpointUsage(endpointId, clientIp, rule) {
|
|
349
|
+
const now = Date.now() / 1e3;
|
|
350
|
+
const windowStart = now - rule.window;
|
|
351
|
+
if (!this.usageCounts.has(endpointId)) {
|
|
352
|
+
this.usageCounts.set(endpointId, /* @__PURE__ */ new Map());
|
|
353
|
+
}
|
|
354
|
+
const endpointMap = this.usageCounts.get(endpointId);
|
|
355
|
+
if (!endpointMap.has(clientIp)) {
|
|
356
|
+
endpointMap.set(clientIp, []);
|
|
357
|
+
}
|
|
358
|
+
const timestamps = endpointMap.get(clientIp);
|
|
359
|
+
const validIdx = timestamps.findIndex((t) => t > windowStart);
|
|
360
|
+
if (validIdx > 0) timestamps.splice(0, validIdx);
|
|
361
|
+
else if (validIdx === -1) timestamps.length = 0;
|
|
362
|
+
timestamps.push(now);
|
|
363
|
+
return timestamps.length > rule.threshold;
|
|
364
|
+
}
|
|
365
|
+
async trackReturnPattern(endpointId, clientIp, response, rule) {
|
|
366
|
+
if (!rule.pattern) return false;
|
|
367
|
+
const matched = this.checkResponsePattern(response, rule.pattern);
|
|
368
|
+
if (!matched) return false;
|
|
369
|
+
const now = Date.now() / 1e3;
|
|
370
|
+
const windowStart = now - rule.window;
|
|
371
|
+
const key = `${endpointId}:${rule.pattern}`;
|
|
372
|
+
if (!this.returnPatterns.has(key)) {
|
|
373
|
+
this.returnPatterns.set(key, /* @__PURE__ */ new Map());
|
|
374
|
+
}
|
|
375
|
+
const patternMap = this.returnPatterns.get(key);
|
|
376
|
+
if (!patternMap.has(clientIp)) {
|
|
377
|
+
patternMap.set(clientIp, []);
|
|
378
|
+
}
|
|
379
|
+
const timestamps = patternMap.get(clientIp);
|
|
380
|
+
const validIdx = timestamps.findIndex((t) => t > windowStart);
|
|
381
|
+
if (validIdx > 0) timestamps.splice(0, validIdx);
|
|
382
|
+
else if (validIdx === -1) timestamps.length = 0;
|
|
383
|
+
timestamps.push(now);
|
|
384
|
+
return timestamps.length > rule.threshold;
|
|
385
|
+
}
|
|
386
|
+
checkResponsePattern(response, pattern) {
|
|
387
|
+
if (pattern.startsWith("status:")) {
|
|
388
|
+
const code = parseInt(pattern.slice(7), 10);
|
|
389
|
+
return response.statusCode === code;
|
|
390
|
+
}
|
|
391
|
+
if (pattern.startsWith("regex:")) {
|
|
392
|
+
const re = new RegExp(pattern.slice(6), "i");
|
|
393
|
+
return response.bodyText ? re.test(response.bodyText) : false;
|
|
394
|
+
}
|
|
395
|
+
if (pattern.startsWith("json:")) {
|
|
396
|
+
if (!response.bodyText) return false;
|
|
397
|
+
try {
|
|
398
|
+
const data = JSON.parse(response.bodyText);
|
|
399
|
+
const path = pattern.slice(5);
|
|
400
|
+
const parts = path.split(".");
|
|
401
|
+
let current = data;
|
|
402
|
+
for (const part of parts) {
|
|
403
|
+
if (current === null || current === void 0) return false;
|
|
404
|
+
current = current[part];
|
|
405
|
+
}
|
|
406
|
+
return current !== void 0 && current !== null;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return response.bodyText ? response.bodyText.includes(pattern) : false;
|
|
412
|
+
}
|
|
413
|
+
async applyAction(rule, clientIp, endpointId, details) {
|
|
414
|
+
if (this.config.passiveMode) {
|
|
415
|
+
this.logger.info(`[PASSIVE] Would ${rule.action} ${clientIp} for ${details}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
switch (rule.action) {
|
|
419
|
+
case "ban":
|
|
420
|
+
this.logger.warn(`Behavioral ban: ${clientIp} - ${details}`);
|
|
421
|
+
break;
|
|
422
|
+
case "log":
|
|
423
|
+
this.logger.info(`Behavioral log: ${clientIp} - ${details}`);
|
|
424
|
+
break;
|
|
425
|
+
case "throttle":
|
|
426
|
+
this.logger.info(`Behavioral throttle: ${clientIp} - ${details}`);
|
|
427
|
+
break;
|
|
428
|
+
case "alert":
|
|
429
|
+
this.logger.warn(`Behavioral alert: ${clientIp} - ${details}`);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
if (rule.customAction) {
|
|
433
|
+
try {
|
|
434
|
+
rule.customAction(rule.action, clientIp, endpointId, details);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (this.agentHandler) {
|
|
439
|
+
try {
|
|
440
|
+
await this.agentHandler.sendEvent({
|
|
441
|
+
eventType: "behavioral_action",
|
|
442
|
+
ipAddress: clientIp,
|
|
443
|
+
actionTaken: rule.action,
|
|
444
|
+
reason: details,
|
|
445
|
+
metadata: { endpointId, ruleType: rule.ruleType, threshold: rule.threshold }
|
|
446
|
+
});
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async reset() {
|
|
452
|
+
this.usageCounts.clear();
|
|
453
|
+
this.returnPatterns.clear();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/handlers/dynamic-rules.ts
|
|
458
|
+
var DynamicRuleManager = class {
|
|
459
|
+
constructor(config, logger) {
|
|
460
|
+
this.config = config;
|
|
461
|
+
this.logger = logger;
|
|
462
|
+
}
|
|
463
|
+
currentRules = null;
|
|
464
|
+
updateTimer = null;
|
|
465
|
+
lastUpdate = 0;
|
|
466
|
+
agentHandler = null;
|
|
467
|
+
redisHandler = null;
|
|
468
|
+
async initializeAgent(agentHandler) {
|
|
469
|
+
this.agentHandler = agentHandler;
|
|
470
|
+
if (this.config.enableDynamicRules) {
|
|
471
|
+
this.startUpdateLoop();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async initializeRedis(redisHandler) {
|
|
475
|
+
this.redisHandler = redisHandler;
|
|
476
|
+
}
|
|
477
|
+
startUpdateLoop() {
|
|
478
|
+
if (this.updateTimer) return;
|
|
479
|
+
this.updateTimer = setInterval(
|
|
480
|
+
() => {
|
|
481
|
+
this.updateRules().catch((e) => this.logger.error(`Rule update failed: ${e}`));
|
|
482
|
+
},
|
|
483
|
+
this.config.dynamicRuleInterval * 1e3
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
async updateRules() {
|
|
487
|
+
if (!this.agentHandler) return;
|
|
488
|
+
try {
|
|
489
|
+
const rawRules = await this.agentHandler.getDynamicRules();
|
|
490
|
+
if (!rawRules) return;
|
|
491
|
+
const parsed = DynamicRulesSchema.safeParse(rawRules);
|
|
492
|
+
if (!parsed.success) {
|
|
493
|
+
this.logger.warn(`Invalid dynamic rules: ${parsed.error.message}`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const rules = parsed.data;
|
|
497
|
+
if (this.currentRules && this.currentRules.ruleId === rules.ruleId && this.currentRules.version >= rules.version) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.currentRules = rules;
|
|
501
|
+
this.lastUpdate = Date.now() / 1e3;
|
|
502
|
+
this.logger.info(`Applied dynamic rules: ${rules.ruleId} v${rules.version}`);
|
|
503
|
+
if (this.agentHandler) {
|
|
504
|
+
try {
|
|
505
|
+
await this.agentHandler.sendEvent({
|
|
506
|
+
eventType: "dynamic_rule_applied",
|
|
507
|
+
ipAddress: "system",
|
|
508
|
+
actionTaken: "rules_updated",
|
|
509
|
+
reason: `Applied rules ${rules.ruleId} v${rules.version}`
|
|
510
|
+
});
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch (e) {
|
|
515
|
+
this.logger.error(`Failed to fetch dynamic rules: ${e}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
getCurrentRules() {
|
|
519
|
+
return this.currentRules;
|
|
520
|
+
}
|
|
521
|
+
async forceUpdate() {
|
|
522
|
+
await this.updateRules();
|
|
523
|
+
}
|
|
524
|
+
async stop() {
|
|
525
|
+
if (this.updateTimer) {
|
|
526
|
+
clearInterval(this.updateTimer);
|
|
527
|
+
this.updateTimer = null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// src/handlers/ip-ban.ts
|
|
533
|
+
var IPBanManager = class {
|
|
534
|
+
constructor(logger) {
|
|
535
|
+
this.logger = logger;
|
|
536
|
+
}
|
|
537
|
+
bannedIps = /* @__PURE__ */ new Map();
|
|
538
|
+
redisHandler = null;
|
|
539
|
+
agentHandler = null;
|
|
540
|
+
maxSize = 1e4;
|
|
541
|
+
async initializeRedis(redisHandler) {
|
|
542
|
+
this.redisHandler = redisHandler;
|
|
543
|
+
}
|
|
544
|
+
async initializeAgent(agentHandler) {
|
|
545
|
+
this.agentHandler = agentHandler;
|
|
546
|
+
}
|
|
547
|
+
async banIp(ip, duration, reason) {
|
|
548
|
+
const now = Date.now() / 1e3;
|
|
549
|
+
const expiresAt = now + duration;
|
|
550
|
+
if (this.bannedIps.size >= this.maxSize) {
|
|
551
|
+
const oldestKey = this.bannedIps.keys().next().value;
|
|
552
|
+
if (oldestKey) this.bannedIps.delete(oldestKey);
|
|
553
|
+
}
|
|
554
|
+
this.bannedIps.set(ip, { expiresAt, reason, bannedAt: now });
|
|
555
|
+
if (this.redisHandler) {
|
|
556
|
+
await this.redisHandler.setKey("banned_ips", ip, String(expiresAt), duration);
|
|
557
|
+
}
|
|
558
|
+
if (this.agentHandler) {
|
|
559
|
+
try {
|
|
560
|
+
await this.agentHandler.sendEvent({
|
|
561
|
+
eventType: "ip_banned",
|
|
562
|
+
ipAddress: ip,
|
|
563
|
+
actionTaken: "ip_banned",
|
|
564
|
+
reason,
|
|
565
|
+
metadata: { duration, expiresAt }
|
|
566
|
+
});
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
this.logger.info(`IP banned: ${ip} for ${duration}s - ${reason}`);
|
|
571
|
+
}
|
|
572
|
+
async isIpBanned(ip) {
|
|
573
|
+
const now = Date.now() / 1e3;
|
|
574
|
+
const entry = this.bannedIps.get(ip);
|
|
575
|
+
if (entry) {
|
|
576
|
+
if (now <= entry.expiresAt) return true;
|
|
577
|
+
this.bannedIps.delete(ip);
|
|
578
|
+
}
|
|
579
|
+
if (this.redisHandler) {
|
|
580
|
+
const expiryStr = await this.redisHandler.getKey("banned_ips", ip);
|
|
581
|
+
if (typeof expiryStr === "string") {
|
|
582
|
+
const expiresAt = parseFloat(expiryStr);
|
|
583
|
+
if (now <= expiresAt) {
|
|
584
|
+
this.bannedIps.set(ip, {
|
|
585
|
+
expiresAt,
|
|
586
|
+
reason: "restored_from_redis",
|
|
587
|
+
bannedAt: now
|
|
588
|
+
});
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
await this.redisHandler.delete("banned_ips", ip);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
async unbanIp(ip) {
|
|
597
|
+
this.bannedIps.delete(ip);
|
|
598
|
+
if (this.redisHandler) {
|
|
599
|
+
await this.redisHandler.delete("banned_ips", ip);
|
|
600
|
+
}
|
|
601
|
+
if (this.agentHandler) {
|
|
602
|
+
try {
|
|
603
|
+
await this.agentHandler.sendEvent({
|
|
604
|
+
eventType: "ip_unbanned",
|
|
605
|
+
ipAddress: ip,
|
|
606
|
+
actionTaken: "ip_unbanned",
|
|
607
|
+
reason: "Manual unban"
|
|
608
|
+
});
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
this.logger.info(`IP unbanned: ${ip}`);
|
|
613
|
+
}
|
|
614
|
+
async reset() {
|
|
615
|
+
this.bannedIps.clear();
|
|
616
|
+
if (this.redisHandler) {
|
|
617
|
+
await this.redisHandler.deletePattern("banned_ips:*");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// src/handlers/rate-limit.ts
|
|
623
|
+
var RATE_LIMIT_SCRIPT = `
|
|
624
|
+
local key = KEYS[1]
|
|
625
|
+
local now = tonumber(ARGV[1])
|
|
626
|
+
local window = tonumber(ARGV[2])
|
|
627
|
+
local limit = tonumber(ARGV[3])
|
|
628
|
+
local window_start = now - window
|
|
629
|
+
|
|
630
|
+
redis.call('ZADD', key, now, now)
|
|
631
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
|
|
632
|
+
local count = redis.call('ZCARD', key)
|
|
633
|
+
redis.call('EXPIRE', key, window * 2)
|
|
634
|
+
|
|
635
|
+
return count
|
|
636
|
+
`;
|
|
637
|
+
var RateLimitManager = class {
|
|
638
|
+
constructor(logger) {
|
|
639
|
+
this.logger = logger;
|
|
640
|
+
}
|
|
641
|
+
requestTimestamps = /* @__PURE__ */ new Map();
|
|
642
|
+
redisHandler = null;
|
|
643
|
+
agentHandler = null;
|
|
644
|
+
rateLimitScriptSha = null;
|
|
645
|
+
async initializeRedis(redisHandler) {
|
|
646
|
+
this.redisHandler = redisHandler;
|
|
647
|
+
const client = redisHandler.getRawClient();
|
|
648
|
+
if (client) {
|
|
649
|
+
try {
|
|
650
|
+
this.rateLimitScriptSha = await client.script("load", RATE_LIMIT_SCRIPT);
|
|
651
|
+
} catch (e) {
|
|
652
|
+
this.logger.warn(`Failed to load rate limit Lua script: ${e}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async initializeAgent(agentHandler) {
|
|
657
|
+
this.agentHandler = agentHandler;
|
|
658
|
+
}
|
|
659
|
+
async checkRateLimit(request, clientIp, createErrorResponse, endpointPath = null, rateLimit = 10, rateLimitWindow = 60) {
|
|
660
|
+
const key = endpointPath ? `${clientIp}:${endpointPath}` : clientIp;
|
|
661
|
+
const now = Date.now() / 1e3;
|
|
662
|
+
let count = null;
|
|
663
|
+
if (this.redisHandler) {
|
|
664
|
+
count = await this.getRedisRequestCount(key, now, rateLimitWindow, rateLimit);
|
|
665
|
+
}
|
|
666
|
+
if (count === null) {
|
|
667
|
+
count = this.getInMemoryRequestCount(key, now, rateLimitWindow);
|
|
668
|
+
}
|
|
669
|
+
if (count > rateLimit) {
|
|
670
|
+
return this.handleRateLimitExceeded(
|
|
671
|
+
request,
|
|
672
|
+
clientIp,
|
|
673
|
+
count,
|
|
674
|
+
createErrorResponse,
|
|
675
|
+
rateLimitWindow
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
async getRedisRequestCount(key, now, window, _limit) {
|
|
681
|
+
const client = this.redisHandler?.getRawClient();
|
|
682
|
+
if (!client) return null;
|
|
683
|
+
const redisKey = `rate_limit:rate:${key}`;
|
|
684
|
+
const prefix = this.redisHandler["prefix"];
|
|
685
|
+
const fullKey = `${prefix}${redisKey}`;
|
|
686
|
+
try {
|
|
687
|
+
if (this.rateLimitScriptSha) {
|
|
688
|
+
const count2 = await client.evalsha(
|
|
689
|
+
this.rateLimitScriptSha,
|
|
690
|
+
1,
|
|
691
|
+
fullKey,
|
|
692
|
+
now,
|
|
693
|
+
window,
|
|
694
|
+
_limit
|
|
695
|
+
);
|
|
696
|
+
return Number(count2);
|
|
697
|
+
}
|
|
698
|
+
await client.zadd(fullKey, now, String(now));
|
|
699
|
+
await client.zremrangebyscore(fullKey, 0, now - window);
|
|
700
|
+
const count = await client.zcard(fullKey);
|
|
701
|
+
await client.eval('redis.call("EXPIRE", KEYS[1], ARGV[1])', 1, fullKey, window * 2);
|
|
702
|
+
return count;
|
|
703
|
+
} catch (e) {
|
|
704
|
+
this.logger.warn(`Redis rate limit check failed, falling back to in-memory: ${e}`);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
getInMemoryRequestCount(key, now, window) {
|
|
709
|
+
let timestamps = this.requestTimestamps.get(key);
|
|
710
|
+
if (!timestamps) {
|
|
711
|
+
timestamps = [];
|
|
712
|
+
this.requestTimestamps.set(key, timestamps);
|
|
713
|
+
}
|
|
714
|
+
const windowStart = now - window;
|
|
715
|
+
const validIndex = timestamps.findIndex((t) => t > windowStart);
|
|
716
|
+
if (validIndex > 0) {
|
|
717
|
+
timestamps.splice(0, validIndex);
|
|
718
|
+
} else if (validIndex === -1) {
|
|
719
|
+
timestamps.length = 0;
|
|
720
|
+
}
|
|
721
|
+
timestamps.push(now);
|
|
722
|
+
return timestamps.length;
|
|
723
|
+
}
|
|
724
|
+
async handleRateLimitExceeded(request, clientIp, count, createErrorResponse, window) {
|
|
725
|
+
this.logger.warn(`Rate limit exceeded for ${clientIp}: ${count} requests`);
|
|
726
|
+
if (this.agentHandler) {
|
|
727
|
+
try {
|
|
728
|
+
await this.agentHandler.sendEvent({
|
|
729
|
+
eventType: "rate_limit_exceeded",
|
|
730
|
+
ipAddress: clientIp,
|
|
731
|
+
actionTaken: "request_blocked",
|
|
732
|
+
reason: `Rate limit exceeded: ${count} requests in ${window}s window`,
|
|
733
|
+
metadata: {
|
|
734
|
+
endpoint: request.urlPath,
|
|
735
|
+
method: request.method,
|
|
736
|
+
requestCount: count,
|
|
737
|
+
window
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return createErrorResponse(429, "Rate limit exceeded");
|
|
744
|
+
}
|
|
745
|
+
async reset() {
|
|
746
|
+
this.requestTimestamps.clear();
|
|
747
|
+
if (this.redisHandler) {
|
|
748
|
+
await this.redisHandler.deletePattern("rate_limit:rate:*");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/handlers/redis.ts
|
|
754
|
+
var RedisManager = class {
|
|
755
|
+
constructor(config, logger) {
|
|
756
|
+
this.config = config;
|
|
757
|
+
this.logger = logger;
|
|
758
|
+
this.prefix = config.redisPrefix;
|
|
759
|
+
}
|
|
760
|
+
client = null;
|
|
761
|
+
closed = false;
|
|
762
|
+
agentHandler = null;
|
|
763
|
+
prefix;
|
|
764
|
+
async initialize() {
|
|
765
|
+
if (!this.config.enableRedis || this.closed) return;
|
|
766
|
+
try {
|
|
767
|
+
const { default: Redis } = await import("ioredis");
|
|
768
|
+
this.client = new Redis(this.config.redisUrl);
|
|
769
|
+
await this.client.ping();
|
|
770
|
+
this.logger.info("Redis connection established");
|
|
771
|
+
} catch (e) {
|
|
772
|
+
this.logger.error(`Redis connection failed: ${e}`);
|
|
773
|
+
this.client = null;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async close() {
|
|
777
|
+
this.closed = true;
|
|
778
|
+
if (this.client) {
|
|
779
|
+
try {
|
|
780
|
+
await this.client.quit();
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
this.client = null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async initializeAgent(agentHandler) {
|
|
787
|
+
this.agentHandler = agentHandler;
|
|
788
|
+
}
|
|
789
|
+
/* v8 ignore start -- getConnection returns pooled disposable; V8 cannot track inline Symbol.asyncDispose */
|
|
790
|
+
getConnection() {
|
|
791
|
+
const client = this.client;
|
|
792
|
+
return {
|
|
793
|
+
[Symbol.asyncDispose]: async () => {
|
|
794
|
+
},
|
|
795
|
+
get client() {
|
|
796
|
+
return client;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
/* v8 ignore stop */
|
|
801
|
+
formatKey(namespace, key) {
|
|
802
|
+
return `${this.prefix}${namespace}:${key}`;
|
|
803
|
+
}
|
|
804
|
+
async getKey(namespace, key) {
|
|
805
|
+
if (!this.client) return null;
|
|
806
|
+
try {
|
|
807
|
+
return await this.client.get(this.formatKey(namespace, key));
|
|
808
|
+
} catch (e) {
|
|
809
|
+
this.logger.error(`Redis get failed: ${e}`);
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async setKey(namespace, key, value, ttl) {
|
|
814
|
+
if (!this.client) return null;
|
|
815
|
+
try {
|
|
816
|
+
const fullKey = this.formatKey(namespace, key);
|
|
817
|
+
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
818
|
+
if (ttl && ttl > 0) {
|
|
819
|
+
await this.client.setex(fullKey, ttl, strValue);
|
|
820
|
+
} else {
|
|
821
|
+
await this.client.set(fullKey, strValue);
|
|
822
|
+
}
|
|
823
|
+
return true;
|
|
824
|
+
} catch (e) {
|
|
825
|
+
this.logger.error(`Redis set failed: ${e}`);
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async incr(namespace, key, ttl) {
|
|
830
|
+
if (!this.client) return null;
|
|
831
|
+
try {
|
|
832
|
+
const fullKey = this.formatKey(namespace, key);
|
|
833
|
+
const count = await this.client.incr(fullKey);
|
|
834
|
+
if (ttl && ttl > 0) {
|
|
835
|
+
await this.client.expire(fullKey, ttl);
|
|
836
|
+
}
|
|
837
|
+
return count;
|
|
838
|
+
} catch (e) {
|
|
839
|
+
this.logger.error(`Redis incr failed: ${e}`);
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async exists(namespace, key) {
|
|
844
|
+
if (!this.client) return null;
|
|
845
|
+
try {
|
|
846
|
+
const result = await this.client.exists(this.formatKey(namespace, key));
|
|
847
|
+
return result > 0;
|
|
848
|
+
} catch (e) {
|
|
849
|
+
this.logger.error(`Redis exists failed: ${e}`);
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async delete(namespace, key) {
|
|
854
|
+
if (!this.client) return null;
|
|
855
|
+
try {
|
|
856
|
+
return await this.client.del(this.formatKey(namespace, key));
|
|
857
|
+
} catch (e) {
|
|
858
|
+
this.logger.error(`Redis delete failed: ${e}`);
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async keys(pattern) {
|
|
863
|
+
if (!this.client) return null;
|
|
864
|
+
try {
|
|
865
|
+
return await this.client.keys(`${this.prefix}${pattern}`);
|
|
866
|
+
} catch (e) {
|
|
867
|
+
this.logger.error(`Redis keys failed: ${e}`);
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async deletePattern(pattern) {
|
|
872
|
+
if (!this.client) return null;
|
|
873
|
+
try {
|
|
874
|
+
const matchedKeys = await this.client.keys(`${this.prefix}${pattern}`);
|
|
875
|
+
if (matchedKeys.length === 0) return 0;
|
|
876
|
+
return await this.client.del(...matchedKeys);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
this.logger.error(`Redis deletePattern failed: ${e}`);
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
getRawClient() {
|
|
883
|
+
return this.client;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
// src/handlers/security-headers.ts
|
|
888
|
+
var DEFAULT_HEADERS = {
|
|
889
|
+
"X-Content-Type-Options": "nosniff",
|
|
890
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
891
|
+
"X-XSS-Protection": "1; mode=block",
|
|
892
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
893
|
+
"Permissions-Policy": "geolocation=(), microphone=(), camera=()",
|
|
894
|
+
"X-Permitted-Cross-Domain-Policies": "none",
|
|
895
|
+
"X-Download-Options": "noopen",
|
|
896
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
897
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
898
|
+
"Cross-Origin-Resource-Policy": "same-origin"
|
|
899
|
+
};
|
|
900
|
+
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
901
|
+
function validateHeaderValue(value) {
|
|
902
|
+
if (value.includes("\r") || value.includes("\n")) {
|
|
903
|
+
throw new Error("Header value must not contain CR or LF characters");
|
|
904
|
+
}
|
|
905
|
+
if (value.length > MAX_HEADER_VALUE_LENGTH) {
|
|
906
|
+
throw new Error(`Header value exceeds maximum length of ${MAX_HEADER_VALUE_LENGTH}`);
|
|
907
|
+
}
|
|
908
|
+
return value.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
909
|
+
}
|
|
910
|
+
function generateCacheKey(requestPath) {
|
|
911
|
+
const normalized = requestPath.toLowerCase().replace(/\/+$/, "");
|
|
912
|
+
let hash = 0;
|
|
913
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
914
|
+
hash = (hash << 5) - hash + normalized.charCodeAt(i);
|
|
915
|
+
hash |= 0;
|
|
916
|
+
}
|
|
917
|
+
return String(Math.abs(hash)).padStart(16, "0").slice(0, 16);
|
|
918
|
+
}
|
|
919
|
+
var SecurityHeadersManager = class {
|
|
920
|
+
constructor(logger) {
|
|
921
|
+
this.logger = logger;
|
|
922
|
+
}
|
|
923
|
+
headersCache = /* @__PURE__ */ new Map();
|
|
924
|
+
defaultHeaders = { ...DEFAULT_HEADERS };
|
|
925
|
+
customHeaders = {};
|
|
926
|
+
cspConfig = null;
|
|
927
|
+
hstsConfig = null;
|
|
928
|
+
corsConfig = null;
|
|
929
|
+
redisHandler = null;
|
|
930
|
+
agentHandler = null;
|
|
931
|
+
cacheMaxSize = 1e3;
|
|
932
|
+
cacheTtlMs = 3e5;
|
|
933
|
+
cacheTimestamps = /* @__PURE__ */ new Map();
|
|
934
|
+
async initializeRedis(redisHandler) {
|
|
935
|
+
this.redisHandler = redisHandler;
|
|
936
|
+
await this.loadCachedConfig();
|
|
937
|
+
}
|
|
938
|
+
/* v8 ignore start -- initializeAgent assignment; tested via handler tests but V8 misses when called from mock */
|
|
939
|
+
async initializeAgent(agentHandler) {
|
|
940
|
+
this.agentHandler = agentHandler;
|
|
941
|
+
}
|
|
942
|
+
/* v8 ignore stop */
|
|
943
|
+
async loadCachedConfig() {
|
|
944
|
+
if (!this.redisHandler) return;
|
|
945
|
+
const cspJson = await this.redisHandler.getKey("security_headers", "csp_config");
|
|
946
|
+
if (typeof cspJson === "string") {
|
|
947
|
+
try {
|
|
948
|
+
this.cspConfig = JSON.parse(cspJson);
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const hstsJson = await this.redisHandler.getKey("security_headers", "hsts_config");
|
|
953
|
+
if (typeof hstsJson === "string") {
|
|
954
|
+
try {
|
|
955
|
+
this.hstsConfig = JSON.parse(hstsJson);
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const customJson = await this.redisHandler.getKey("security_headers", "custom_headers");
|
|
960
|
+
if (typeof customJson === "string") {
|
|
961
|
+
try {
|
|
962
|
+
this.customHeaders = JSON.parse(customJson);
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
configure(options) {
|
|
968
|
+
if (options.enabled === false) {
|
|
969
|
+
this.defaultHeaders = {};
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (options.csp) this.cspConfig = options.csp;
|
|
973
|
+
if (options.hstsMaxAge !== void 0) {
|
|
974
|
+
this.hstsConfig = {
|
|
975
|
+
maxAge: options.hstsMaxAge,
|
|
976
|
+
includeSubdomains: options.hstsIncludeSubdomains ?? true,
|
|
977
|
+
preload: options.hstsPreload ?? false
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
if (options.frameOptions) this.defaultHeaders["X-Frame-Options"] = validateHeaderValue(options.frameOptions);
|
|
981
|
+
if (options.contentTypeOptions) this.defaultHeaders["X-Content-Type-Options"] = validateHeaderValue(options.contentTypeOptions);
|
|
982
|
+
if (options.xssProtection) this.defaultHeaders["X-XSS-Protection"] = validateHeaderValue(options.xssProtection);
|
|
983
|
+
if (options.referrerPolicy) this.defaultHeaders["Referrer-Policy"] = validateHeaderValue(options.referrerPolicy);
|
|
984
|
+
if (options.permissionsPolicy) this.defaultHeaders["Permissions-Policy"] = validateHeaderValue(options.permissionsPolicy);
|
|
985
|
+
if (options.customHeaders) {
|
|
986
|
+
for (const [key, value] of Object.entries(options.customHeaders)) {
|
|
987
|
+
this.customHeaders[key] = validateHeaderValue(value);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (options.corsOrigins) {
|
|
991
|
+
this.corsConfig = {
|
|
992
|
+
origins: options.corsOrigins,
|
|
993
|
+
allowCredentials: options.corsAllowCredentials ?? false,
|
|
994
|
+
allowMethods: options.corsAllowMethods ?? ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
995
|
+
allowHeaders: options.corsAllowHeaders ?? ["*"]
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
this.cacheConfiguration();
|
|
999
|
+
}
|
|
1000
|
+
async cacheConfiguration() {
|
|
1001
|
+
if (!this.redisHandler) return;
|
|
1002
|
+
const ttl = 86400;
|
|
1003
|
+
if (this.cspConfig) await this.redisHandler.setKey("security_headers", "csp_config", JSON.stringify(this.cspConfig), ttl);
|
|
1004
|
+
if (this.hstsConfig) await this.redisHandler.setKey("security_headers", "hsts_config", JSON.stringify(this.hstsConfig), ttl);
|
|
1005
|
+
if (Object.keys(this.customHeaders).length > 0) {
|
|
1006
|
+
await this.redisHandler.setKey("security_headers", "custom_headers", JSON.stringify(this.customHeaders), ttl);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
buildCsp() {
|
|
1010
|
+
if (!this.cspConfig) return null;
|
|
1011
|
+
return Object.entries(this.cspConfig).map(([directive, values]) => `${directive} ${values.join(" ")}`).join("; ");
|
|
1012
|
+
}
|
|
1013
|
+
buildHsts() {
|
|
1014
|
+
if (!this.hstsConfig) return null;
|
|
1015
|
+
let header = `max-age=${this.hstsConfig.maxAge}`;
|
|
1016
|
+
if (this.hstsConfig.includeSubdomains) header += "; includeSubDomains";
|
|
1017
|
+
if (this.hstsConfig.preload) header += "; preload";
|
|
1018
|
+
return header;
|
|
1019
|
+
}
|
|
1020
|
+
async getHeaders(requestPath) {
|
|
1021
|
+
const cacheKey = generateCacheKey(requestPath);
|
|
1022
|
+
const now = Date.now();
|
|
1023
|
+
const cachedTimestamp = this.cacheTimestamps.get(cacheKey);
|
|
1024
|
+
if (cachedTimestamp && now - cachedTimestamp < this.cacheTtlMs) {
|
|
1025
|
+
const cached = this.headersCache.get(cacheKey);
|
|
1026
|
+
if (cached) return { ...cached };
|
|
1027
|
+
}
|
|
1028
|
+
const headers = { ...this.defaultHeaders };
|
|
1029
|
+
const csp = this.buildCsp();
|
|
1030
|
+
if (csp) headers["Content-Security-Policy"] = csp;
|
|
1031
|
+
const hsts = this.buildHsts();
|
|
1032
|
+
if (hsts) headers["Strict-Transport-Security"] = hsts;
|
|
1033
|
+
for (const [key, value] of Object.entries(this.customHeaders)) {
|
|
1034
|
+
headers[key] = value;
|
|
1035
|
+
}
|
|
1036
|
+
if (this.headersCache.size >= this.cacheMaxSize) {
|
|
1037
|
+
const oldestKey = this.headersCache.keys().next().value;
|
|
1038
|
+
if (oldestKey) {
|
|
1039
|
+
this.headersCache.delete(oldestKey);
|
|
1040
|
+
this.cacheTimestamps.delete(oldestKey);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
this.headersCache.set(cacheKey, headers);
|
|
1044
|
+
this.cacheTimestamps.set(cacheKey, now);
|
|
1045
|
+
return { ...headers };
|
|
1046
|
+
}
|
|
1047
|
+
getCorsHeaders(origin) {
|
|
1048
|
+
if (!this.corsConfig) return {};
|
|
1049
|
+
const isAllowed = this.corsConfig.origins.includes("*") || this.corsConfig.origins.includes(origin);
|
|
1050
|
+
if (!isAllowed) return {};
|
|
1051
|
+
const headers = {
|
|
1052
|
+
"Access-Control-Allow-Origin": this.corsConfig.origins.includes("*") ? "*" : origin,
|
|
1053
|
+
"Access-Control-Allow-Methods": this.corsConfig.allowMethods.join(", "),
|
|
1054
|
+
"Access-Control-Allow-Headers": this.corsConfig.allowHeaders.join(", ")
|
|
1055
|
+
};
|
|
1056
|
+
if (this.corsConfig.allowCredentials) {
|
|
1057
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
1058
|
+
}
|
|
1059
|
+
return headers;
|
|
1060
|
+
}
|
|
1061
|
+
async reset() {
|
|
1062
|
+
this.headersCache.clear();
|
|
1063
|
+
this.cacheTimestamps.clear();
|
|
1064
|
+
this.defaultHeaders = { ...DEFAULT_HEADERS };
|
|
1065
|
+
this.customHeaders = {};
|
|
1066
|
+
this.cspConfig = null;
|
|
1067
|
+
this.hstsConfig = null;
|
|
1068
|
+
this.corsConfig = null;
|
|
1069
|
+
if (this.redisHandler) {
|
|
1070
|
+
await this.redisHandler.deletePattern("security_headers:*");
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// src/core/initialization/handler-initializer.ts
|
|
1076
|
+
var HandlerInitializer = class {
|
|
1077
|
+
constructor(config, logger, agentHandler = null, geoIpHandler = null, guardDecorator = null) {
|
|
1078
|
+
this.config = config;
|
|
1079
|
+
this.logger = logger;
|
|
1080
|
+
this.agentHandler = agentHandler;
|
|
1081
|
+
this.geoIpHandler = geoIpHandler;
|
|
1082
|
+
this.guardDecorator = guardDecorator;
|
|
1083
|
+
}
|
|
1084
|
+
async initialize() {
|
|
1085
|
+
const ipBanHandler = new IPBanManager(this.logger);
|
|
1086
|
+
const rateLimitHandler = new RateLimitManager(this.logger);
|
|
1087
|
+
const cloudHandler = new CloudHandler(this.logger);
|
|
1088
|
+
const susPatternsHandler = new SusPatternsManager(this.config, this.logger);
|
|
1089
|
+
const securityHeadersHandler = new SecurityHeadersManager(this.logger);
|
|
1090
|
+
const behaviorTracker = new BehaviorTracker(this.config, this.logger);
|
|
1091
|
+
const dynamicRuleHandler = new DynamicRuleManager(this.config, this.logger);
|
|
1092
|
+
let redisHandler = null;
|
|
1093
|
+
if (this.config.enableRedis) {
|
|
1094
|
+
try {
|
|
1095
|
+
redisHandler = new RedisManager(this.config, this.logger);
|
|
1096
|
+
await redisHandler.initialize();
|
|
1097
|
+
await ipBanHandler.initializeRedis(redisHandler);
|
|
1098
|
+
await rateLimitHandler.initializeRedis(redisHandler);
|
|
1099
|
+
await susPatternsHandler.initializeRedis(redisHandler);
|
|
1100
|
+
await securityHeadersHandler.initializeRedis(redisHandler);
|
|
1101
|
+
await behaviorTracker.initializeRedis(redisHandler);
|
|
1102
|
+
await dynamicRuleHandler.initializeRedis(redisHandler);
|
|
1103
|
+
if (this.config.blockCloudProviders.size > 0) {
|
|
1104
|
+
await cloudHandler.initializeRedis(
|
|
1105
|
+
redisHandler,
|
|
1106
|
+
this.config.blockCloudProviders,
|
|
1107
|
+
this.config.cloudIpRefreshInterval
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
if (this.geoIpHandler) {
|
|
1111
|
+
await this.geoIpHandler.initializeRedis(redisHandler);
|
|
1112
|
+
}
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
this.logger.warn(`Redis initialization failed, falling back to in-memory: ${e}`);
|
|
1115
|
+
redisHandler = null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (this.geoIpHandler && !this.geoIpHandler.isInitialized) {
|
|
1119
|
+
await this.geoIpHandler.initialize();
|
|
1120
|
+
}
|
|
1121
|
+
if (this.agentHandler) {
|
|
1122
|
+
await this.initializeAgentIntegrations(
|
|
1123
|
+
ipBanHandler,
|
|
1124
|
+
rateLimitHandler,
|
|
1125
|
+
cloudHandler,
|
|
1126
|
+
susPatternsHandler,
|
|
1127
|
+
dynamicRuleHandler,
|
|
1128
|
+
redisHandler
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
this.configureSecurityHeaders(securityHeadersHandler);
|
|
1132
|
+
return {
|
|
1133
|
+
redisHandler,
|
|
1134
|
+
ipBanHandler,
|
|
1135
|
+
rateLimitHandler,
|
|
1136
|
+
cloudHandler,
|
|
1137
|
+
susPatternsHandler,
|
|
1138
|
+
securityHeadersHandler,
|
|
1139
|
+
behaviorTracker,
|
|
1140
|
+
dynamicRuleHandler,
|
|
1141
|
+
geoIpHandler: this.geoIpHandler
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
async initializeAgentIntegrations(ipBanHandler, rateLimitHandler, cloudHandler, susPatternsHandler, dynamicRuleHandler, redisHandler) {
|
|
1145
|
+
if (!this.agentHandler) return;
|
|
1146
|
+
await this.agentHandler.start();
|
|
1147
|
+
if (redisHandler) {
|
|
1148
|
+
await this.agentHandler.initializeRedis(redisHandler);
|
|
1149
|
+
await redisHandler.initializeAgent(this.agentHandler);
|
|
1150
|
+
}
|
|
1151
|
+
await ipBanHandler.initializeAgent(this.agentHandler);
|
|
1152
|
+
await rateLimitHandler.initializeAgent(this.agentHandler);
|
|
1153
|
+
await susPatternsHandler.initializeAgent(this.agentHandler);
|
|
1154
|
+
if (this.config.blockCloudProviders.size > 0) {
|
|
1155
|
+
await cloudHandler.initializeAgent(this.agentHandler);
|
|
1156
|
+
}
|
|
1157
|
+
if (this.geoIpHandler) {
|
|
1158
|
+
await this.geoIpHandler.initializeAgent(this.agentHandler);
|
|
1159
|
+
}
|
|
1160
|
+
if (this.config.enableDynamicRules) {
|
|
1161
|
+
await dynamicRuleHandler.initializeAgent(this.agentHandler);
|
|
1162
|
+
}
|
|
1163
|
+
if (this.guardDecorator && typeof this.guardDecorator["initializeAgent"] === "function") {
|
|
1164
|
+
await this.guardDecorator.initializeAgent(this.agentHandler);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
configureSecurityHeaders(manager) {
|
|
1168
|
+
const headers = this.config.securityHeaders;
|
|
1169
|
+
if (!headers) return;
|
|
1170
|
+
manager.configure({
|
|
1171
|
+
enabled: headers.enabled,
|
|
1172
|
+
csp: headers.csp,
|
|
1173
|
+
hstsMaxAge: headers.hsts?.maxAge,
|
|
1174
|
+
hstsIncludeSubdomains: headers.hsts?.includeSubdomains,
|
|
1175
|
+
hstsPreload: headers.hsts?.preload,
|
|
1176
|
+
frameOptions: headers.frameOptions,
|
|
1177
|
+
contentTypeOptions: headers.contentTypeOptions,
|
|
1178
|
+
xssProtection: headers.xssProtection,
|
|
1179
|
+
referrerPolicy: headers.referrerPolicy,
|
|
1180
|
+
permissionsPolicy: headers.permissionsPolicy,
|
|
1181
|
+
customHeaders: headers.custom ?? void 0,
|
|
1182
|
+
corsOrigins: this.config.enableCors ? this.config.corsAllowOrigins : void 0,
|
|
1183
|
+
corsAllowCredentials: this.config.corsAllowCredentials,
|
|
1184
|
+
corsAllowMethods: this.config.corsAllowMethods,
|
|
1185
|
+
corsAllowHeaders: this.config.corsAllowHeaders
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
// src/core/validation/validator.ts
|
|
1191
|
+
import * as ipaddr2 from "ipaddr.js";
|
|
1192
|
+
var RequestValidator = class {
|
|
1193
|
+
constructor(config, logger, eventBus) {
|
|
1194
|
+
this.config = config;
|
|
1195
|
+
this.logger = logger;
|
|
1196
|
+
this.eventBus = eventBus;
|
|
1197
|
+
}
|
|
1198
|
+
isRequestHttps(request) {
|
|
1199
|
+
let isHttps = request.urlScheme === "https";
|
|
1200
|
+
if (this.config.trustXForwardedProto && this.config.trustedProxies.length > 0 && request.clientHost) {
|
|
1201
|
+
if (this.isTrustedProxy(request.clientHost)) {
|
|
1202
|
+
const forwardedProto = request.headers["x-forwarded-proto"] ?? "";
|
|
1203
|
+
isHttps = isHttps || forwardedProto.toLowerCase() === "https";
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return isHttps;
|
|
1207
|
+
}
|
|
1208
|
+
isTrustedProxy(connectingIp) {
|
|
1209
|
+
for (const proxy of this.config.trustedProxies) {
|
|
1210
|
+
if (!proxy.includes("/")) {
|
|
1211
|
+
if (connectingIp === proxy) return true;
|
|
1212
|
+
} else {
|
|
1213
|
+
try {
|
|
1214
|
+
const parsed = ipaddr2.parse(connectingIp);
|
|
1215
|
+
const [addr, prefixLen] = ipaddr2.parseCIDR(proxy);
|
|
1216
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
1217
|
+
} catch {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
async checkTimeWindow(timeRestrictions) {
|
|
1225
|
+
try {
|
|
1226
|
+
const { start, end } = timeRestrictions;
|
|
1227
|
+
const now = /* @__PURE__ */ new Date();
|
|
1228
|
+
const currentTime = now.toISOString().slice(11, 16);
|
|
1229
|
+
if (start > end) {
|
|
1230
|
+
return currentTime >= start || currentTime <= end;
|
|
1231
|
+
}
|
|
1232
|
+
return currentTime >= start && currentTime <= end;
|
|
1233
|
+
} catch (e) {
|
|
1234
|
+
this.logger.error(`Error checking time window: ${e}`);
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async isPathExcluded(request) {
|
|
1239
|
+
const excluded = this.config.excludePaths.some(
|
|
1240
|
+
(path) => request.urlPath.startsWith(path)
|
|
1241
|
+
);
|
|
1242
|
+
if (excluded) {
|
|
1243
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
1244
|
+
"path_excluded",
|
|
1245
|
+
request,
|
|
1246
|
+
"security_checks_bypassed",
|
|
1247
|
+
`Path ${request.urlPath} excluded from security checks`,
|
|
1248
|
+
{ excludedPath: request.urlPath, configuredExclusions: this.config.excludePaths }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return excluded;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// src/core/routing/resolver.ts
|
|
1256
|
+
var RouteConfigResolver = class {
|
|
1257
|
+
constructor(config) {
|
|
1258
|
+
this.config = config;
|
|
1259
|
+
}
|
|
1260
|
+
guardDecorator = null;
|
|
1261
|
+
setGuardDecorator(decorator) {
|
|
1262
|
+
this.guardDecorator = decorator;
|
|
1263
|
+
}
|
|
1264
|
+
getRouteConfig(request) {
|
|
1265
|
+
const decorator = this.guardDecorator ?? request.state.guardDecorator;
|
|
1266
|
+
if (!decorator) return null;
|
|
1267
|
+
const routeId = request.state.guardRouteId;
|
|
1268
|
+
if (!routeId) return null;
|
|
1269
|
+
const getConfig = decorator.getRouteConfig;
|
|
1270
|
+
if (typeof getConfig !== "function") return null;
|
|
1271
|
+
return getConfig.call(decorator, routeId) ?? null;
|
|
1272
|
+
}
|
|
1273
|
+
shouldBypassCheck(checkName, routeConfig) {
|
|
1274
|
+
if (!routeConfig) return false;
|
|
1275
|
+
return routeConfig.bypassedChecks.has(checkName) || routeConfig.bypassedChecks.has("all");
|
|
1276
|
+
}
|
|
1277
|
+
getCloudProvidersToCheck(routeConfig) {
|
|
1278
|
+
if (routeConfig && routeConfig.blockCloudProviders.size > 0) {
|
|
1279
|
+
return [...routeConfig.blockCloudProviders];
|
|
1280
|
+
}
|
|
1281
|
+
if (this.config.blockCloudProviders.size > 0) {
|
|
1282
|
+
return [...this.config.blockCloudProviders];
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// src/core/bypass/handler.ts
|
|
1289
|
+
var BypassHandler = class {
|
|
1290
|
+
constructor(config, eventBus, routeResolver, responseFactory, validator) {
|
|
1291
|
+
this.config = config;
|
|
1292
|
+
this.eventBus = eventBus;
|
|
1293
|
+
this.routeResolver = routeResolver;
|
|
1294
|
+
this.responseFactory = responseFactory;
|
|
1295
|
+
this.validator = validator;
|
|
1296
|
+
}
|
|
1297
|
+
async handlePassthrough(request, callNext) {
|
|
1298
|
+
if (!request.clientHost) {
|
|
1299
|
+
const response = await callNext(request);
|
|
1300
|
+
return this.responseFactory.applyModifier(response);
|
|
1301
|
+
}
|
|
1302
|
+
if (await this.validator.isPathExcluded(request)) {
|
|
1303
|
+
const response = await callNext(request);
|
|
1304
|
+
return this.responseFactory.applyModifier(response);
|
|
1305
|
+
}
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
async handleSecurityBypass(request, callNext, routeConfig) {
|
|
1309
|
+
if (!routeConfig || !this.routeResolver.shouldBypassCheck("all", routeConfig)) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
1313
|
+
"security_bypass",
|
|
1314
|
+
request,
|
|
1315
|
+
"all_checks_bypassed",
|
|
1316
|
+
"Route configured to bypass all security checks",
|
|
1317
|
+
{ bypassedChecks: [...routeConfig.bypassedChecks], endpoint: request.urlPath }
|
|
1318
|
+
);
|
|
1319
|
+
if (!this.config.passiveMode) {
|
|
1320
|
+
const response = await callNext(request);
|
|
1321
|
+
return this.responseFactory.applyModifier(response);
|
|
1322
|
+
}
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
// src/core/responses/factory.ts
|
|
1328
|
+
var ErrorResponseFactory = class {
|
|
1329
|
+
constructor(config, logger, metricsCollector, guardResponseFactory, securityHeadersManager, agentHandler = null) {
|
|
1330
|
+
this.config = config;
|
|
1331
|
+
this.logger = logger;
|
|
1332
|
+
this.metricsCollector = metricsCollector;
|
|
1333
|
+
this.guardResponseFactory = guardResponseFactory;
|
|
1334
|
+
this.securityHeadersManager = securityHeadersManager;
|
|
1335
|
+
this.agentHandler = agentHandler;
|
|
1336
|
+
}
|
|
1337
|
+
async createErrorResponse(statusCode, defaultMessage) {
|
|
1338
|
+
const message = this.config.customErrorResponses[statusCode] ?? defaultMessage;
|
|
1339
|
+
const response = this.guardResponseFactory.createResponse(message, statusCode);
|
|
1340
|
+
await this.applySecurityHeaders(response, void 0);
|
|
1341
|
+
return this.applyModifier(response);
|
|
1342
|
+
}
|
|
1343
|
+
async createHttpsRedirect(request) {
|
|
1344
|
+
const httpsUrl = request.urlReplaceScheme("https");
|
|
1345
|
+
const response = this.guardResponseFactory.createRedirectResponse(httpsUrl, 301);
|
|
1346
|
+
return this.applyModifier(response);
|
|
1347
|
+
}
|
|
1348
|
+
async applySecurityHeaders(response, requestPath) {
|
|
1349
|
+
const headersConfig = this.config.securityHeaders;
|
|
1350
|
+
if (headersConfig && headersConfig.enabled) {
|
|
1351
|
+
const securityHeaders = await this.securityHeadersManager.getHeaders(requestPath ?? "/");
|
|
1352
|
+
for (const [name, value] of Object.entries(securityHeaders)) {
|
|
1353
|
+
response.setHeader(name, value);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return response;
|
|
1357
|
+
}
|
|
1358
|
+
async applyCorsHeaders(response, origin) {
|
|
1359
|
+
const corsHeaders = this.securityHeadersManager.getCorsHeaders(origin);
|
|
1360
|
+
for (const [name, value] of Object.entries(corsHeaders)) {
|
|
1361
|
+
response.setHeader(name, value);
|
|
1362
|
+
}
|
|
1363
|
+
return response;
|
|
1364
|
+
}
|
|
1365
|
+
async applyModifier(response) {
|
|
1366
|
+
if (this.config.customResponseModifier) {
|
|
1367
|
+
return this.config.customResponseModifier(response);
|
|
1368
|
+
}
|
|
1369
|
+
return response;
|
|
1370
|
+
}
|
|
1371
|
+
async processResponse(request, response, responseTime, routeConfig, processBehavioralRules) {
|
|
1372
|
+
if (routeConfig && routeConfig.behaviorRules.length > 0 && processBehavioralRules) {
|
|
1373
|
+
const clientIp = request.clientHost ?? "unknown";
|
|
1374
|
+
await processBehavioralRules(request, response, clientIp, routeConfig);
|
|
1375
|
+
}
|
|
1376
|
+
await this.metricsCollector.collectRequestMetrics(request, responseTime, response.statusCode);
|
|
1377
|
+
await this.applySecurityHeaders(response, request.urlPath);
|
|
1378
|
+
const origin = request.headers["origin"];
|
|
1379
|
+
if (origin) {
|
|
1380
|
+
await this.applyCorsHeaders(response, origin);
|
|
1381
|
+
}
|
|
1382
|
+
return this.applyModifier(response);
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// src/core/behavioral/processor.ts
|
|
1387
|
+
var BehavioralProcessor = class {
|
|
1388
|
+
constructor(logger, eventBus) {
|
|
1389
|
+
this.logger = logger;
|
|
1390
|
+
this.eventBus = eventBus;
|
|
1391
|
+
}
|
|
1392
|
+
guardDecorator = null;
|
|
1393
|
+
setGuardDecorator(decorator) {
|
|
1394
|
+
this.guardDecorator = decorator;
|
|
1395
|
+
}
|
|
1396
|
+
async processUsageRules(request, clientIp, routeConfig) {
|
|
1397
|
+
if (!this.guardDecorator) return;
|
|
1398
|
+
const endpointId = this.getEndpointId(request);
|
|
1399
|
+
const tracker = this.guardDecorator.behaviorTracker;
|
|
1400
|
+
for (const rule of routeConfig.behaviorRules) {
|
|
1401
|
+
if (rule.ruleType === "usage" || rule.ruleType === "frequency") {
|
|
1402
|
+
const exceeded = await tracker.trackEndpointUsage(endpointId, clientIp, rule);
|
|
1403
|
+
if (exceeded) {
|
|
1404
|
+
const details = `${rule.threshold} calls in ${rule.window}s`;
|
|
1405
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
1406
|
+
"decorator_violation",
|
|
1407
|
+
request,
|
|
1408
|
+
"behavioral_action_triggered",
|
|
1409
|
+
`Behavioral ${rule.ruleType} threshold exceeded: ${details}`,
|
|
1410
|
+
{
|
|
1411
|
+
decoratorType: "behavioral",
|
|
1412
|
+
violationType: rule.ruleType,
|
|
1413
|
+
threshold: rule.threshold,
|
|
1414
|
+
window: rule.window,
|
|
1415
|
+
action: rule.action,
|
|
1416
|
+
endpointId
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
1419
|
+
await tracker.applyAction(rule, clientIp, endpointId, `Usage threshold exceeded: ${details}`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async processReturnRules(request, response, clientIp, routeConfig) {
|
|
1425
|
+
if (!this.guardDecorator) return;
|
|
1426
|
+
const endpointId = this.getEndpointId(request);
|
|
1427
|
+
const tracker = this.guardDecorator.behaviorTracker;
|
|
1428
|
+
for (const rule of routeConfig.behaviorRules) {
|
|
1429
|
+
if (rule.ruleType === "return_pattern") {
|
|
1430
|
+
const detected = await tracker.trackReturnPattern(endpointId, clientIp, response, rule);
|
|
1431
|
+
if (detected) {
|
|
1432
|
+
const details = `${rule.threshold} for '${rule.pattern}' in ${rule.window}s`;
|
|
1433
|
+
await this.eventBus.sendMiddlewareEvent(
|
|
1434
|
+
"decorator_violation",
|
|
1435
|
+
request,
|
|
1436
|
+
"behavioral_action_triggered",
|
|
1437
|
+
`Return pattern threshold exceeded: ${details}`,
|
|
1438
|
+
{
|
|
1439
|
+
decoratorType: "behavioral",
|
|
1440
|
+
violationType: "return_pattern",
|
|
1441
|
+
threshold: rule.threshold,
|
|
1442
|
+
window: rule.window,
|
|
1443
|
+
pattern: rule.pattern,
|
|
1444
|
+
action: rule.action,
|
|
1445
|
+
endpointId
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
await tracker.applyAction(rule, clientIp, endpointId, `Return pattern threshold exceeded: ${details}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
getEndpointId(request) {
|
|
1454
|
+
const endpointId = request.state.guardEndpointId;
|
|
1455
|
+
if (typeof endpointId === "string") return endpointId;
|
|
1456
|
+
return `${request.method}:${request.urlPath}`;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
// src/core/checks/pipeline.ts
|
|
1461
|
+
var SecurityCheckPipeline = class {
|
|
1462
|
+
constructor(checks, logger) {
|
|
1463
|
+
this.checks = checks;
|
|
1464
|
+
this.logger = logger;
|
|
1465
|
+
}
|
|
1466
|
+
async execute(request) {
|
|
1467
|
+
for (const check of this.checks) {
|
|
1468
|
+
try {
|
|
1469
|
+
const response = await check.check(request);
|
|
1470
|
+
if (response !== null) return response;
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
this.logger.error(`Security check '${check.checkName}' failed: ${e}`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
add(check) {
|
|
1478
|
+
this.checks.push(check);
|
|
1479
|
+
}
|
|
1480
|
+
insert(index, check) {
|
|
1481
|
+
this.checks.splice(index, 0, check);
|
|
1482
|
+
}
|
|
1483
|
+
remove(name) {
|
|
1484
|
+
const idx = this.checks.findIndex((c) => c.checkName === name);
|
|
1485
|
+
if (idx === -1) return false;
|
|
1486
|
+
this.checks.splice(idx, 1);
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
getCheckNames() {
|
|
1490
|
+
return this.checks.map((c) => c.checkName);
|
|
1491
|
+
}
|
|
1492
|
+
get length() {
|
|
1493
|
+
return this.checks.length;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// src/core/checks/base.ts
|
|
1498
|
+
var SecurityCheck = class {
|
|
1499
|
+
constructor(middleware) {
|
|
1500
|
+
this.middleware = middleware;
|
|
1501
|
+
}
|
|
1502
|
+
get config() {
|
|
1503
|
+
return this.middleware.config;
|
|
1504
|
+
}
|
|
1505
|
+
get logger() {
|
|
1506
|
+
return this.middleware.logger;
|
|
1507
|
+
}
|
|
1508
|
+
async sendEvent(type, request, action, reason, meta) {
|
|
1509
|
+
const eventBus = this.middleware.eventBus;
|
|
1510
|
+
await eventBus.sendMiddlewareEvent(type, request, action, reason, meta);
|
|
1511
|
+
}
|
|
1512
|
+
async createErrorResponse(statusCode, message) {
|
|
1513
|
+
return this.middleware.createErrorResponse(statusCode, message);
|
|
1514
|
+
}
|
|
1515
|
+
isPassiveMode() {
|
|
1516
|
+
return this.config.passiveMode;
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// src/core/checks/implementations/route-config.ts
|
|
1521
|
+
var RouteConfigCheck = class extends SecurityCheck {
|
|
1522
|
+
get checkName() {
|
|
1523
|
+
return "route_config";
|
|
1524
|
+
}
|
|
1525
|
+
async check(request) {
|
|
1526
|
+
const routeResolver = this.middleware.routeResolver;
|
|
1527
|
+
const routeConfig = routeResolver.getRouteConfig(request);
|
|
1528
|
+
if (routeConfig) {
|
|
1529
|
+
request.state["_routeConfig"] = routeConfig;
|
|
1530
|
+
}
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/core/checks/implementations/emergency-mode.ts
|
|
1536
|
+
var EmergencyModeCheck = class extends SecurityCheck {
|
|
1537
|
+
get checkName() {
|
|
1538
|
+
return "emergency_mode";
|
|
1539
|
+
}
|
|
1540
|
+
async check(request) {
|
|
1541
|
+
if (!this.config.emergencyMode) return null;
|
|
1542
|
+
const clientIp = request.clientHost ?? "";
|
|
1543
|
+
if (this.config.emergencyWhitelist.includes(clientIp)) return null;
|
|
1544
|
+
await this.sendEvent("emergency_mode", request, "request_blocked", "Emergency mode active");
|
|
1545
|
+
return this.createErrorResponse(503, "Service temporarily unavailable");
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
// src/core/checks/implementations/https-enforcement.ts
|
|
1550
|
+
var HttpsEnforcementCheck = class extends SecurityCheck {
|
|
1551
|
+
validator;
|
|
1552
|
+
responseFactory;
|
|
1553
|
+
constructor(middleware, validator, responseFactory) {
|
|
1554
|
+
super(middleware);
|
|
1555
|
+
this.validator = validator;
|
|
1556
|
+
this.responseFactory = responseFactory;
|
|
1557
|
+
}
|
|
1558
|
+
get checkName() {
|
|
1559
|
+
return "https_enforcement";
|
|
1560
|
+
}
|
|
1561
|
+
async check(request) {
|
|
1562
|
+
if (!this.config.enforceHttps) return null;
|
|
1563
|
+
if (this.validator.isRequestHttps(request)) return null;
|
|
1564
|
+
if (this.isPassiveMode()) {
|
|
1565
|
+
this.logger.info(`[PASSIVE] Would redirect to HTTPS: ${request.urlPath}`);
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
return this.responseFactory.createHttpsRedirect(request);
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
// src/core/checks/implementations/request-logging.ts
|
|
1573
|
+
var RequestLoggingCheck = class extends SecurityCheck {
|
|
1574
|
+
get checkName() {
|
|
1575
|
+
return "request_logging";
|
|
1576
|
+
}
|
|
1577
|
+
async check(request) {
|
|
1578
|
+
if (this.config.logRequestLevel) {
|
|
1579
|
+
logActivity(request, this.logger, "request", "", false, "", this.config.logRequestLevel);
|
|
1580
|
+
await this.sendEvent("request_logged", request, "logged", "Request logged");
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
// src/core/checks/implementations/request-size-content.ts
|
|
1587
|
+
var RequestSizeContentCheck = class extends SecurityCheck {
|
|
1588
|
+
get checkName() {
|
|
1589
|
+
return "request_size_content";
|
|
1590
|
+
}
|
|
1591
|
+
async check(request) {
|
|
1592
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1593
|
+
if (!routeConfig) return null;
|
|
1594
|
+
if (routeConfig.maxRequestSize !== null) {
|
|
1595
|
+
const contentLength = parseInt(request.headers["content-length"] ?? "0", 10);
|
|
1596
|
+
if (contentLength > routeConfig.maxRequestSize) {
|
|
1597
|
+
if (this.isPassiveMode()) {
|
|
1598
|
+
this.logger.info(`[PASSIVE] Request too large: ${contentLength} > ${routeConfig.maxRequestSize}`);
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
return this.createErrorResponse(413, "Request entity too large");
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (routeConfig.allowedContentTypes !== null) {
|
|
1605
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
1606
|
+
if (contentType && !routeConfig.allowedContentTypes.some((t) => contentType.includes(t))) {
|
|
1607
|
+
if (this.isPassiveMode()) {
|
|
1608
|
+
this.logger.info(`[PASSIVE] Invalid content type: ${contentType}`);
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
return this.createErrorResponse(415, "Unsupported media type");
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
// src/core/checks/implementations/required-headers.ts
|
|
1619
|
+
var RequiredHeadersCheck = class extends SecurityCheck {
|
|
1620
|
+
get checkName() {
|
|
1621
|
+
return "required_headers";
|
|
1622
|
+
}
|
|
1623
|
+
async check(request) {
|
|
1624
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1625
|
+
if (!routeConfig || Object.keys(routeConfig.requiredHeaders).length === 0) return null;
|
|
1626
|
+
for (const [headerName, expectedValue] of Object.entries(routeConfig.requiredHeaders)) {
|
|
1627
|
+
const actualValue = request.headers[headerName.toLowerCase()];
|
|
1628
|
+
if (!actualValue || expectedValue && actualValue !== expectedValue) {
|
|
1629
|
+
if (this.isPassiveMode()) {
|
|
1630
|
+
this.logger.info(`[PASSIVE] Missing required header: ${headerName}`);
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
return this.createErrorResponse(400, `Missing or invalid required header: ${headerName}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
// src/core/checks/helpers.ts
|
|
1641
|
+
import * as ipaddr3 from "ipaddr.js";
|
|
1642
|
+
function isIpInBlacklist(clientIp, blacklist) {
|
|
1643
|
+
for (const blocked of blacklist) {
|
|
1644
|
+
if (blocked.includes("/")) {
|
|
1645
|
+
try {
|
|
1646
|
+
const parsed = ipaddr3.parse(clientIp);
|
|
1647
|
+
const [addr, prefixLen] = ipaddr3.parseCIDR(blocked);
|
|
1648
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
1649
|
+
} catch {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
} else if (clientIp === blocked) {
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
function isIpInWhitelist(clientIp, whitelist) {
|
|
1659
|
+
if (whitelist.length === 0) return null;
|
|
1660
|
+
for (const allowed of whitelist) {
|
|
1661
|
+
if (allowed.includes("/")) {
|
|
1662
|
+
try {
|
|
1663
|
+
const parsed = ipaddr3.parse(clientIp);
|
|
1664
|
+
const [addr, prefixLen] = ipaddr3.parseCIDR(allowed);
|
|
1665
|
+
if (parsed.kind() === addr.kind() && parsed.match([addr, prefixLen])) return true;
|
|
1666
|
+
} catch {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
} else if (clientIp === allowed) {
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
function checkCountryAccess(clientIp, routeConfig, geoIpHandler) {
|
|
1676
|
+
if (!geoIpHandler) return null;
|
|
1677
|
+
let country = null;
|
|
1678
|
+
if (routeConfig.blockedCountries && routeConfig.blockedCountries.length > 0) {
|
|
1679
|
+
country = geoIpHandler.getCountry(clientIp);
|
|
1680
|
+
if (country && routeConfig.blockedCountries.includes(country)) return false;
|
|
1681
|
+
}
|
|
1682
|
+
if (routeConfig.whitelistCountries && routeConfig.whitelistCountries.length > 0) {
|
|
1683
|
+
if (country === null) country = geoIpHandler.getCountry(clientIp);
|
|
1684
|
+
if (country) return routeConfig.whitelistCountries.includes(country);
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
async function checkRouteIpAccess(clientIp, routeConfig, middleware) {
|
|
1690
|
+
try {
|
|
1691
|
+
if (routeConfig.ipBlacklist && routeConfig.ipBlacklist.length > 0) {
|
|
1692
|
+
if (isIpInBlacklist(clientIp, routeConfig.ipBlacklist)) return false;
|
|
1693
|
+
}
|
|
1694
|
+
if (routeConfig.ipWhitelist && routeConfig.ipWhitelist.length > 0) {
|
|
1695
|
+
const whitelistResult = isIpInWhitelist(clientIp, routeConfig.ipWhitelist);
|
|
1696
|
+
if (whitelistResult !== null) return whitelistResult;
|
|
1697
|
+
}
|
|
1698
|
+
const countryResult = checkCountryAccess(clientIp, routeConfig, middleware.geoIpHandler);
|
|
1699
|
+
if (countryResult !== null) return countryResult;
|
|
1700
|
+
return null;
|
|
1701
|
+
} catch {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
async function checkUserAgentAllowed(userAgent, routeConfig, config) {
|
|
1706
|
+
if (routeConfig && routeConfig.blockedUserAgents.length > 0) {
|
|
1707
|
+
for (const pattern of routeConfig.blockedUserAgents) {
|
|
1708
|
+
if (new RegExp(pattern, "i").test(userAgent)) return false;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (const pattern of config.blockedUserAgents) {
|
|
1712
|
+
if (new RegExp(pattern, "i").test(userAgent)) return false;
|
|
1713
|
+
}
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
function validateAuthHeader(authHeader, authType) {
|
|
1717
|
+
if (authType === "bearer") {
|
|
1718
|
+
if (!authHeader.startsWith("Bearer ")) return [false, "Missing or invalid Bearer token"];
|
|
1719
|
+
} else if (authType === "basic") {
|
|
1720
|
+
if (!authHeader.startsWith("Basic ")) return [false, "Missing or invalid Basic authentication"];
|
|
1721
|
+
} else {
|
|
1722
|
+
if (!authHeader) return [false, `Missing ${authType} authentication`];
|
|
1723
|
+
}
|
|
1724
|
+
return [true, ""];
|
|
1725
|
+
}
|
|
1726
|
+
function isReferrerDomainAllowed(referrer, allowedDomains) {
|
|
1727
|
+
try {
|
|
1728
|
+
const url = new URL(referrer);
|
|
1729
|
+
const referrerDomain = url.hostname.toLowerCase();
|
|
1730
|
+
for (const allowed of allowedDomains) {
|
|
1731
|
+
const lowerAllowed = allowed.toLowerCase();
|
|
1732
|
+
if (referrerDomain === lowerAllowed || referrerDomain.endsWith(`.${lowerAllowed}`)) {
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return false;
|
|
1737
|
+
} catch {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async function detectPenetrationPatterns(request, routeConfig, config, shouldBypassCheckFn) {
|
|
1742
|
+
let penetrationEnabled = config.enablePenetrationDetection;
|
|
1743
|
+
let routeSpecificDetection = null;
|
|
1744
|
+
if (routeConfig) {
|
|
1745
|
+
routeSpecificDetection = routeConfig.enableSuspiciousDetection;
|
|
1746
|
+
penetrationEnabled = routeSpecificDetection;
|
|
1747
|
+
}
|
|
1748
|
+
if (penetrationEnabled && !shouldBypassCheckFn("penetration", routeConfig)) {
|
|
1749
|
+
const { detectPenetrationAttempt: detectPenetrationAttempt2 } = await import("./utils-5L6SNIYK.js");
|
|
1750
|
+
return detectPenetrationAttempt2(request);
|
|
1751
|
+
}
|
|
1752
|
+
const reason = routeSpecificDetection === false && config.enablePenetrationDetection ? "disabled_by_decorator" : "not_enabled";
|
|
1753
|
+
return [false, reason];
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/core/checks/implementations/authentication.ts
|
|
1757
|
+
var AuthenticationCheck = class extends SecurityCheck {
|
|
1758
|
+
get checkName() {
|
|
1759
|
+
return "authentication";
|
|
1760
|
+
}
|
|
1761
|
+
async check(request) {
|
|
1762
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1763
|
+
if (!routeConfig) return null;
|
|
1764
|
+
if (routeConfig.authRequired) {
|
|
1765
|
+
const authHeader = request.headers["authorization"] ?? "";
|
|
1766
|
+
const [isValid2, message] = validateAuthHeader(authHeader, routeConfig.authRequired);
|
|
1767
|
+
if (!isValid2) {
|
|
1768
|
+
if (this.isPassiveMode()) {
|
|
1769
|
+
this.logger.info(`[PASSIVE] Auth failed: ${message}`);
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
await this.sendEvent("authentication_failed", request, "request_blocked", message);
|
|
1773
|
+
return this.createErrorResponse(401, message);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if (routeConfig.apiKeyRequired) {
|
|
1777
|
+
const apiKey = request.headers["x-api-key"] ?? "";
|
|
1778
|
+
if (!apiKey) {
|
|
1779
|
+
if (this.isPassiveMode()) {
|
|
1780
|
+
this.logger.info("[PASSIVE] Missing API key");
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
return this.createErrorResponse(401, "API key required");
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
|
|
1790
|
+
// src/core/checks/implementations/referrer.ts
|
|
1791
|
+
var ReferrerCheck = class extends SecurityCheck {
|
|
1792
|
+
get checkName() {
|
|
1793
|
+
return "referrer";
|
|
1794
|
+
}
|
|
1795
|
+
async check(request) {
|
|
1796
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1797
|
+
if (!routeConfig?.requireReferrer || routeConfig.requireReferrer.length === 0) return null;
|
|
1798
|
+
const referrer = request.headers["referer"] ?? request.headers["referrer"] ?? "";
|
|
1799
|
+
if (!referrer || !isReferrerDomainAllowed(referrer, routeConfig.requireReferrer)) {
|
|
1800
|
+
if (this.isPassiveMode()) {
|
|
1801
|
+
this.logger.info(`[PASSIVE] Invalid referrer: ${referrer}`);
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
return this.createErrorResponse(403, "Invalid referrer");
|
|
1805
|
+
}
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
// src/core/checks/implementations/custom-validators.ts
|
|
1811
|
+
var CustomValidatorsCheck = class extends SecurityCheck {
|
|
1812
|
+
get checkName() {
|
|
1813
|
+
return "custom_validators";
|
|
1814
|
+
}
|
|
1815
|
+
async check(request) {
|
|
1816
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1817
|
+
if (!routeConfig || routeConfig.customValidators.length === 0) return null;
|
|
1818
|
+
for (const validator of routeConfig.customValidators) {
|
|
1819
|
+
const response = await validator(request);
|
|
1820
|
+
if (response !== null) return response;
|
|
1821
|
+
}
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// src/core/checks/implementations/time-window.ts
|
|
1827
|
+
var TimeWindowCheck = class extends SecurityCheck {
|
|
1828
|
+
validator;
|
|
1829
|
+
constructor(middleware, validator) {
|
|
1830
|
+
super(middleware);
|
|
1831
|
+
this.validator = validator;
|
|
1832
|
+
}
|
|
1833
|
+
get checkName() {
|
|
1834
|
+
return "time_window";
|
|
1835
|
+
}
|
|
1836
|
+
async check(request) {
|
|
1837
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1838
|
+
if (!routeConfig?.timeRestrictions) return null;
|
|
1839
|
+
const withinWindow = await this.validator.checkTimeWindow(routeConfig.timeRestrictions);
|
|
1840
|
+
if (!withinWindow) {
|
|
1841
|
+
if (this.isPassiveMode()) {
|
|
1842
|
+
this.logger.info("[PASSIVE] Request outside time window");
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
return this.createErrorResponse(403, "Access denied: outside allowed time window");
|
|
1846
|
+
}
|
|
1847
|
+
return null;
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
// src/core/checks/implementations/cloud-ip-refresh.ts
|
|
1852
|
+
var CloudIpRefreshCheck = class extends SecurityCheck {
|
|
1853
|
+
get checkName() {
|
|
1854
|
+
return "cloud_ip_refresh";
|
|
1855
|
+
}
|
|
1856
|
+
async check(_request) {
|
|
1857
|
+
if (this.config.blockCloudProviders.size === 0) return null;
|
|
1858
|
+
const now = Date.now() / 1e3;
|
|
1859
|
+
const elapsed = now - this.middleware.lastCloudIpRefresh;
|
|
1860
|
+
if (elapsed >= this.config.cloudIpRefreshInterval) {
|
|
1861
|
+
this.middleware.lastCloudIpRefresh = now;
|
|
1862
|
+
try {
|
|
1863
|
+
await this.middleware.refreshCloudIpRanges();
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
this.logger.error(`Cloud IP refresh failed: ${e}`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return null;
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
// src/core/checks/implementations/ip-security.ts
|
|
1873
|
+
var IpSecurityCheck = class extends SecurityCheck {
|
|
1874
|
+
get checkName() {
|
|
1875
|
+
return "ip_security";
|
|
1876
|
+
}
|
|
1877
|
+
async check(request) {
|
|
1878
|
+
const clientIp = request.clientHost;
|
|
1879
|
+
if (!clientIp) return null;
|
|
1880
|
+
const ipBanHandler = this.middleware.rateLimitHandler;
|
|
1881
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1882
|
+
if (routeConfig) {
|
|
1883
|
+
const routeResult = await checkRouteIpAccess(clientIp, routeConfig, this.middleware);
|
|
1884
|
+
if (routeResult === false) {
|
|
1885
|
+
if (this.isPassiveMode()) {
|
|
1886
|
+
this.logger.info(`[PASSIVE] IP blocked by route config: ${clientIp}`);
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
await this.sendEvent("ip_blocked", request, "request_blocked", `IP ${clientIp} blocked by route config`);
|
|
1890
|
+
return this.createErrorResponse(403, "Access denied");
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const allowed = await isIpAllowed(clientIp, this.config, this.middleware.geoIpHandler);
|
|
1894
|
+
if (!allowed) {
|
|
1895
|
+
if (this.isPassiveMode()) {
|
|
1896
|
+
this.logger.info(`[PASSIVE] IP not allowed: ${clientIp}`);
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
await this.sendEvent("ip_blocked", request, "request_blocked", `IP ${clientIp} not allowed`);
|
|
1900
|
+
return this.createErrorResponse(403, "Access denied");
|
|
1901
|
+
}
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
// src/core/checks/implementations/cloud-provider.ts
|
|
1907
|
+
var CloudProviderCheck = class extends SecurityCheck {
|
|
1908
|
+
get checkName() {
|
|
1909
|
+
return "cloud_provider";
|
|
1910
|
+
}
|
|
1911
|
+
async check(request) {
|
|
1912
|
+
const clientIp = request.clientHost;
|
|
1913
|
+
if (!clientIp) return null;
|
|
1914
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1915
|
+
const resolver = this.middleware.routeResolver;
|
|
1916
|
+
const providers = resolver.getCloudProvidersToCheck(routeConfig ?? null);
|
|
1917
|
+
if (!providers || providers.length === 0) return null;
|
|
1918
|
+
const { CloudHandler: CloudHandler2 } = await import("./cloud-46J7XK7I.js");
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// src/core/checks/implementations/user-agent.ts
|
|
1924
|
+
var UserAgentCheck = class extends SecurityCheck {
|
|
1925
|
+
get checkName() {
|
|
1926
|
+
return "user_agent";
|
|
1927
|
+
}
|
|
1928
|
+
async check(request) {
|
|
1929
|
+
const userAgent = request.headers["user-agent"] ?? "";
|
|
1930
|
+
if (!userAgent) return null;
|
|
1931
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1932
|
+
const allowed = await checkUserAgentAllowed(userAgent, routeConfig ?? null, this.config);
|
|
1933
|
+
if (!allowed) {
|
|
1934
|
+
if (this.isPassiveMode()) {
|
|
1935
|
+
this.logger.info(`[PASSIVE] Blocked user agent: ${userAgent}`);
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
await this.sendEvent("ua_blocked", request, "request_blocked", `Blocked user agent: ${userAgent}`);
|
|
1939
|
+
return this.createErrorResponse(403, "Access denied");
|
|
1940
|
+
}
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// src/core/checks/implementations/rate-limit.ts
|
|
1946
|
+
var RateLimitCheck = class extends SecurityCheck {
|
|
1947
|
+
get checkName() {
|
|
1948
|
+
return "rate_limit";
|
|
1949
|
+
}
|
|
1950
|
+
async check(request) {
|
|
1951
|
+
if (!this.config.enableRateLimiting) return null;
|
|
1952
|
+
const clientIp = request.clientHost;
|
|
1953
|
+
if (!clientIp) return null;
|
|
1954
|
+
const rateLimitHandler = this.middleware.rateLimitHandler;
|
|
1955
|
+
const routeConfig = request.state["_routeConfig"];
|
|
1956
|
+
const createError = this.createErrorResponse.bind(this);
|
|
1957
|
+
if (routeConfig?.rateLimit !== null && routeConfig?.rateLimit !== void 0) {
|
|
1958
|
+
const response2 = await rateLimitHandler.checkRateLimit(
|
|
1959
|
+
request,
|
|
1960
|
+
clientIp,
|
|
1961
|
+
createError,
|
|
1962
|
+
request.urlPath,
|
|
1963
|
+
routeConfig.rateLimit,
|
|
1964
|
+
routeConfig.rateLimitWindow ?? this.config.rateLimitWindow
|
|
1965
|
+
);
|
|
1966
|
+
if (response2) {
|
|
1967
|
+
if (this.isPassiveMode()) {
|
|
1968
|
+
this.logger.info(`[PASSIVE] Route rate limit exceeded for ${clientIp}`);
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
return response2;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
const endpointLimit = this.config.endpointRateLimits[request.urlPath];
|
|
1975
|
+
if (endpointLimit) {
|
|
1976
|
+
const [limit, window] = endpointLimit;
|
|
1977
|
+
const response2 = await rateLimitHandler.checkRateLimit(
|
|
1978
|
+
request,
|
|
1979
|
+
clientIp,
|
|
1980
|
+
createError,
|
|
1981
|
+
request.urlPath,
|
|
1982
|
+
limit,
|
|
1983
|
+
window
|
|
1984
|
+
);
|
|
1985
|
+
if (response2) {
|
|
1986
|
+
if (this.isPassiveMode()) {
|
|
1987
|
+
this.logger.info(`[PASSIVE] Endpoint rate limit exceeded for ${clientIp}`);
|
|
1988
|
+
return null;
|
|
1989
|
+
}
|
|
1990
|
+
return response2;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const response = await rateLimitHandler.checkRateLimit(
|
|
1994
|
+
request,
|
|
1995
|
+
clientIp,
|
|
1996
|
+
createError,
|
|
1997
|
+
null,
|
|
1998
|
+
this.config.rateLimit,
|
|
1999
|
+
this.config.rateLimitWindow
|
|
2000
|
+
);
|
|
2001
|
+
if (response) {
|
|
2002
|
+
if (this.isPassiveMode()) {
|
|
2003
|
+
this.logger.info(`[PASSIVE] Global rate limit exceeded for ${clientIp}`);
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
return response;
|
|
2007
|
+
}
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
// src/core/checks/implementations/suspicious-activity.ts
|
|
2013
|
+
var SuspiciousActivityCheck = class extends SecurityCheck {
|
|
2014
|
+
get checkName() {
|
|
2015
|
+
return "suspicious_activity";
|
|
2016
|
+
}
|
|
2017
|
+
async check(request) {
|
|
2018
|
+
if (!this.config.enablePenetrationDetection) return null;
|
|
2019
|
+
const clientIp = request.clientHost;
|
|
2020
|
+
if (!clientIp) return null;
|
|
2021
|
+
const routeConfig = request.state["_routeConfig"];
|
|
2022
|
+
const resolver = this.middleware.routeResolver;
|
|
2023
|
+
const [isThreat, triggerInfo] = await detectPenetrationPatterns(
|
|
2024
|
+
request,
|
|
2025
|
+
routeConfig ?? null,
|
|
2026
|
+
this.config,
|
|
2027
|
+
(check, rc) => resolver.shouldBypassCheck(check, rc)
|
|
2028
|
+
);
|
|
2029
|
+
if (!isThreat) return null;
|
|
2030
|
+
const counts = this.middleware.suspiciousRequestCounts;
|
|
2031
|
+
const currentCount = (counts.get(clientIp) ?? 0) + 1;
|
|
2032
|
+
counts.set(clientIp, currentCount);
|
|
2033
|
+
logActivity(
|
|
2034
|
+
request,
|
|
2035
|
+
this.logger,
|
|
2036
|
+
"suspicious",
|
|
2037
|
+
"Suspicious activity detected",
|
|
2038
|
+
this.config.passiveMode,
|
|
2039
|
+
triggerInfo,
|
|
2040
|
+
this.config.logSuspiciousLevel
|
|
2041
|
+
);
|
|
2042
|
+
await this.sendEvent(
|
|
2043
|
+
"penetration_attempt",
|
|
2044
|
+
request,
|
|
2045
|
+
"request_blocked",
|
|
2046
|
+
`Suspicious activity: ${triggerInfo}`,
|
|
2047
|
+
{ triggerInfo, requestCount: currentCount }
|
|
2048
|
+
);
|
|
2049
|
+
if (this.isPassiveMode()) return null;
|
|
2050
|
+
return this.createErrorResponse(403, "Suspicious activity detected");
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
|
|
2054
|
+
// src/core/checks/implementations/custom-request.ts
|
|
2055
|
+
var CustomRequestCheck = class extends SecurityCheck {
|
|
2056
|
+
get checkName() {
|
|
2057
|
+
return "custom_request";
|
|
2058
|
+
}
|
|
2059
|
+
async check(request) {
|
|
2060
|
+
if (!this.config.customRequestCheck) return null;
|
|
2061
|
+
return this.config.customRequestCheck(request);
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
// src/middleware-support.ts
|
|
2066
|
+
async function initializeSecurityMiddleware(config, logger, guardResponseFactory, agentHandler, geoIpHandler, guardDecorator) {
|
|
2067
|
+
const initializer = new HandlerInitializer(
|
|
2068
|
+
config,
|
|
2069
|
+
logger,
|
|
2070
|
+
agentHandler ?? null,
|
|
2071
|
+
geoIpHandler ?? null,
|
|
2072
|
+
guardDecorator ?? null
|
|
2073
|
+
);
|
|
2074
|
+
const registry = await initializer.initialize();
|
|
2075
|
+
const eventBus = new SecurityEventBus(
|
|
2076
|
+
agentHandler ?? null,
|
|
2077
|
+
config,
|
|
2078
|
+
logger,
|
|
2079
|
+
registry.geoIpHandler
|
|
2080
|
+
);
|
|
2081
|
+
const metricsCollector = new MetricsCollector(
|
|
2082
|
+
agentHandler ?? null,
|
|
2083
|
+
config,
|
|
2084
|
+
logger
|
|
2085
|
+
);
|
|
2086
|
+
const validator = new RequestValidator(config, logger, eventBus);
|
|
2087
|
+
const routeResolver = new RouteConfigResolver(config);
|
|
2088
|
+
if (guardDecorator) routeResolver.setGuardDecorator(guardDecorator);
|
|
2089
|
+
const errorResponseFactory = new ErrorResponseFactory(
|
|
2090
|
+
config,
|
|
2091
|
+
logger,
|
|
2092
|
+
metricsCollector,
|
|
2093
|
+
guardResponseFactory,
|
|
2094
|
+
registry.securityHeadersHandler,
|
|
2095
|
+
agentHandler ?? null
|
|
2096
|
+
);
|
|
2097
|
+
const bypassHandler = new BypassHandler(
|
|
2098
|
+
config,
|
|
2099
|
+
eventBus,
|
|
2100
|
+
routeResolver,
|
|
2101
|
+
errorResponseFactory,
|
|
2102
|
+
validator
|
|
2103
|
+
);
|
|
2104
|
+
const behavioralProcessor = new BehavioralProcessor(logger, eventBus);
|
|
2105
|
+
if (guardDecorator) {
|
|
2106
|
+
behavioralProcessor.setGuardDecorator(
|
|
2107
|
+
guardDecorator
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
const middlewareProtocol = {
|
|
2111
|
+
get config() {
|
|
2112
|
+
return config;
|
|
2113
|
+
},
|
|
2114
|
+
get logger() {
|
|
2115
|
+
return logger;
|
|
2116
|
+
},
|
|
2117
|
+
lastCloudIpRefresh: 0,
|
|
2118
|
+
suspiciousRequestCounts: /* @__PURE__ */ new Map(),
|
|
2119
|
+
get eventBus() {
|
|
2120
|
+
return eventBus;
|
|
2121
|
+
},
|
|
2122
|
+
get routeResolver() {
|
|
2123
|
+
return routeResolver;
|
|
2124
|
+
},
|
|
2125
|
+
get responseFactory() {
|
|
2126
|
+
return errorResponseFactory;
|
|
2127
|
+
},
|
|
2128
|
+
get rateLimitHandler() {
|
|
2129
|
+
return registry.rateLimitHandler;
|
|
2130
|
+
},
|
|
2131
|
+
get agentHandler() {
|
|
2132
|
+
return agentHandler ?? null;
|
|
2133
|
+
},
|
|
2134
|
+
get geoIpHandler() {
|
|
2135
|
+
return registry.geoIpHandler ?? null;
|
|
2136
|
+
},
|
|
2137
|
+
get guardResponseFactory() {
|
|
2138
|
+
return guardResponseFactory;
|
|
2139
|
+
},
|
|
2140
|
+
async createErrorResponse(statusCode, message) {
|
|
2141
|
+
return errorResponseFactory.createErrorResponse(statusCode, message);
|
|
2142
|
+
},
|
|
2143
|
+
async refreshCloudIpRanges() {
|
|
2144
|
+
if (registry.cloudHandler && config.blockCloudProviders.size > 0) {
|
|
2145
|
+
await registry.cloudHandler.refreshAsync(config.blockCloudProviders);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
const pipeline = new SecurityCheckPipeline([
|
|
2150
|
+
new RouteConfigCheck(middlewareProtocol),
|
|
2151
|
+
new EmergencyModeCheck(middlewareProtocol),
|
|
2152
|
+
new HttpsEnforcementCheck(middlewareProtocol, validator, errorResponseFactory),
|
|
2153
|
+
new RequestLoggingCheck(middlewareProtocol),
|
|
2154
|
+
new RequestSizeContentCheck(middlewareProtocol),
|
|
2155
|
+
new RequiredHeadersCheck(middlewareProtocol),
|
|
2156
|
+
new AuthenticationCheck(middlewareProtocol),
|
|
2157
|
+
new ReferrerCheck(middlewareProtocol),
|
|
2158
|
+
new CustomValidatorsCheck(middlewareProtocol),
|
|
2159
|
+
new TimeWindowCheck(middlewareProtocol, validator),
|
|
2160
|
+
new CloudIpRefreshCheck(middlewareProtocol),
|
|
2161
|
+
new IpSecurityCheck(middlewareProtocol),
|
|
2162
|
+
new CloudProviderCheck(middlewareProtocol),
|
|
2163
|
+
new UserAgentCheck(middlewareProtocol),
|
|
2164
|
+
new RateLimitCheck(middlewareProtocol),
|
|
2165
|
+
new SuspiciousActivityCheck(middlewareProtocol),
|
|
2166
|
+
new CustomRequestCheck(middlewareProtocol)
|
|
2167
|
+
], logger);
|
|
2168
|
+
return {
|
|
2169
|
+
registry,
|
|
2170
|
+
pipeline,
|
|
2171
|
+
eventBus,
|
|
2172
|
+
metricsCollector,
|
|
2173
|
+
validator,
|
|
2174
|
+
routeResolver,
|
|
2175
|
+
bypassHandler,
|
|
2176
|
+
errorResponseFactory,
|
|
2177
|
+
behavioralProcessor,
|
|
2178
|
+
middlewareProtocol
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/decorators/base.ts
|
|
2183
|
+
var routeIdMap = /* @__PURE__ */ new WeakMap();
|
|
2184
|
+
var routeIdCounter = 0;
|
|
2185
|
+
var BaseSecurityDecorator = class {
|
|
2186
|
+
routeConfigs = /* @__PURE__ */ new Map();
|
|
2187
|
+
behaviorTracker;
|
|
2188
|
+
agentHandler = null;
|
|
2189
|
+
config;
|
|
2190
|
+
logger;
|
|
2191
|
+
constructor(config, logger) {
|
|
2192
|
+
this.config = config;
|
|
2193
|
+
this.logger = logger ?? defaultLogger;
|
|
2194
|
+
this.behaviorTracker = new BehaviorTracker(config, this.logger);
|
|
2195
|
+
}
|
|
2196
|
+
getRouteConfig(routeId) {
|
|
2197
|
+
return this.routeConfigs.get(routeId);
|
|
2198
|
+
}
|
|
2199
|
+
ensureRouteConfig(fn) {
|
|
2200
|
+
const id = this.getRouteId(fn);
|
|
2201
|
+
if (!this.routeConfigs.has(id)) {
|
|
2202
|
+
const rc = new RouteConfig();
|
|
2203
|
+
rc.enableSuspiciousDetection = this.config.enablePenetrationDetection;
|
|
2204
|
+
this.routeConfigs.set(id, rc);
|
|
2205
|
+
}
|
|
2206
|
+
return this.routeConfigs.get(id);
|
|
2207
|
+
}
|
|
2208
|
+
applyRouteConfig(fn) {
|
|
2209
|
+
fn["_guardRouteId"] = this.getRouteId(fn);
|
|
2210
|
+
return fn;
|
|
2211
|
+
}
|
|
2212
|
+
getRouteId(fn) {
|
|
2213
|
+
if (!routeIdMap.has(fn)) {
|
|
2214
|
+
routeIdMap.set(fn, `guard_route_${++routeIdCounter}`);
|
|
2215
|
+
}
|
|
2216
|
+
return routeIdMap.get(fn);
|
|
2217
|
+
}
|
|
2218
|
+
async initializeBehaviorTracking(redisHandler) {
|
|
2219
|
+
if (redisHandler) await this.behaviorTracker.initializeRedis(redisHandler);
|
|
2220
|
+
}
|
|
2221
|
+
async initializeAgent(agentHandler) {
|
|
2222
|
+
this.agentHandler = agentHandler;
|
|
2223
|
+
await this.behaviorTracker.initializeAgent(agentHandler);
|
|
2224
|
+
}
|
|
2225
|
+
async sendDecoratorEvent(eventType, _request, actionTaken, reason, decoratorType, meta) {
|
|
2226
|
+
if (!this.agentHandler) return;
|
|
2227
|
+
try {
|
|
2228
|
+
await this.agentHandler.sendEvent({
|
|
2229
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2230
|
+
eventType,
|
|
2231
|
+
actionTaken,
|
|
2232
|
+
reason,
|
|
2233
|
+
decoratorType,
|
|
2234
|
+
metadata: meta ?? {}
|
|
2235
|
+
});
|
|
2236
|
+
} catch {
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
async sendAccessDeniedEvent(request, reason, decoratorType, meta) {
|
|
2240
|
+
await this.sendDecoratorEvent("access_denied", request, "request_blocked", reason, decoratorType, meta);
|
|
2241
|
+
}
|
|
2242
|
+
async sendAuthenticationFailedEvent(request, reason, authType, meta) {
|
|
2243
|
+
await this.sendDecoratorEvent("authentication_failed", request, "request_blocked", reason, "authentication", { authType, ...meta });
|
|
2244
|
+
}
|
|
2245
|
+
async sendRateLimitEvent(request, limit, window, meta) {
|
|
2246
|
+
await this.sendDecoratorEvent("rate_limit_exceeded", request, "request_blocked", `Rate limit ${limit}/${window}s exceeded`, "rate_limit", { limit, window, ...meta });
|
|
2247
|
+
}
|
|
2248
|
+
async sendDecoratorViolationEvent(request, violationType, reason, meta) {
|
|
2249
|
+
await this.sendDecoratorEvent("decorator_violation", request, "request_blocked", reason, violationType, meta);
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
function getRouteDecoratorConfig(request, decoratorHandler) {
|
|
2253
|
+
const routeId = request.state.guardRouteId;
|
|
2254
|
+
if (!routeId || typeof routeId !== "string") return void 0;
|
|
2255
|
+
return decoratorHandler.getRouteConfig(routeId);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// src/decorators/access-control.ts
|
|
2259
|
+
function AccessControl(Base) {
|
|
2260
|
+
return class extends Base {
|
|
2261
|
+
requireIp(whitelist, blacklist) {
|
|
2262
|
+
return (fn) => {
|
|
2263
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2264
|
+
if (whitelist) rc.ipWhitelist = whitelist;
|
|
2265
|
+
if (blacklist) rc.ipBlacklist = blacklist;
|
|
2266
|
+
return this.applyRouteConfig(fn);
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
blockCountries(countries) {
|
|
2270
|
+
return (fn) => {
|
|
2271
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2272
|
+
rc.blockedCountries = countries;
|
|
2273
|
+
return this.applyRouteConfig(fn);
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
allowCountries(countries) {
|
|
2277
|
+
return (fn) => {
|
|
2278
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2279
|
+
rc.whitelistCountries = countries;
|
|
2280
|
+
return this.applyRouteConfig(fn);
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
blockClouds(providers) {
|
|
2284
|
+
return (fn) => {
|
|
2285
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2286
|
+
rc.blockCloudProviders = new Set(providers ?? ["AWS", "GCP", "Azure"]);
|
|
2287
|
+
return this.applyRouteConfig(fn);
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
bypass(checks) {
|
|
2291
|
+
return (fn) => {
|
|
2292
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2293
|
+
for (const check of checks) rc.bypassedChecks.add(check);
|
|
2294
|
+
return this.applyRouteConfig(fn);
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// src/decorators/rate-limiting.ts
|
|
2301
|
+
function RateLimiting(Base) {
|
|
2302
|
+
return class extends Base {
|
|
2303
|
+
rateLimit(requests, window = 60) {
|
|
2304
|
+
return (fn) => {
|
|
2305
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2306
|
+
rc.rateLimit = requests;
|
|
2307
|
+
rc.rateLimitWindow = window;
|
|
2308
|
+
return this.applyRouteConfig(fn);
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
geoRateLimit(limits) {
|
|
2312
|
+
return (fn) => {
|
|
2313
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2314
|
+
rc.geoRateLimits = limits;
|
|
2315
|
+
return this.applyRouteConfig(fn);
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// src/decorators/authentication.ts
|
|
2322
|
+
function Authentication(Base) {
|
|
2323
|
+
return class extends Base {
|
|
2324
|
+
requireHttps() {
|
|
2325
|
+
return (fn) => {
|
|
2326
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2327
|
+
rc.requireHttps = true;
|
|
2328
|
+
return this.applyRouteConfig(fn);
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
requireAuth(type = "bearer") {
|
|
2332
|
+
return (fn) => {
|
|
2333
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2334
|
+
rc.authRequired = type;
|
|
2335
|
+
return this.applyRouteConfig(fn);
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
apiKeyAuth(headerName = "X-API-Key") {
|
|
2339
|
+
return (fn) => {
|
|
2340
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2341
|
+
rc.apiKeyRequired = true;
|
|
2342
|
+
rc.requiredHeaders[headerName] = "";
|
|
2343
|
+
return this.applyRouteConfig(fn);
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
requireHeaders(headers) {
|
|
2347
|
+
return (fn) => {
|
|
2348
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2349
|
+
Object.assign(rc.requiredHeaders, headers);
|
|
2350
|
+
return this.applyRouteConfig(fn);
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/decorators/content-filtering.ts
|
|
2357
|
+
function ContentFiltering(Base) {
|
|
2358
|
+
return class extends Base {
|
|
2359
|
+
blockUserAgents(patterns) {
|
|
2360
|
+
return (fn) => {
|
|
2361
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2362
|
+
rc.blockedUserAgents.push(...patterns);
|
|
2363
|
+
return this.applyRouteConfig(fn);
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
contentTypeFilter(allowedTypes) {
|
|
2367
|
+
return (fn) => {
|
|
2368
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2369
|
+
rc.allowedContentTypes = allowedTypes;
|
|
2370
|
+
return this.applyRouteConfig(fn);
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
maxRequestSize(sizeBytes) {
|
|
2374
|
+
return (fn) => {
|
|
2375
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2376
|
+
rc.maxRequestSize = sizeBytes;
|
|
2377
|
+
return this.applyRouteConfig(fn);
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
requireReferrer(allowedDomains) {
|
|
2381
|
+
return (fn) => {
|
|
2382
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2383
|
+
rc.requireReferrer = allowedDomains;
|
|
2384
|
+
return this.applyRouteConfig(fn);
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
customValidation(validator) {
|
|
2388
|
+
return (fn) => {
|
|
2389
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2390
|
+
rc.customValidators.push(validator);
|
|
2391
|
+
return this.applyRouteConfig(fn);
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/decorators/behavioral.ts
|
|
2398
|
+
function Behavioral(Base) {
|
|
2399
|
+
return class extends Base {
|
|
2400
|
+
usageMonitor(maxCalls, window = 3600, action = "ban") {
|
|
2401
|
+
return (fn) => {
|
|
2402
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2403
|
+
rc.behaviorRules.push(new BehaviorRule("usage", maxCalls, window, null, action));
|
|
2404
|
+
return this.applyRouteConfig(fn);
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
returnMonitor(pattern, maxOccurrences, window = 86400, action = "ban") {
|
|
2408
|
+
return (fn) => {
|
|
2409
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2410
|
+
rc.behaviorRules.push(new BehaviorRule("return_pattern", maxOccurrences, window, pattern, action));
|
|
2411
|
+
return this.applyRouteConfig(fn);
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
behaviorAnalysis(rules) {
|
|
2415
|
+
return (fn) => {
|
|
2416
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2417
|
+
rc.behaviorRules.push(...rules);
|
|
2418
|
+
return this.applyRouteConfig(fn);
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
suspiciousFrequency(maxFrequency, window = 300, action = "ban") {
|
|
2422
|
+
return (fn) => {
|
|
2423
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2424
|
+
rc.behaviorRules.push(new BehaviorRule("frequency", maxFrequency, window, null, action));
|
|
2425
|
+
return this.applyRouteConfig(fn);
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// src/decorators/advanced.ts
|
|
2432
|
+
function Advanced(Base) {
|
|
2433
|
+
return class extends Base {
|
|
2434
|
+
timeWindow(startTime, endTime, _timezone = "UTC") {
|
|
2435
|
+
return (fn) => {
|
|
2436
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2437
|
+
rc.timeRestrictions = { start: startTime, end: endTime };
|
|
2438
|
+
return this.applyRouteConfig(fn);
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
suspiciousDetection(enabled = true) {
|
|
2442
|
+
return (fn) => {
|
|
2443
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2444
|
+
rc.enableSuspiciousDetection = enabled;
|
|
2445
|
+
return this.applyRouteConfig(fn);
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
honeypotDetection(trapFields) {
|
|
2449
|
+
return (fn) => {
|
|
2450
|
+
const rc = this.ensureRouteConfig(fn);
|
|
2451
|
+
rc.customValidators.push(async (request) => {
|
|
2452
|
+
try {
|
|
2453
|
+
const bodyBytes = await request.body();
|
|
2454
|
+
if (bodyBytes.length === 0) return null;
|
|
2455
|
+
const bodyText = new TextDecoder().decode(bodyBytes);
|
|
2456
|
+
let data = {};
|
|
2457
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
2458
|
+
if (contentType.includes("json")) {
|
|
2459
|
+
data = JSON.parse(bodyText);
|
|
2460
|
+
} else if (contentType.includes("form")) {
|
|
2461
|
+
for (const pair of bodyText.split("&")) {
|
|
2462
|
+
const [key, value] = pair.split("=");
|
|
2463
|
+
if (key && value) data[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
for (const field of trapFields) {
|
|
2467
|
+
if (data[field] !== void 0 && data[field] !== "" && data[field] !== null) {
|
|
2468
|
+
return {
|
|
2469
|
+
statusCode: 403,
|
|
2470
|
+
headers: {},
|
|
2471
|
+
setHeader() {
|
|
2472
|
+
},
|
|
2473
|
+
body: new TextEncoder().encode("Forbidden"),
|
|
2474
|
+
bodyText: "Forbidden"
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
} catch {
|
|
2479
|
+
}
|
|
2480
|
+
return null;
|
|
2481
|
+
});
|
|
2482
|
+
return this.applyRouteConfig(fn);
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// src/decorators/index.ts
|
|
2489
|
+
var SecurityDecorator = Advanced(
|
|
2490
|
+
ContentFiltering(
|
|
2491
|
+
Behavioral(
|
|
2492
|
+
Authentication(
|
|
2493
|
+
RateLimiting(
|
|
2494
|
+
AccessControl(
|
|
2495
|
+
BaseSecurityDecorator
|
|
2496
|
+
)
|
|
2497
|
+
)
|
|
2498
|
+
)
|
|
2499
|
+
)
|
|
2500
|
+
)
|
|
2501
|
+
);
|
|
2502
|
+
export {
|
|
2503
|
+
BaseSecurityDecorator,
|
|
2504
|
+
BehaviorRule,
|
|
2505
|
+
BehavioralProcessor,
|
|
2506
|
+
BypassHandler,
|
|
2507
|
+
ContentPreprocessor,
|
|
2508
|
+
DynamicRulesSchema,
|
|
2509
|
+
ErrorResponseFactory,
|
|
2510
|
+
MetricsCollector,
|
|
2511
|
+
PatternCompiler,
|
|
2512
|
+
PerformanceMonitor,
|
|
2513
|
+
RequestValidator,
|
|
2514
|
+
RouteConfig,
|
|
2515
|
+
RouteConfigResolver,
|
|
2516
|
+
SecurityCheckPipeline,
|
|
2517
|
+
SecurityConfigSchema,
|
|
2518
|
+
SecurityDecorator,
|
|
2519
|
+
SecurityEventBus,
|
|
2520
|
+
SemanticAnalyzer,
|
|
2521
|
+
checkIpCountry,
|
|
2522
|
+
defaultLogger,
|
|
2523
|
+
detectPenetrationAttempt,
|
|
2524
|
+
extractClientIp,
|
|
2525
|
+
getRouteDecoratorConfig,
|
|
2526
|
+
initializeSecurityMiddleware,
|
|
2527
|
+
isIpAllowed,
|
|
2528
|
+
isUserAgentAllowed,
|
|
2529
|
+
logActivity,
|
|
2530
|
+
sanitizeForLog,
|
|
2531
|
+
sendAgentEvent
|
|
2532
|
+
};
|
|
2533
|
+
//# sourceMappingURL=index.js.map
|