nextjs-secure 0.5.0 → 0.7.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/README.md +736 -688
- package/dist/audit.cjs +1337 -0
- package/dist/audit.cjs.map +1 -0
- package/dist/audit.d.cts +679 -0
- package/dist/audit.d.ts +679 -0
- package/dist/audit.js +1300 -0
- package/dist/audit.js.map +1 -0
- package/dist/bot.cjs +1521 -0
- package/dist/bot.cjs.map +1 -0
- package/dist/bot.d.cts +567 -0
- package/dist/bot.d.ts +567 -0
- package/dist/bot.js +1484 -0
- package/dist/bot.js.map +1 -0
- package/dist/index.cjs +2850 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2784 -10
- package/dist/index.js.map +1 -1
- package/package.json +26 -1
package/dist/audit.cjs
ADDED
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/middleware/audit/stores/memory.ts
|
|
4
|
+
var MemoryStore = class {
|
|
5
|
+
entries = [];
|
|
6
|
+
maxEntries;
|
|
7
|
+
ttl;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.maxEntries = options.maxEntries || 1e3;
|
|
10
|
+
this.ttl = options.ttl || 0;
|
|
11
|
+
}
|
|
12
|
+
async write(entry) {
|
|
13
|
+
this.entries.push(entry);
|
|
14
|
+
if (this.entries.length > this.maxEntries) {
|
|
15
|
+
this.entries = this.entries.slice(-this.maxEntries);
|
|
16
|
+
}
|
|
17
|
+
if (this.ttl > 0) {
|
|
18
|
+
this.cleanExpired();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async query(options = {}) {
|
|
22
|
+
let result = [...this.entries];
|
|
23
|
+
if (options.level) {
|
|
24
|
+
const levels = Array.isArray(options.level) ? options.level : [options.level];
|
|
25
|
+
result = result.filter((e) => levels.includes(e.level));
|
|
26
|
+
}
|
|
27
|
+
if (options.type) {
|
|
28
|
+
result = result.filter((e) => e.type === options.type);
|
|
29
|
+
}
|
|
30
|
+
if (options.event) {
|
|
31
|
+
const events = Array.isArray(options.event) ? options.event : [options.event];
|
|
32
|
+
result = result.filter(
|
|
33
|
+
(e) => e.type === "security" && events.includes(e.event)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (options.startTime) {
|
|
37
|
+
result = result.filter((e) => e.timestamp >= options.startTime);
|
|
38
|
+
}
|
|
39
|
+
if (options.endTime) {
|
|
40
|
+
result = result.filter((e) => e.timestamp <= options.endTime);
|
|
41
|
+
}
|
|
42
|
+
if (options.ip) {
|
|
43
|
+
result = result.filter((e) => {
|
|
44
|
+
if (e.type === "request") return e.request.ip === options.ip;
|
|
45
|
+
if (e.type === "security") return e.source.ip === options.ip;
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (options.userId) {
|
|
50
|
+
result = result.filter((e) => {
|
|
51
|
+
if (e.type === "request") return e.user?.id === options.userId;
|
|
52
|
+
if (e.type === "security") return e.source.userId === options.userId;
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (options.offset) {
|
|
57
|
+
result = result.slice(options.offset);
|
|
58
|
+
}
|
|
59
|
+
if (options.limit) {
|
|
60
|
+
result = result.slice(0, options.limit);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
async flush() {
|
|
65
|
+
}
|
|
66
|
+
async close() {
|
|
67
|
+
this.entries = [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get all entries (for testing)
|
|
71
|
+
*/
|
|
72
|
+
getEntries() {
|
|
73
|
+
return [...this.entries];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Clear all entries
|
|
77
|
+
*/
|
|
78
|
+
clear() {
|
|
79
|
+
this.entries = [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get entry count
|
|
83
|
+
*/
|
|
84
|
+
size() {
|
|
85
|
+
return this.entries.length;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clean expired entries
|
|
89
|
+
*/
|
|
90
|
+
cleanExpired() {
|
|
91
|
+
if (this.ttl <= 0) return;
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
this.entries = this.entries.filter((e) => {
|
|
94
|
+
const age = now - e.timestamp.getTime();
|
|
95
|
+
return age < this.ttl;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function createMemoryStore(options) {
|
|
100
|
+
return new MemoryStore(options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/middleware/audit/stores/console.ts
|
|
104
|
+
var COLORS = {
|
|
105
|
+
reset: "\x1B[0m",
|
|
106
|
+
bold: "\x1B[1m",
|
|
107
|
+
dim: "\x1B[2m",
|
|
108
|
+
// Log levels
|
|
109
|
+
debug: "\x1B[36m",
|
|
110
|
+
// Cyan
|
|
111
|
+
info: "\x1B[32m",
|
|
112
|
+
// Green
|
|
113
|
+
warn: "\x1B[33m",
|
|
114
|
+
// Yellow
|
|
115
|
+
error: "\x1B[31m",
|
|
116
|
+
// Red
|
|
117
|
+
critical: "\x1B[35m",
|
|
118
|
+
// Magenta
|
|
119
|
+
// Security severity
|
|
120
|
+
low: "\x1B[36m",
|
|
121
|
+
// Cyan
|
|
122
|
+
medium: "\x1B[33m",
|
|
123
|
+
// Yellow
|
|
124
|
+
high: "\x1B[31m",
|
|
125
|
+
// Red
|
|
126
|
+
// Other
|
|
127
|
+
timestamp: "\x1B[90m",
|
|
128
|
+
// Gray
|
|
129
|
+
method: "\x1B[34m",
|
|
130
|
+
// Blue
|
|
131
|
+
status2xx: "\x1B[32m",
|
|
132
|
+
// Green
|
|
133
|
+
status3xx: "\x1B[36m",
|
|
134
|
+
// Cyan
|
|
135
|
+
status4xx: "\x1B[33m",
|
|
136
|
+
// Yellow
|
|
137
|
+
status5xx: "\x1B[31m"
|
|
138
|
+
// Red
|
|
139
|
+
};
|
|
140
|
+
var LEVEL_PRIORITY = {
|
|
141
|
+
debug: 0,
|
|
142
|
+
info: 1,
|
|
143
|
+
warn: 2,
|
|
144
|
+
error: 3,
|
|
145
|
+
critical: 4
|
|
146
|
+
};
|
|
147
|
+
var ConsoleStore = class {
|
|
148
|
+
colorize;
|
|
149
|
+
showTimestamp;
|
|
150
|
+
pretty;
|
|
151
|
+
minLevel;
|
|
152
|
+
constructor(options = {}) {
|
|
153
|
+
this.colorize = options.colorize ?? process.env.NODE_ENV !== "production";
|
|
154
|
+
this.showTimestamp = options.timestamp ?? true;
|
|
155
|
+
this.pretty = options.pretty ?? false;
|
|
156
|
+
this.minLevel = options.level || "info";
|
|
157
|
+
}
|
|
158
|
+
async write(entry) {
|
|
159
|
+
if (LEVEL_PRIORITY[entry.level] < LEVEL_PRIORITY[this.minLevel]) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const output = this.pretty ? this.formatPretty(entry) : this.formatCompact(entry);
|
|
163
|
+
switch (entry.level) {
|
|
164
|
+
case "debug":
|
|
165
|
+
console.debug(output);
|
|
166
|
+
break;
|
|
167
|
+
case "info":
|
|
168
|
+
console.info(output);
|
|
169
|
+
break;
|
|
170
|
+
case "warn":
|
|
171
|
+
console.warn(output);
|
|
172
|
+
break;
|
|
173
|
+
case "error":
|
|
174
|
+
case "critical":
|
|
175
|
+
console.error(output);
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
console.log(output);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async flush() {
|
|
182
|
+
}
|
|
183
|
+
async close() {
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Format entry in compact single-line format
|
|
187
|
+
*/
|
|
188
|
+
formatCompact(entry) {
|
|
189
|
+
const parts = [];
|
|
190
|
+
if (this.showTimestamp) {
|
|
191
|
+
const ts = entry.timestamp.toISOString();
|
|
192
|
+
parts.push(this.color(ts, "timestamp"));
|
|
193
|
+
}
|
|
194
|
+
parts.push(this.colorLevel(entry.level));
|
|
195
|
+
if (entry.type === "request") {
|
|
196
|
+
const req = entry.request;
|
|
197
|
+
const res = entry.response;
|
|
198
|
+
parts.push(this.color(req.method, "method"));
|
|
199
|
+
parts.push(req.path);
|
|
200
|
+
if (res) {
|
|
201
|
+
parts.push(this.colorStatus(res.status));
|
|
202
|
+
parts.push(this.color(`${res.duration}ms`, "dim"));
|
|
203
|
+
}
|
|
204
|
+
if (req.ip) {
|
|
205
|
+
parts.push(this.color(`[${req.ip}]`, "dim"));
|
|
206
|
+
}
|
|
207
|
+
if (entry.user?.id) {
|
|
208
|
+
parts.push(this.color(`user:${entry.user.id}`, "dim"));
|
|
209
|
+
}
|
|
210
|
+
if (entry.error) {
|
|
211
|
+
parts.push(this.color(`ERROR: ${entry.error.message}`, "error"));
|
|
212
|
+
}
|
|
213
|
+
} else if (entry.type === "security") {
|
|
214
|
+
parts.push(this.colorSeverity(entry.severity));
|
|
215
|
+
parts.push(entry.event);
|
|
216
|
+
if (entry.source.ip) {
|
|
217
|
+
parts.push(this.color(`[${entry.source.ip}]`, "dim"));
|
|
218
|
+
}
|
|
219
|
+
if (entry.source.userId) {
|
|
220
|
+
parts.push(this.color(`user:${entry.source.userId}`, "dim"));
|
|
221
|
+
}
|
|
222
|
+
parts.push(entry.message);
|
|
223
|
+
}
|
|
224
|
+
return parts.join(" ");
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Format entry in pretty multi-line format
|
|
228
|
+
*/
|
|
229
|
+
formatPretty(entry) {
|
|
230
|
+
const lines = [];
|
|
231
|
+
const header = [
|
|
232
|
+
this.color(entry.timestamp.toISOString(), "timestamp"),
|
|
233
|
+
this.colorLevel(entry.level),
|
|
234
|
+
`[${entry.type.toUpperCase()}]`
|
|
235
|
+
].join(" ");
|
|
236
|
+
lines.push(header);
|
|
237
|
+
if (entry.type === "request") {
|
|
238
|
+
const req = entry.request;
|
|
239
|
+
const res = entry.response;
|
|
240
|
+
lines.push(` ${this.color(req.method, "method")} ${req.url}`);
|
|
241
|
+
if (req.ip) lines.push(` IP: ${req.ip}`);
|
|
242
|
+
if (req.userAgent) lines.push(` UA: ${req.userAgent}`);
|
|
243
|
+
if (res) {
|
|
244
|
+
lines.push(` Status: ${this.colorStatus(res.status)} (${res.duration}ms)`);
|
|
245
|
+
}
|
|
246
|
+
if (entry.user) {
|
|
247
|
+
lines.push(` User: ${JSON.stringify(entry.user)}`);
|
|
248
|
+
}
|
|
249
|
+
if (entry.error) {
|
|
250
|
+
lines.push(` ${this.color("Error:", "error")} ${entry.error.message}`);
|
|
251
|
+
if (entry.error.stack) {
|
|
252
|
+
lines.push(` ${this.color(entry.error.stack, "dim")}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (entry.type === "security") {
|
|
256
|
+
lines.push(` Event: ${entry.event}`);
|
|
257
|
+
lines.push(` Severity: ${this.colorSeverity(entry.severity)}`);
|
|
258
|
+
lines.push(` Message: ${entry.message}`);
|
|
259
|
+
if (entry.source.ip) lines.push(` Source IP: ${entry.source.ip}`);
|
|
260
|
+
if (entry.source.userId) lines.push(` Source User: ${entry.source.userId}`);
|
|
261
|
+
if (entry.target) {
|
|
262
|
+
lines.push(` Target: ${JSON.stringify(entry.target)}`);
|
|
263
|
+
}
|
|
264
|
+
if (entry.details) {
|
|
265
|
+
lines.push(` Details: ${JSON.stringify(entry.details)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
|
|
269
|
+
lines.push(` Metadata: ${JSON.stringify(entry.metadata)}`);
|
|
270
|
+
}
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Apply color if enabled
|
|
275
|
+
*/
|
|
276
|
+
color(text, colorName) {
|
|
277
|
+
if (!this.colorize) return text;
|
|
278
|
+
return `${COLORS[colorName]}${text}${COLORS.reset}`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Color log level
|
|
282
|
+
*/
|
|
283
|
+
colorLevel(level) {
|
|
284
|
+
const text = level.toUpperCase().padEnd(8);
|
|
285
|
+
if (!this.colorize) return `[${text}]`;
|
|
286
|
+
return `[${COLORS[level]}${text}${COLORS.reset}]`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Color HTTP status
|
|
290
|
+
*/
|
|
291
|
+
colorStatus(status) {
|
|
292
|
+
const text = status.toString();
|
|
293
|
+
if (!this.colorize) return text;
|
|
294
|
+
if (status >= 500) return `${COLORS.status5xx}${text}${COLORS.reset}`;
|
|
295
|
+
if (status >= 400) return `${COLORS.status4xx}${text}${COLORS.reset}`;
|
|
296
|
+
if (status >= 300) return `${COLORS.status3xx}${text}${COLORS.reset}`;
|
|
297
|
+
return `${COLORS.status2xx}${text}${COLORS.reset}`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Color severity
|
|
301
|
+
*/
|
|
302
|
+
colorSeverity(severity) {
|
|
303
|
+
const text = `[${severity.toUpperCase()}]`;
|
|
304
|
+
if (!this.colorize) return text;
|
|
305
|
+
const colorKey = severity === "critical" ? "critical" : severity;
|
|
306
|
+
return `${COLORS[colorKey]}${text}${COLORS.reset}`;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
function createConsoleStore(options) {
|
|
310
|
+
return new ConsoleStore(options);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/middleware/audit/stores/external.ts
|
|
314
|
+
var ExternalStore = class {
|
|
315
|
+
endpoint;
|
|
316
|
+
headers;
|
|
317
|
+
batchSize;
|
|
318
|
+
flushInterval;
|
|
319
|
+
retryAttempts;
|
|
320
|
+
timeout;
|
|
321
|
+
buffer = [];
|
|
322
|
+
flushTimer = null;
|
|
323
|
+
isFlushing = false;
|
|
324
|
+
constructor(options) {
|
|
325
|
+
this.endpoint = options.endpoint;
|
|
326
|
+
this.headers = {
|
|
327
|
+
"Content-Type": "application/json",
|
|
328
|
+
...options.apiKey ? { "Authorization": `Bearer ${options.apiKey}` } : {},
|
|
329
|
+
...options.headers
|
|
330
|
+
};
|
|
331
|
+
this.batchSize = options.batchSize || 100;
|
|
332
|
+
this.flushInterval = options.flushInterval || 5e3;
|
|
333
|
+
this.retryAttempts = options.retryAttempts || 3;
|
|
334
|
+
this.timeout = options.timeout || 1e4;
|
|
335
|
+
if (this.flushInterval > 0) {
|
|
336
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async write(entry) {
|
|
340
|
+
this.buffer.push(entry);
|
|
341
|
+
if (this.buffer.length >= this.batchSize) {
|
|
342
|
+
await this.flush();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async flush() {
|
|
346
|
+
if (this.isFlushing || this.buffer.length === 0) return;
|
|
347
|
+
this.isFlushing = true;
|
|
348
|
+
const entries = [...this.buffer];
|
|
349
|
+
this.buffer = [];
|
|
350
|
+
try {
|
|
351
|
+
await this.send(entries);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
this.buffer = [...entries, ...this.buffer];
|
|
354
|
+
console.error("[ExternalStore] Failed to flush logs:", error);
|
|
355
|
+
} finally {
|
|
356
|
+
this.isFlushing = false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async close() {
|
|
360
|
+
if (this.flushTimer) {
|
|
361
|
+
clearInterval(this.flushTimer);
|
|
362
|
+
this.flushTimer = null;
|
|
363
|
+
}
|
|
364
|
+
await this.flush();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Send entries to external endpoint
|
|
368
|
+
*/
|
|
369
|
+
async send(entries) {
|
|
370
|
+
let lastError = null;
|
|
371
|
+
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
|
|
372
|
+
try {
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
375
|
+
const response = await fetch(this.endpoint, {
|
|
376
|
+
method: "POST",
|
|
377
|
+
headers: this.headers,
|
|
378
|
+
body: JSON.stringify({
|
|
379
|
+
logs: entries.map((e) => this.serialize(e)),
|
|
380
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
381
|
+
count: entries.length
|
|
382
|
+
}),
|
|
383
|
+
signal: controller.signal
|
|
384
|
+
});
|
|
385
|
+
clearTimeout(timeoutId);
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
392
|
+
if (attempt < this.retryAttempts - 1) {
|
|
393
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
throw lastError || new Error("Failed to send logs");
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Serialize entry for transmission
|
|
401
|
+
*/
|
|
402
|
+
serialize(entry) {
|
|
403
|
+
return {
|
|
404
|
+
...entry,
|
|
405
|
+
timestamp: entry.timestamp.toISOString()
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Sleep helper
|
|
410
|
+
*/
|
|
411
|
+
sleep(ms) {
|
|
412
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get buffer size (for monitoring)
|
|
416
|
+
*/
|
|
417
|
+
getBufferSize() {
|
|
418
|
+
return this.buffer.length;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
function createExternalStore(options) {
|
|
422
|
+
return new ExternalStore(options);
|
|
423
|
+
}
|
|
424
|
+
function createDatadogStore(options) {
|
|
425
|
+
const site = options.site || "datadoghq.com";
|
|
426
|
+
const endpoint = `https://http-intake.logs.${site}/api/v2/logs`;
|
|
427
|
+
return new ExternalStore({
|
|
428
|
+
endpoint,
|
|
429
|
+
headers: {
|
|
430
|
+
"DD-API-KEY": options.apiKey,
|
|
431
|
+
"Content-Type": "application/json"
|
|
432
|
+
},
|
|
433
|
+
batchSize: options.batchSize || 100,
|
|
434
|
+
flushInterval: options.flushInterval || 5e3
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
var MultiStore = class {
|
|
438
|
+
stores;
|
|
439
|
+
constructor(stores) {
|
|
440
|
+
this.stores = stores;
|
|
441
|
+
}
|
|
442
|
+
async write(entry) {
|
|
443
|
+
await Promise.all(this.stores.map((store) => store.write(entry)));
|
|
444
|
+
}
|
|
445
|
+
async query(options) {
|
|
446
|
+
for (const store of this.stores) {
|
|
447
|
+
if (store.query) {
|
|
448
|
+
return store.query(options);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
async flush() {
|
|
454
|
+
await Promise.all(this.stores.map((store) => store.flush?.()));
|
|
455
|
+
}
|
|
456
|
+
async close() {
|
|
457
|
+
await Promise.all(this.stores.map((store) => store.close?.()));
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function createMultiStore(stores) {
|
|
461
|
+
return new MultiStore(stores);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/middleware/audit/formatters.ts
|
|
465
|
+
var JSONFormatter = class {
|
|
466
|
+
pretty;
|
|
467
|
+
includeTimestamp;
|
|
468
|
+
constructor(options = {}) {
|
|
469
|
+
this.pretty = options.pretty ?? false;
|
|
470
|
+
this.includeTimestamp = options.includeTimestamp ?? true;
|
|
471
|
+
}
|
|
472
|
+
format(entry) {
|
|
473
|
+
const output = {
|
|
474
|
+
...entry,
|
|
475
|
+
timestamp: this.includeTimestamp ? entry.timestamp.toISOString() : void 0
|
|
476
|
+
};
|
|
477
|
+
return this.pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var TextFormatter = class {
|
|
481
|
+
template;
|
|
482
|
+
dateFormat;
|
|
483
|
+
constructor(options = {}) {
|
|
484
|
+
this.template = options.template || "{timestamp} [{level}] {message}";
|
|
485
|
+
this.dateFormat = options.dateFormat || "iso";
|
|
486
|
+
}
|
|
487
|
+
format(entry) {
|
|
488
|
+
let output = this.template;
|
|
489
|
+
output = output.replace("{timestamp}", this.formatDate(entry.timestamp));
|
|
490
|
+
output = output.replace("{level}", entry.level.toUpperCase().padEnd(8));
|
|
491
|
+
output = output.replace("{message}", entry.message);
|
|
492
|
+
output = output.replace("{type}", entry.type);
|
|
493
|
+
output = output.replace("{id}", entry.id);
|
|
494
|
+
if (entry.category) {
|
|
495
|
+
output = output.replace("{category}", entry.category);
|
|
496
|
+
}
|
|
497
|
+
if (entry.type === "request") {
|
|
498
|
+
output = output.replace("{method}", entry.request.method);
|
|
499
|
+
output = output.replace("{path}", entry.request.path);
|
|
500
|
+
output = output.replace("{url}", entry.request.url);
|
|
501
|
+
output = output.replace("{ip}", entry.request.ip || "-");
|
|
502
|
+
output = output.replace("{status}", entry.response?.status?.toString() || "-");
|
|
503
|
+
output = output.replace("{duration}", entry.response?.duration?.toString() || "-");
|
|
504
|
+
}
|
|
505
|
+
if (entry.type === "security") {
|
|
506
|
+
output = output.replace("{event}", entry.event);
|
|
507
|
+
output = output.replace("{severity}", entry.severity);
|
|
508
|
+
}
|
|
509
|
+
return output;
|
|
510
|
+
}
|
|
511
|
+
formatDate(date) {
|
|
512
|
+
switch (this.dateFormat) {
|
|
513
|
+
case "utc":
|
|
514
|
+
return date.toUTCString();
|
|
515
|
+
case "local":
|
|
516
|
+
return date.toLocaleString();
|
|
517
|
+
case "iso":
|
|
518
|
+
default:
|
|
519
|
+
return date.toISOString();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var CLFFormatter = class {
|
|
524
|
+
format(entry) {
|
|
525
|
+
if (entry.type !== "request") {
|
|
526
|
+
return `[${entry.timestamp.toISOString()}] ${entry.level.toUpperCase()} ${entry.message}`;
|
|
527
|
+
}
|
|
528
|
+
const req = entry.request;
|
|
529
|
+
const res = entry.response;
|
|
530
|
+
const host = req.ip || "-";
|
|
531
|
+
const ident = "-";
|
|
532
|
+
const authuser = entry.user?.id || "-";
|
|
533
|
+
const date = this.formatCLFDate(entry.timestamp);
|
|
534
|
+
const request = `${req.method} ${req.path} HTTP/1.1`;
|
|
535
|
+
const status = res?.status || 0;
|
|
536
|
+
const bytes = res?.contentLength || 0;
|
|
537
|
+
return `${host} ${ident} ${authuser} [${date}] "${request}" ${status} ${bytes}`;
|
|
538
|
+
}
|
|
539
|
+
formatCLFDate(date) {
|
|
540
|
+
const months = [
|
|
541
|
+
"Jan",
|
|
542
|
+
"Feb",
|
|
543
|
+
"Mar",
|
|
544
|
+
"Apr",
|
|
545
|
+
"May",
|
|
546
|
+
"Jun",
|
|
547
|
+
"Jul",
|
|
548
|
+
"Aug",
|
|
549
|
+
"Sep",
|
|
550
|
+
"Oct",
|
|
551
|
+
"Nov",
|
|
552
|
+
"Dec"
|
|
553
|
+
];
|
|
554
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
555
|
+
const month = months[date.getMonth()];
|
|
556
|
+
const year = date.getFullYear();
|
|
557
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
558
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
559
|
+
const seconds = date.getSeconds().toString().padStart(2, "0");
|
|
560
|
+
const offset = -date.getTimezoneOffset();
|
|
561
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
562
|
+
const offsetHours = Math.floor(Math.abs(offset) / 60).toString().padStart(2, "0");
|
|
563
|
+
const offsetMins = (Math.abs(offset) % 60).toString().padStart(2, "0");
|
|
564
|
+
return `${day}/${month}/${year}:${hours}:${minutes}:${seconds} ${offsetSign}${offsetHours}${offsetMins}`;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var StructuredFormatter = class {
|
|
568
|
+
delimiter;
|
|
569
|
+
kvSeparator;
|
|
570
|
+
constructor(options = {}) {
|
|
571
|
+
this.delimiter = options.delimiter || " ";
|
|
572
|
+
this.kvSeparator = options.kvSeparator || "=";
|
|
573
|
+
}
|
|
574
|
+
format(entry) {
|
|
575
|
+
const pairs = [];
|
|
576
|
+
pairs.push(this.pair("timestamp", entry.timestamp.toISOString()));
|
|
577
|
+
pairs.push(this.pair("level", entry.level));
|
|
578
|
+
pairs.push(this.pair("type", entry.type));
|
|
579
|
+
pairs.push(this.pair("id", entry.id));
|
|
580
|
+
pairs.push(this.pair("message", this.escape(entry.message)));
|
|
581
|
+
if (entry.category) {
|
|
582
|
+
pairs.push(this.pair("category", entry.category));
|
|
583
|
+
}
|
|
584
|
+
if (entry.type === "request") {
|
|
585
|
+
pairs.push(this.pair("method", entry.request.method));
|
|
586
|
+
pairs.push(this.pair("path", entry.request.path));
|
|
587
|
+
if (entry.request.ip) pairs.push(this.pair("ip", entry.request.ip));
|
|
588
|
+
if (entry.response) {
|
|
589
|
+
pairs.push(this.pair("status", entry.response.status.toString()));
|
|
590
|
+
pairs.push(this.pair("duration_ms", entry.response.duration.toString()));
|
|
591
|
+
}
|
|
592
|
+
if (entry.user?.id) pairs.push(this.pair("user_id", entry.user.id));
|
|
593
|
+
if (entry.error) {
|
|
594
|
+
pairs.push(this.pair("error", this.escape(entry.error.message)));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (entry.type === "security") {
|
|
598
|
+
pairs.push(this.pair("event", entry.event));
|
|
599
|
+
pairs.push(this.pair("severity", entry.severity));
|
|
600
|
+
if (entry.source.ip) pairs.push(this.pair("source_ip", entry.source.ip));
|
|
601
|
+
if (entry.source.userId) pairs.push(this.pair("source_user", entry.source.userId));
|
|
602
|
+
}
|
|
603
|
+
return pairs.join(this.delimiter);
|
|
604
|
+
}
|
|
605
|
+
pair(key, value) {
|
|
606
|
+
return `${key}${this.kvSeparator}${value}`;
|
|
607
|
+
}
|
|
608
|
+
escape(value) {
|
|
609
|
+
if (value.includes(" ") || value.includes('"')) {
|
|
610
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
611
|
+
}
|
|
612
|
+
return value;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
function createJSONFormatter(options) {
|
|
616
|
+
return new JSONFormatter(options);
|
|
617
|
+
}
|
|
618
|
+
function createTextFormatter(options) {
|
|
619
|
+
return new TextFormatter(options);
|
|
620
|
+
}
|
|
621
|
+
function createCLFFormatter() {
|
|
622
|
+
return new CLFFormatter();
|
|
623
|
+
}
|
|
624
|
+
function createStructuredFormatter(options) {
|
|
625
|
+
return new StructuredFormatter(options);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/middleware/audit/redaction.ts
|
|
629
|
+
var DEFAULT_PII_FIELDS = [
|
|
630
|
+
// Authentication
|
|
631
|
+
"password",
|
|
632
|
+
"passwd",
|
|
633
|
+
"secret",
|
|
634
|
+
"token",
|
|
635
|
+
"api_key",
|
|
636
|
+
"apiKey",
|
|
637
|
+
"api-key",
|
|
638
|
+
"access_token",
|
|
639
|
+
"accessToken",
|
|
640
|
+
"refresh_token",
|
|
641
|
+
"refreshToken",
|
|
642
|
+
"authorization",
|
|
643
|
+
"auth",
|
|
644
|
+
// Personal information
|
|
645
|
+
"ssn",
|
|
646
|
+
"social_security",
|
|
647
|
+
"socialSecurity",
|
|
648
|
+
"credit_card",
|
|
649
|
+
"creditCard",
|
|
650
|
+
"card_number",
|
|
651
|
+
"cardNumber",
|
|
652
|
+
"cvv",
|
|
653
|
+
"cvc",
|
|
654
|
+
"pin",
|
|
655
|
+
// Contact
|
|
656
|
+
"email",
|
|
657
|
+
"phone",
|
|
658
|
+
"phone_number",
|
|
659
|
+
"phoneNumber",
|
|
660
|
+
"mobile",
|
|
661
|
+
"address",
|
|
662
|
+
"street",
|
|
663
|
+
"zip",
|
|
664
|
+
"zipcode",
|
|
665
|
+
"postal_code",
|
|
666
|
+
"postalCode",
|
|
667
|
+
// Identity
|
|
668
|
+
"date_of_birth",
|
|
669
|
+
"dateOfBirth",
|
|
670
|
+
"dob",
|
|
671
|
+
"birth_date",
|
|
672
|
+
"birthDate",
|
|
673
|
+
"passport",
|
|
674
|
+
"license",
|
|
675
|
+
"national_id",
|
|
676
|
+
"nationalId"
|
|
677
|
+
];
|
|
678
|
+
function mask(value, options = {}) {
|
|
679
|
+
const {
|
|
680
|
+
char = "*",
|
|
681
|
+
preserveLength = false,
|
|
682
|
+
showFirst = 0,
|
|
683
|
+
showLast = 0
|
|
684
|
+
} = options;
|
|
685
|
+
if (!value) return value;
|
|
686
|
+
const len = value.length;
|
|
687
|
+
if (preserveLength) {
|
|
688
|
+
const first2 = value.slice(0, showFirst);
|
|
689
|
+
const last2 = value.slice(-showLast || len);
|
|
690
|
+
const middle = char.repeat(Math.max(0, len - showFirst - showLast));
|
|
691
|
+
return first2 + middle + (showLast > 0 ? last2 : "");
|
|
692
|
+
}
|
|
693
|
+
const maskLen = 8;
|
|
694
|
+
const first = showFirst > 0 ? value.slice(0, showFirst) : "";
|
|
695
|
+
const last = showLast > 0 ? value.slice(-showLast) : "";
|
|
696
|
+
return first + char.repeat(maskLen) + last;
|
|
697
|
+
}
|
|
698
|
+
function hash(value, salt = "") {
|
|
699
|
+
const str = salt + value;
|
|
700
|
+
let hash2 = 0;
|
|
701
|
+
for (let i = 0; i < str.length; i++) {
|
|
702
|
+
const char = str.charCodeAt(i);
|
|
703
|
+
hash2 = (hash2 << 5) - hash2 + char;
|
|
704
|
+
hash2 = hash2 & hash2;
|
|
705
|
+
}
|
|
706
|
+
const hex = Math.abs(hash2).toString(16).padStart(8, "0");
|
|
707
|
+
return hex + hex.slice(0, 8);
|
|
708
|
+
}
|
|
709
|
+
function redactValue(value, field, config) {
|
|
710
|
+
if (typeof value !== "string") return value;
|
|
711
|
+
if (!value) return value;
|
|
712
|
+
const shouldRedact = config.fields.some((f) => {
|
|
713
|
+
const fieldLower = field.toLowerCase();
|
|
714
|
+
const fLower = f.toLowerCase();
|
|
715
|
+
return fieldLower === fLower || fieldLower.endsWith("." + fLower) || fieldLower.includes("[" + fLower + "]");
|
|
716
|
+
});
|
|
717
|
+
if (!shouldRedact) return value;
|
|
718
|
+
if (config.customRedactor) {
|
|
719
|
+
return config.customRedactor(value, field);
|
|
720
|
+
}
|
|
721
|
+
switch (config.mode) {
|
|
722
|
+
case "mask":
|
|
723
|
+
return mask(value, {
|
|
724
|
+
char: config.maskChar || "*",
|
|
725
|
+
preserveLength: config.preserveLength,
|
|
726
|
+
showFirst: 2,
|
|
727
|
+
showLast: 2
|
|
728
|
+
});
|
|
729
|
+
case "hash":
|
|
730
|
+
return `[HASH:${hash(value)}]`;
|
|
731
|
+
case "remove":
|
|
732
|
+
return "[REDACTED]";
|
|
733
|
+
default:
|
|
734
|
+
return "[REDACTED]";
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function redactObject(obj, config, path = "") {
|
|
738
|
+
if (typeof obj === "string") {
|
|
739
|
+
return redactValue(obj, path, config);
|
|
740
|
+
}
|
|
741
|
+
if (Array.isArray(obj)) {
|
|
742
|
+
return obj.map((item, i) => redactObject(item, config, `${path}[${i}]`));
|
|
743
|
+
}
|
|
744
|
+
if (typeof obj === "object" && obj !== null) {
|
|
745
|
+
const result = {};
|
|
746
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
747
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
748
|
+
result[key] = redactObject(value, config, newPath);
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
return obj;
|
|
753
|
+
}
|
|
754
|
+
function createRedactor(config = {}) {
|
|
755
|
+
const fullConfig = {
|
|
756
|
+
fields: config.fields || DEFAULT_PII_FIELDS,
|
|
757
|
+
mode: config.mode || "mask",
|
|
758
|
+
maskChar: config.maskChar || "*",
|
|
759
|
+
preserveLength: config.preserveLength || false,
|
|
760
|
+
customRedactor: config.customRedactor
|
|
761
|
+
};
|
|
762
|
+
return (obj) => redactObject(obj, fullConfig);
|
|
763
|
+
}
|
|
764
|
+
function redactHeaders(headers, sensitiveHeaders = ["authorization", "cookie", "x-api-key", "x-auth-token"]) {
|
|
765
|
+
const result = {};
|
|
766
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
767
|
+
const keyLower = key.toLowerCase();
|
|
768
|
+
if (sensitiveHeaders.some((h) => keyLower === h.toLowerCase())) {
|
|
769
|
+
result[key] = "[REDACTED]";
|
|
770
|
+
} else {
|
|
771
|
+
result[key] = value;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
function redactQuery(query, sensitiveParams = ["token", "key", "secret", "password", "auth"]) {
|
|
777
|
+
const result = {};
|
|
778
|
+
for (const [key, value] of Object.entries(query)) {
|
|
779
|
+
const keyLower = key.toLowerCase();
|
|
780
|
+
if (sensitiveParams.some((p) => keyLower.includes(p.toLowerCase()))) {
|
|
781
|
+
result[key] = "[REDACTED]";
|
|
782
|
+
} else {
|
|
783
|
+
result[key] = value;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
function redactEmail(email) {
|
|
789
|
+
if (!email || !email.includes("@")) return mask(email);
|
|
790
|
+
const [, domain] = email.split("@");
|
|
791
|
+
return `****@${domain}`;
|
|
792
|
+
}
|
|
793
|
+
function redactCreditCard(cardNumber) {
|
|
794
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
795
|
+
if (cleaned.length < 4) return mask(cardNumber);
|
|
796
|
+
return "**** **** **** " + cleaned.slice(-4);
|
|
797
|
+
}
|
|
798
|
+
function redactPhone(phone) {
|
|
799
|
+
const cleaned = phone.replace(/\D/g, "");
|
|
800
|
+
if (cleaned.length < 4) return mask(phone);
|
|
801
|
+
return mask(phone, { preserveLength: true, showLast: 4 });
|
|
802
|
+
}
|
|
803
|
+
function redactIP(ip) {
|
|
804
|
+
if (ip.includes(":")) {
|
|
805
|
+
const parts2 = ip.split(":");
|
|
806
|
+
return parts2[0] + ":****:****:****";
|
|
807
|
+
}
|
|
808
|
+
const parts = ip.split(".");
|
|
809
|
+
if (parts.length !== 4) return mask(ip);
|
|
810
|
+
return `${parts[0]}.${parts[1]}.*.*`;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/middleware/audit/events.ts
|
|
814
|
+
function generateId() {
|
|
815
|
+
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
816
|
+
}
|
|
817
|
+
function severityToLevel(severity) {
|
|
818
|
+
switch (severity) {
|
|
819
|
+
case "low":
|
|
820
|
+
return "info";
|
|
821
|
+
case "medium":
|
|
822
|
+
return "warn";
|
|
823
|
+
case "high":
|
|
824
|
+
return "error";
|
|
825
|
+
case "critical":
|
|
826
|
+
return "critical";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
var SecurityEventTracker = class {
|
|
830
|
+
store;
|
|
831
|
+
defaultSeverity;
|
|
832
|
+
onEvent;
|
|
833
|
+
constructor(config) {
|
|
834
|
+
this.store = config.store;
|
|
835
|
+
this.defaultSeverity = config.defaultSeverity || "medium";
|
|
836
|
+
this.onEvent = config.onEvent;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Track a security event
|
|
840
|
+
*/
|
|
841
|
+
async track(options) {
|
|
842
|
+
const severity = options.severity || this.defaultSeverity;
|
|
843
|
+
const entry = {
|
|
844
|
+
id: generateId(),
|
|
845
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
846
|
+
type: "security",
|
|
847
|
+
level: severityToLevel(severity),
|
|
848
|
+
message: options.message,
|
|
849
|
+
event: options.event,
|
|
850
|
+
severity,
|
|
851
|
+
source: options.source || {},
|
|
852
|
+
target: options.target,
|
|
853
|
+
details: options.details,
|
|
854
|
+
mitigated: options.mitigated,
|
|
855
|
+
metadata: options.metadata
|
|
856
|
+
};
|
|
857
|
+
await this.store.write(entry);
|
|
858
|
+
if (this.onEvent) {
|
|
859
|
+
await this.onEvent(entry);
|
|
860
|
+
}
|
|
861
|
+
return entry;
|
|
862
|
+
}
|
|
863
|
+
// Convenience methods for common events
|
|
864
|
+
/**
|
|
865
|
+
* Track failed authentication
|
|
866
|
+
*/
|
|
867
|
+
async authFailed(options) {
|
|
868
|
+
return this.track({
|
|
869
|
+
event: "auth.failed",
|
|
870
|
+
message: options.reason || "Authentication failed",
|
|
871
|
+
severity: "medium",
|
|
872
|
+
source: {
|
|
873
|
+
ip: options.ip,
|
|
874
|
+
userAgent: options.userAgent
|
|
875
|
+
},
|
|
876
|
+
details: {
|
|
877
|
+
attemptedEmail: options.email,
|
|
878
|
+
reason: options.reason
|
|
879
|
+
},
|
|
880
|
+
metadata: options.metadata
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Track successful login
|
|
885
|
+
*/
|
|
886
|
+
async authLogin(options) {
|
|
887
|
+
return this.track({
|
|
888
|
+
event: "auth.login",
|
|
889
|
+
message: `User ${options.userId} logged in`,
|
|
890
|
+
severity: "low",
|
|
891
|
+
source: {
|
|
892
|
+
ip: options.ip,
|
|
893
|
+
userAgent: options.userAgent,
|
|
894
|
+
userId: options.userId
|
|
895
|
+
},
|
|
896
|
+
details: {
|
|
897
|
+
method: options.method || "credentials"
|
|
898
|
+
},
|
|
899
|
+
metadata: options.metadata
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Track logout
|
|
904
|
+
*/
|
|
905
|
+
async authLogout(options) {
|
|
906
|
+
return this.track({
|
|
907
|
+
event: "auth.logout",
|
|
908
|
+
message: `User ${options.userId} logged out`,
|
|
909
|
+
severity: "low",
|
|
910
|
+
source: {
|
|
911
|
+
ip: options.ip,
|
|
912
|
+
userId: options.userId
|
|
913
|
+
},
|
|
914
|
+
details: {
|
|
915
|
+
reason: options.reason || "user"
|
|
916
|
+
},
|
|
917
|
+
metadata: options.metadata
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Track permission denied
|
|
922
|
+
*/
|
|
923
|
+
async permissionDenied(options) {
|
|
924
|
+
return this.track({
|
|
925
|
+
event: "auth.permission_denied",
|
|
926
|
+
message: `Permission denied for ${options.action} on ${options.resource}`,
|
|
927
|
+
severity: "medium",
|
|
928
|
+
source: {
|
|
929
|
+
ip: options.ip,
|
|
930
|
+
userId: options.userId
|
|
931
|
+
},
|
|
932
|
+
target: {
|
|
933
|
+
resource: options.resource,
|
|
934
|
+
action: options.action
|
|
935
|
+
},
|
|
936
|
+
details: {
|
|
937
|
+
requiredRole: options.requiredRole
|
|
938
|
+
},
|
|
939
|
+
metadata: options.metadata
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Track rate limit exceeded
|
|
944
|
+
*/
|
|
945
|
+
async rateLimitExceeded(options) {
|
|
946
|
+
return this.track({
|
|
947
|
+
event: "ratelimit.exceeded",
|
|
948
|
+
message: `Rate limit exceeded for ${options.endpoint}`,
|
|
949
|
+
severity: "medium",
|
|
950
|
+
source: {
|
|
951
|
+
ip: options.ip,
|
|
952
|
+
userId: options.userId
|
|
953
|
+
},
|
|
954
|
+
target: {
|
|
955
|
+
resource: options.endpoint
|
|
956
|
+
},
|
|
957
|
+
details: {
|
|
958
|
+
limit: options.limit,
|
|
959
|
+
window: options.window
|
|
960
|
+
},
|
|
961
|
+
metadata: options.metadata
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Track CSRF validation failure
|
|
966
|
+
*/
|
|
967
|
+
async csrfInvalid(options) {
|
|
968
|
+
return this.track({
|
|
969
|
+
event: "csrf.invalid",
|
|
970
|
+
message: `CSRF validation failed for ${options.endpoint}`,
|
|
971
|
+
severity: "high",
|
|
972
|
+
source: {
|
|
973
|
+
ip: options.ip,
|
|
974
|
+
userId: options.userId
|
|
975
|
+
},
|
|
976
|
+
target: {
|
|
977
|
+
resource: options.endpoint
|
|
978
|
+
},
|
|
979
|
+
details: {
|
|
980
|
+
reason: options.reason
|
|
981
|
+
},
|
|
982
|
+
metadata: options.metadata
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Track XSS detection
|
|
987
|
+
*/
|
|
988
|
+
async xssDetected(options) {
|
|
989
|
+
return this.track({
|
|
990
|
+
event: "xss.detected",
|
|
991
|
+
message: `XSS payload detected in ${options.field}`,
|
|
992
|
+
severity: "high",
|
|
993
|
+
source: {
|
|
994
|
+
ip: options.ip,
|
|
995
|
+
userId: options.userId
|
|
996
|
+
},
|
|
997
|
+
target: {
|
|
998
|
+
resource: options.endpoint
|
|
999
|
+
},
|
|
1000
|
+
details: {
|
|
1001
|
+
field: options.field,
|
|
1002
|
+
payload: options.payload?.slice(0, 100)
|
|
1003
|
+
// Truncate
|
|
1004
|
+
},
|
|
1005
|
+
mitigated: true,
|
|
1006
|
+
metadata: options.metadata
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Track SQL injection detection
|
|
1011
|
+
*/
|
|
1012
|
+
async sqliDetected(options) {
|
|
1013
|
+
return this.track({
|
|
1014
|
+
event: "sqli.detected",
|
|
1015
|
+
message: `SQL injection attempt detected in ${options.field}`,
|
|
1016
|
+
severity: options.severity || "high",
|
|
1017
|
+
source: {
|
|
1018
|
+
ip: options.ip,
|
|
1019
|
+
userId: options.userId
|
|
1020
|
+
},
|
|
1021
|
+
target: {
|
|
1022
|
+
resource: options.endpoint
|
|
1023
|
+
},
|
|
1024
|
+
details: {
|
|
1025
|
+
field: options.field,
|
|
1026
|
+
pattern: options.pattern
|
|
1027
|
+
},
|
|
1028
|
+
mitigated: true,
|
|
1029
|
+
metadata: options.metadata
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Track IP block
|
|
1034
|
+
*/
|
|
1035
|
+
async ipBlocked(options) {
|
|
1036
|
+
return this.track({
|
|
1037
|
+
event: "ip.blocked",
|
|
1038
|
+
message: `IP ${options.ip} blocked: ${options.reason}`,
|
|
1039
|
+
severity: "high",
|
|
1040
|
+
source: {
|
|
1041
|
+
ip: options.ip
|
|
1042
|
+
},
|
|
1043
|
+
details: {
|
|
1044
|
+
reason: options.reason,
|
|
1045
|
+
duration: options.duration
|
|
1046
|
+
},
|
|
1047
|
+
metadata: options.metadata
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Track suspicious activity
|
|
1052
|
+
*/
|
|
1053
|
+
async suspicious(options) {
|
|
1054
|
+
return this.track({
|
|
1055
|
+
event: "ip.suspicious",
|
|
1056
|
+
message: options.activity,
|
|
1057
|
+
severity: options.severity || "medium",
|
|
1058
|
+
source: {
|
|
1059
|
+
ip: options.ip,
|
|
1060
|
+
userId: options.userId
|
|
1061
|
+
},
|
|
1062
|
+
details: options.details,
|
|
1063
|
+
metadata: options.metadata
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Track custom event
|
|
1068
|
+
*/
|
|
1069
|
+
async custom(options) {
|
|
1070
|
+
return this.track({
|
|
1071
|
+
event: "custom",
|
|
1072
|
+
...options
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
function createSecurityTracker(config) {
|
|
1077
|
+
return new SecurityEventTracker(config);
|
|
1078
|
+
}
|
|
1079
|
+
async function trackSecurityEvent(store, options) {
|
|
1080
|
+
const tracker = new SecurityEventTracker({ store });
|
|
1081
|
+
return tracker.track(options);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/middleware/audit/middleware.ts
|
|
1085
|
+
function generateRequestId() {
|
|
1086
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1087
|
+
}
|
|
1088
|
+
function getClientIP(req) {
|
|
1089
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || void 0;
|
|
1090
|
+
}
|
|
1091
|
+
function headersToRecord(headers, includeHeaders) {
|
|
1092
|
+
if (!includeHeaders) return {};
|
|
1093
|
+
const result = {};
|
|
1094
|
+
if (includeHeaders === true) {
|
|
1095
|
+
headers.forEach((value, key) => {
|
|
1096
|
+
result[key] = value;
|
|
1097
|
+
});
|
|
1098
|
+
} else if (Array.isArray(includeHeaders)) {
|
|
1099
|
+
for (const key of includeHeaders) {
|
|
1100
|
+
const value = headers.get(key);
|
|
1101
|
+
if (value) result[key] = value;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
function parseQuery(url) {
|
|
1107
|
+
const result = {};
|
|
1108
|
+
try {
|
|
1109
|
+
const urlObj = new URL(url);
|
|
1110
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
1111
|
+
result[key] = value;
|
|
1112
|
+
});
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
return result;
|
|
1116
|
+
}
|
|
1117
|
+
function statusToLevel(status) {
|
|
1118
|
+
if (status >= 500) return "error";
|
|
1119
|
+
if (status >= 400) return "warn";
|
|
1120
|
+
return "info";
|
|
1121
|
+
}
|
|
1122
|
+
function shouldSkip(req, status, exclude) {
|
|
1123
|
+
if (!exclude) return false;
|
|
1124
|
+
const url = new URL(req.url);
|
|
1125
|
+
if (exclude.paths?.length) {
|
|
1126
|
+
const matchesPath = exclude.paths.some((pattern) => {
|
|
1127
|
+
if (pattern.includes("*")) {
|
|
1128
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
1129
|
+
return regex.test(url.pathname);
|
|
1130
|
+
}
|
|
1131
|
+
return url.pathname === pattern || url.pathname.startsWith(pattern);
|
|
1132
|
+
});
|
|
1133
|
+
if (matchesPath) return true;
|
|
1134
|
+
}
|
|
1135
|
+
if (exclude.methods?.includes(req.method)) {
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
if (exclude.statusCodes?.includes(status)) {
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
function withAuditLog(handler, config) {
|
|
1144
|
+
const {
|
|
1145
|
+
enabled = true,
|
|
1146
|
+
store,
|
|
1147
|
+
include = {},
|
|
1148
|
+
exclude,
|
|
1149
|
+
pii,
|
|
1150
|
+
getUser,
|
|
1151
|
+
requestIdHeader = "x-request-id",
|
|
1152
|
+
generateRequestId: customGenerateId,
|
|
1153
|
+
onError,
|
|
1154
|
+
skip
|
|
1155
|
+
} = config;
|
|
1156
|
+
const includeSettings = {
|
|
1157
|
+
ip: include.ip ?? true,
|
|
1158
|
+
userAgent: include.userAgent ?? true,
|
|
1159
|
+
headers: include.headers ?? false,
|
|
1160
|
+
query: include.query ?? true,
|
|
1161
|
+
body: include.body ?? false,
|
|
1162
|
+
response: include.response ?? true,
|
|
1163
|
+
responseBody: include.responseBody ?? false,
|
|
1164
|
+
duration: include.duration ?? true,
|
|
1165
|
+
user: include.user ?? true
|
|
1166
|
+
};
|
|
1167
|
+
const piiConfig = pii || {
|
|
1168
|
+
fields: DEFAULT_PII_FIELDS,
|
|
1169
|
+
mode: "mask"
|
|
1170
|
+
};
|
|
1171
|
+
return async (req) => {
|
|
1172
|
+
if (!enabled) {
|
|
1173
|
+
return handler(req);
|
|
1174
|
+
}
|
|
1175
|
+
if (skip && await skip(req)) {
|
|
1176
|
+
return handler(req);
|
|
1177
|
+
}
|
|
1178
|
+
const startTime = Date.now();
|
|
1179
|
+
const requestId = req.headers.get(requestIdHeader) || (customGenerateId ? customGenerateId() : generateRequestId());
|
|
1180
|
+
const url = new URL(req.url);
|
|
1181
|
+
let requestInfo = {
|
|
1182
|
+
id: requestId,
|
|
1183
|
+
method: req.method,
|
|
1184
|
+
url: req.url,
|
|
1185
|
+
path: url.pathname
|
|
1186
|
+
};
|
|
1187
|
+
if (includeSettings.ip) {
|
|
1188
|
+
requestInfo.ip = getClientIP(req);
|
|
1189
|
+
}
|
|
1190
|
+
if (includeSettings.userAgent) {
|
|
1191
|
+
requestInfo.userAgent = req.headers.get("user-agent") || void 0;
|
|
1192
|
+
}
|
|
1193
|
+
if (includeSettings.headers) {
|
|
1194
|
+
let headers = headersToRecord(req.headers, includeSettings.headers);
|
|
1195
|
+
headers = redactHeaders(headers);
|
|
1196
|
+
requestInfo.headers = headers;
|
|
1197
|
+
}
|
|
1198
|
+
if (includeSettings.query) {
|
|
1199
|
+
let query = parseQuery(req.url);
|
|
1200
|
+
query = redactQuery(query);
|
|
1201
|
+
requestInfo.query = query;
|
|
1202
|
+
}
|
|
1203
|
+
requestInfo.contentType = req.headers.get("content-type") || void 0;
|
|
1204
|
+
requestInfo.contentLength = parseInt(req.headers.get("content-length") || "0", 10) || void 0;
|
|
1205
|
+
let user;
|
|
1206
|
+
if (includeSettings.user && getUser) {
|
|
1207
|
+
try {
|
|
1208
|
+
user = await getUser(req) || void 0;
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
let response;
|
|
1213
|
+
let error;
|
|
1214
|
+
try {
|
|
1215
|
+
response = await handler(req);
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
1218
|
+
throw err;
|
|
1219
|
+
} finally {
|
|
1220
|
+
const duration = Date.now() - startTime;
|
|
1221
|
+
const status = response?.status || 500;
|
|
1222
|
+
if (!shouldSkip(req, status, exclude)) {
|
|
1223
|
+
const entry = {
|
|
1224
|
+
id: requestId,
|
|
1225
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1226
|
+
type: "request",
|
|
1227
|
+
level: error ? "error" : statusToLevel(status),
|
|
1228
|
+
message: `${req.method} ${url.pathname} ${status} ${duration}ms`,
|
|
1229
|
+
request: requestInfo,
|
|
1230
|
+
user
|
|
1231
|
+
};
|
|
1232
|
+
if (includeSettings.response && response) {
|
|
1233
|
+
entry.response = {
|
|
1234
|
+
status: response.status,
|
|
1235
|
+
duration
|
|
1236
|
+
};
|
|
1237
|
+
if (includeSettings.headers) {
|
|
1238
|
+
entry.response.headers = headersToRecord(response.headers, includeSettings.headers);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (error) {
|
|
1242
|
+
entry.error = {
|
|
1243
|
+
name: error.name,
|
|
1244
|
+
message: error.message,
|
|
1245
|
+
stack: error.stack
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const redactedEntry = redactObject(entry, piiConfig);
|
|
1249
|
+
try {
|
|
1250
|
+
await store.write(redactedEntry);
|
|
1251
|
+
} catch (writeError) {
|
|
1252
|
+
if (onError) {
|
|
1253
|
+
onError(writeError instanceof Error ? writeError : new Error(String(writeError)), entry);
|
|
1254
|
+
} else {
|
|
1255
|
+
console.error("[AuditLog] Failed to write log:", writeError);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return response;
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function createAuditMiddleware(config) {
|
|
1264
|
+
return (handler) => withAuditLog(handler, config);
|
|
1265
|
+
}
|
|
1266
|
+
function withRequestId(handler, options = {}) {
|
|
1267
|
+
const { headerName = "x-request-id", generateId: generateId2 = generateRequestId } = options;
|
|
1268
|
+
return async (req) => {
|
|
1269
|
+
const requestId = req.headers.get(headerName) || generateId2();
|
|
1270
|
+
const response = await handler(req);
|
|
1271
|
+
const newResponse = new Response(response.body, {
|
|
1272
|
+
status: response.status,
|
|
1273
|
+
statusText: response.statusText,
|
|
1274
|
+
headers: new Headers(response.headers)
|
|
1275
|
+
});
|
|
1276
|
+
newResponse.headers.set(headerName, requestId);
|
|
1277
|
+
return newResponse;
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function withTiming(handler, options = {}) {
|
|
1281
|
+
const { headerName = "x-response-time", log = false } = options;
|
|
1282
|
+
return async (req) => {
|
|
1283
|
+
const start = Date.now();
|
|
1284
|
+
const response = await handler(req);
|
|
1285
|
+
const duration = Date.now() - start;
|
|
1286
|
+
const newResponse = new Response(response.body, {
|
|
1287
|
+
status: response.status,
|
|
1288
|
+
statusText: response.statusText,
|
|
1289
|
+
headers: new Headers(response.headers)
|
|
1290
|
+
});
|
|
1291
|
+
newResponse.headers.set(headerName, `${duration}ms`);
|
|
1292
|
+
if (log) {
|
|
1293
|
+
const url = new URL(req.url);
|
|
1294
|
+
console.log(`${req.method} ${url.pathname} ${response.status} ${duration}ms`);
|
|
1295
|
+
}
|
|
1296
|
+
return newResponse;
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
exports.CLFFormatter = CLFFormatter;
|
|
1301
|
+
exports.ConsoleStore = ConsoleStore;
|
|
1302
|
+
exports.DEFAULT_PII_FIELDS = DEFAULT_PII_FIELDS;
|
|
1303
|
+
exports.ExternalStore = ExternalStore;
|
|
1304
|
+
exports.JSONFormatter = JSONFormatter;
|
|
1305
|
+
exports.MemoryStore = MemoryStore;
|
|
1306
|
+
exports.MultiStore = MultiStore;
|
|
1307
|
+
exports.SecurityEventTracker = SecurityEventTracker;
|
|
1308
|
+
exports.StructuredFormatter = StructuredFormatter;
|
|
1309
|
+
exports.TextFormatter = TextFormatter;
|
|
1310
|
+
exports.createAuditMiddleware = createAuditMiddleware;
|
|
1311
|
+
exports.createCLFFormatter = createCLFFormatter;
|
|
1312
|
+
exports.createConsoleStore = createConsoleStore;
|
|
1313
|
+
exports.createDatadogStore = createDatadogStore;
|
|
1314
|
+
exports.createExternalStore = createExternalStore;
|
|
1315
|
+
exports.createJSONFormatter = createJSONFormatter;
|
|
1316
|
+
exports.createMemoryStore = createMemoryStore;
|
|
1317
|
+
exports.createMultiStore = createMultiStore;
|
|
1318
|
+
exports.createRedactor = createRedactor;
|
|
1319
|
+
exports.createSecurityTracker = createSecurityTracker;
|
|
1320
|
+
exports.createStructuredFormatter = createStructuredFormatter;
|
|
1321
|
+
exports.createTextFormatter = createTextFormatter;
|
|
1322
|
+
exports.hash = hash;
|
|
1323
|
+
exports.mask = mask;
|
|
1324
|
+
exports.redactCreditCard = redactCreditCard;
|
|
1325
|
+
exports.redactEmail = redactEmail;
|
|
1326
|
+
exports.redactHeaders = redactHeaders;
|
|
1327
|
+
exports.redactIP = redactIP;
|
|
1328
|
+
exports.redactObject = redactObject;
|
|
1329
|
+
exports.redactPhone = redactPhone;
|
|
1330
|
+
exports.redactQuery = redactQuery;
|
|
1331
|
+
exports.redactValue = redactValue;
|
|
1332
|
+
exports.trackSecurityEvent = trackSecurityEvent;
|
|
1333
|
+
exports.withAuditLog = withAuditLog;
|
|
1334
|
+
exports.withRequestId = withRequestId;
|
|
1335
|
+
exports.withTiming = withTiming;
|
|
1336
|
+
//# sourceMappingURL=audit.cjs.map
|
|
1337
|
+
//# sourceMappingURL=audit.cjs.map
|