@shieldpress/tracker 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/README.md +322 -0
- package/dist/express.d.mts +27 -0
- package/dist/express.d.ts +27 -0
- package/dist/express.js +1485 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +1454 -0
- package/dist/express.mjs.map +1 -0
- package/dist/index.d.mts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +1443 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1403 -0
- package/dist/index.mjs.map +1 -0
- package/dist/nextjs.d.mts +54 -0
- package/dist/nextjs.d.ts +54 -0
- package/dist/nextjs.js +1551 -0
- package/dist/nextjs.js.map +1 -0
- package/dist/nextjs.mjs +1517 -0
- package/dist/nextjs.mjs.map +1 -0
- package/dist/tracker-BJBniOSu.d.mts +379 -0
- package/dist/tracker-BJBniOSu.d.ts +379 -0
- package/package.json +74 -0
package/dist/nextjs.mjs
ADDED
|
@@ -0,0 +1,1517 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/config.ts
|
|
9
|
+
var DEFAULT_API_URL = "https://api.shieldpress.net";
|
|
10
|
+
function resolveConfig(config) {
|
|
11
|
+
if (!config.apiKey) throw new Error("[ShieldPress] apiKey is required");
|
|
12
|
+
if (!config.siteId) throw new Error("[ShieldPress] siteId is required");
|
|
13
|
+
return {
|
|
14
|
+
apiKey: config.apiKey,
|
|
15
|
+
siteId: config.siteId,
|
|
16
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
17
|
+
reportInterval: config.reportInterval ?? 60,
|
|
18
|
+
systemMetrics: config.systemMetrics ?? true,
|
|
19
|
+
httpTracking: config.httpTracking ?? true,
|
|
20
|
+
errorTracking: config.errorTracking ?? true,
|
|
21
|
+
securityTracking: config.securityTracking ?? true,
|
|
22
|
+
depAudit: config.depAudit ?? true,
|
|
23
|
+
depAuditInterval: config.depAuditInterval ?? 3600,
|
|
24
|
+
runtimeMetrics: config.runtimeMetrics ?? true,
|
|
25
|
+
envSecurity: config.envSecurity ?? true,
|
|
26
|
+
heartbeat: config.heartbeat ?? true,
|
|
27
|
+
appName: config.appName || "node-app",
|
|
28
|
+
appVersion: config.appVersion || "0.0.0",
|
|
29
|
+
environment: config.environment || process.env.NODE_ENV || "production",
|
|
30
|
+
tags: config.tags || {},
|
|
31
|
+
ignorePaths: config.ignorePaths || [
|
|
32
|
+
"/health",
|
|
33
|
+
"/healthz",
|
|
34
|
+
"/ready",
|
|
35
|
+
"/favicon.ico",
|
|
36
|
+
"/_next/static/*",
|
|
37
|
+
"/_next/image/*"
|
|
38
|
+
],
|
|
39
|
+
maxErrorBuffer: config.maxErrorBuffer ?? 100,
|
|
40
|
+
maxSecurityBuffer: config.maxSecurityBuffer ?? 500,
|
|
41
|
+
debug: config.debug ?? false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/logger.ts
|
|
46
|
+
function createLogger(debug) {
|
|
47
|
+
return {
|
|
48
|
+
info: (...args) => {
|
|
49
|
+
if (debug) console.log("[ShieldPress]", ...args);
|
|
50
|
+
},
|
|
51
|
+
warn: (...args) => {
|
|
52
|
+
console.warn("[ShieldPress]", ...args);
|
|
53
|
+
},
|
|
54
|
+
error: (...args) => {
|
|
55
|
+
console.error("[ShieldPress]", ...args);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/collectors/system.ts
|
|
61
|
+
import os, { networkInterfaces } from "os";
|
|
62
|
+
import { readFileSync, statfsSync } from "fs";
|
|
63
|
+
function getPrivateIp() {
|
|
64
|
+
try {
|
|
65
|
+
const nets = networkInterfaces();
|
|
66
|
+
for (const name of Object.keys(nets)) {
|
|
67
|
+
for (const net of nets[name] || []) {
|
|
68
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
69
|
+
return net.address;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return "";
|
|
74
|
+
} catch {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var cachedPublicIp = "";
|
|
79
|
+
var publicIpLastFetch = 0;
|
|
80
|
+
function getPublicIpCached() {
|
|
81
|
+
if (Date.now() - publicIpLastFetch > 6e5) {
|
|
82
|
+
publicIpLastFetch = Date.now();
|
|
83
|
+
try {
|
|
84
|
+
const http2 = __require("https");
|
|
85
|
+
const req = http2.get("https://api.ipify.org", { timeout: 3e3 }, (res) => {
|
|
86
|
+
let data = "";
|
|
87
|
+
res.on("data", (chunk) => {
|
|
88
|
+
data += chunk;
|
|
89
|
+
});
|
|
90
|
+
res.on("end", () => {
|
|
91
|
+
cachedPublicIp = data.trim();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
req.on("error", () => {
|
|
95
|
+
});
|
|
96
|
+
req.on("timeout", () => {
|
|
97
|
+
req.destroy();
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return cachedPublicIp;
|
|
103
|
+
}
|
|
104
|
+
function getDiskUsage() {
|
|
105
|
+
try {
|
|
106
|
+
if (typeof statfsSync === "function") {
|
|
107
|
+
const root = os.platform() === "win32" ? "C:\\" : "/";
|
|
108
|
+
const stats = statfsSync(root);
|
|
109
|
+
const totalBytes = stats.blocks * stats.bsize;
|
|
110
|
+
const freeBytes = stats.bfree * stats.bsize;
|
|
111
|
+
const usedBytes = totalBytes - freeBytes;
|
|
112
|
+
return {
|
|
113
|
+
totalGb: Math.round(totalBytes / 1073741824 * 100) / 100,
|
|
114
|
+
usedGb: Math.round(usedBytes / 1073741824 * 100) / 100,
|
|
115
|
+
percent: totalBytes > 0 ? Math.round(usedBytes / totalBytes * 1e4) / 100 : 0
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (os.platform() !== "win32") {
|
|
119
|
+
const { execSync } = __require("child_process");
|
|
120
|
+
const output = execSync("df -k / 2>/dev/null", { timeout: 3e3, encoding: "utf-8" });
|
|
121
|
+
const lines = output.trim().split("\n");
|
|
122
|
+
if (lines.length >= 2) {
|
|
123
|
+
const parts = lines[1].split(/\s+/);
|
|
124
|
+
const totalKb = parseInt(parts[1]) || 0;
|
|
125
|
+
const usedKb = parseInt(parts[2]) || 0;
|
|
126
|
+
return {
|
|
127
|
+
totalGb: Math.round(totalKb / 1048576 * 100) / 100,
|
|
128
|
+
usedGb: Math.round(usedKb / 1048576 * 100) / 100,
|
|
129
|
+
percent: totalKb > 0 ? Math.round(usedKb / totalKb * 1e4) / 100 : 0
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { usedGb: 0, totalGb: 0, percent: 0 };
|
|
134
|
+
} catch {
|
|
135
|
+
return { usedGb: 0, totalGb: 0, percent: 0 };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function getLinuxOsInfo() {
|
|
139
|
+
try {
|
|
140
|
+
const content = readFileSync("/etc/os-release", "utf-8");
|
|
141
|
+
const lines = content.split("\n");
|
|
142
|
+
let name = "Linux";
|
|
143
|
+
let version = "";
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (line.startsWith("NAME=")) {
|
|
146
|
+
name = line.split("=")[1]?.replace(/"/g, "") || "Linux";
|
|
147
|
+
}
|
|
148
|
+
if (line.startsWith("VERSION_ID=")) {
|
|
149
|
+
version = line.split("=")[1]?.replace(/"/g, "") || "";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { name, version };
|
|
153
|
+
} catch {
|
|
154
|
+
return { name: "Linux", version: "" };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function getOsInfo() {
|
|
158
|
+
const platform = os.platform();
|
|
159
|
+
if (platform === "linux") {
|
|
160
|
+
return getLinuxOsInfo();
|
|
161
|
+
}
|
|
162
|
+
if (platform === "darwin") {
|
|
163
|
+
return { name: "macOS", version: os.release() };
|
|
164
|
+
}
|
|
165
|
+
if (platform === "win32") {
|
|
166
|
+
return { name: "Windows", version: os.release() };
|
|
167
|
+
}
|
|
168
|
+
return { name: platform, version: os.release() };
|
|
169
|
+
}
|
|
170
|
+
var prevCpuTimes = null;
|
|
171
|
+
function getCpuPercent() {
|
|
172
|
+
const cpus = os.cpus();
|
|
173
|
+
let idle = 0;
|
|
174
|
+
let total = 0;
|
|
175
|
+
for (const cpu of cpus) {
|
|
176
|
+
idle += cpu.times.idle;
|
|
177
|
+
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
178
|
+
}
|
|
179
|
+
if (!prevCpuTimes) {
|
|
180
|
+
prevCpuTimes = { idle, total };
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const idleDiff = idle - prevCpuTimes.idle;
|
|
184
|
+
const totalDiff = total - prevCpuTimes.total;
|
|
185
|
+
prevCpuTimes = { idle, total };
|
|
186
|
+
if (totalDiff === 0) return 0;
|
|
187
|
+
return Math.round((1 - idleDiff / totalDiff) * 1e4) / 100;
|
|
188
|
+
}
|
|
189
|
+
function collectSystemMetrics() {
|
|
190
|
+
try {
|
|
191
|
+
const mem = process.memoryUsage();
|
|
192
|
+
const totalMem = os.totalmem();
|
|
193
|
+
const freeMem = os.freemem();
|
|
194
|
+
const loadAvg = os.loadavg();
|
|
195
|
+
const osInfo = getOsInfo();
|
|
196
|
+
const disk = getDiskUsage();
|
|
197
|
+
return {
|
|
198
|
+
cpuPercent: getCpuPercent(),
|
|
199
|
+
cpuCount: os.cpus().length,
|
|
200
|
+
memUsedMb: Math.round((totalMem - freeMem) / 1048576),
|
|
201
|
+
memTotalMb: Math.round(totalMem / 1048576),
|
|
202
|
+
memPercent: Math.round((totalMem - freeMem) / totalMem * 1e4) / 100,
|
|
203
|
+
heapUsedMb: Math.round(mem.heapUsed / 1048576),
|
|
204
|
+
heapTotalMb: Math.round(mem.heapTotal / 1048576),
|
|
205
|
+
externalMb: Math.round(mem.external / 1048576),
|
|
206
|
+
rss: Math.round(mem.rss / 1048576),
|
|
207
|
+
uptimeSeconds: Math.round(process.uptime()),
|
|
208
|
+
loadAvg,
|
|
209
|
+
platform: os.platform(),
|
|
210
|
+
nodeVersion: process.version,
|
|
211
|
+
pid: process.pid,
|
|
212
|
+
osName: osInfo.name,
|
|
213
|
+
osVersion: osInfo.version,
|
|
214
|
+
osKernel: os.release(),
|
|
215
|
+
osArch: os.arch(),
|
|
216
|
+
osRelease: (() => {
|
|
217
|
+
try {
|
|
218
|
+
return os.version();
|
|
219
|
+
} catch {
|
|
220
|
+
return os.release();
|
|
221
|
+
}
|
|
222
|
+
})(),
|
|
223
|
+
diskUsedGb: disk.usedGb,
|
|
224
|
+
diskTotalGb: disk.totalGb,
|
|
225
|
+
diskPercent: disk.percent,
|
|
226
|
+
// Network
|
|
227
|
+
publicIp: getPublicIpCached(),
|
|
228
|
+
privateIp: getPrivateIp(),
|
|
229
|
+
hostname: os.hostname(),
|
|
230
|
+
// Process details
|
|
231
|
+
ppid: process.ppid || 0,
|
|
232
|
+
uid: process.getuid?.() ?? null,
|
|
233
|
+
cwd: process.cwd(),
|
|
234
|
+
execPath: process.execPath,
|
|
235
|
+
nodeArgs: process.execArgv.join(" ")
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
return {
|
|
239
|
+
cpuPercent: 0,
|
|
240
|
+
cpuCount: 0,
|
|
241
|
+
memUsedMb: 0,
|
|
242
|
+
memTotalMb: 0,
|
|
243
|
+
memPercent: 0,
|
|
244
|
+
heapUsedMb: 0,
|
|
245
|
+
heapTotalMb: 0,
|
|
246
|
+
externalMb: 0,
|
|
247
|
+
rss: 0,
|
|
248
|
+
uptimeSeconds: 0,
|
|
249
|
+
loadAvg: [0, 0, 0],
|
|
250
|
+
platform: process.platform || "unknown",
|
|
251
|
+
nodeVersion: process.version || "unknown",
|
|
252
|
+
pid: process.pid || 0,
|
|
253
|
+
osName: "unknown",
|
|
254
|
+
osVersion: "",
|
|
255
|
+
osKernel: "",
|
|
256
|
+
osArch: "",
|
|
257
|
+
osRelease: "",
|
|
258
|
+
diskUsedGb: 0,
|
|
259
|
+
diskTotalGb: 0,
|
|
260
|
+
diskPercent: 0,
|
|
261
|
+
publicIp: "",
|
|
262
|
+
privateIp: "",
|
|
263
|
+
hostname: "",
|
|
264
|
+
ppid: 0,
|
|
265
|
+
uid: null,
|
|
266
|
+
cwd: "",
|
|
267
|
+
execPath: "",
|
|
268
|
+
nodeArgs: ""
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/collectors/http.ts
|
|
274
|
+
var HttpCollector = class {
|
|
275
|
+
constructor() {
|
|
276
|
+
this.records = [];
|
|
277
|
+
this.windowStart = Date.now();
|
|
278
|
+
}
|
|
279
|
+
record(req) {
|
|
280
|
+
try {
|
|
281
|
+
if (this.records.length >= 1e4) {
|
|
282
|
+
this.records.splice(0, 1e3);
|
|
283
|
+
}
|
|
284
|
+
this.records.push({ ...req, timestamp: Date.now() });
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
flush() {
|
|
289
|
+
try {
|
|
290
|
+
return this._flush();
|
|
291
|
+
} catch {
|
|
292
|
+
this.records = [];
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
_flush() {
|
|
297
|
+
if (this.records.length === 0) return null;
|
|
298
|
+
const records = this.records;
|
|
299
|
+
this.records = [];
|
|
300
|
+
const windowMs = Date.now() - this.windowStart;
|
|
301
|
+
this.windowStart = Date.now();
|
|
302
|
+
const durations = records.map((r) => r.durationMs).sort((a, b) => a - b);
|
|
303
|
+
const statusCodes = {};
|
|
304
|
+
const pathMap = /* @__PURE__ */ new Map();
|
|
305
|
+
for (const r of records) {
|
|
306
|
+
const key = `${r.statusCode}`;
|
|
307
|
+
statusCodes[key] = (statusCodes[key] || 0) + 1;
|
|
308
|
+
const pathKey = `${r.method}:${r.path}`;
|
|
309
|
+
if (!pathMap.has(pathKey)) pathMap.set(pathKey, []);
|
|
310
|
+
pathMap.get(pathKey).push(r);
|
|
311
|
+
}
|
|
312
|
+
const topPaths = [...pathMap.entries()].map(([key, recs]) => {
|
|
313
|
+
const [method, path] = key.split(":", 2);
|
|
314
|
+
const pathDurations = recs.map((r) => r.durationMs).sort((a, b) => a - b);
|
|
315
|
+
return {
|
|
316
|
+
path,
|
|
317
|
+
method,
|
|
318
|
+
count: recs.length,
|
|
319
|
+
avgMs: Math.round(pathDurations.reduce((a, b) => a + b, 0) / recs.length),
|
|
320
|
+
p95Ms: percentile(pathDurations, 95),
|
|
321
|
+
errorCount: recs.filter((r) => r.statusCode >= 400).length
|
|
322
|
+
};
|
|
323
|
+
}).sort((a, b) => b.count - a.count).slice(0, 20);
|
|
324
|
+
return {
|
|
325
|
+
totalRequests: records.length,
|
|
326
|
+
totalErrors: records.filter((r) => r.statusCode >= 500).length,
|
|
327
|
+
avgResponseMs: Math.round(durations.reduce((a, b) => a + b, 0) / durations.length),
|
|
328
|
+
p50ResponseMs: percentile(durations, 50),
|
|
329
|
+
p95ResponseMs: percentile(durations, 95),
|
|
330
|
+
p99ResponseMs: percentile(durations, 99),
|
|
331
|
+
maxResponseMs: durations[durations.length - 1] || 0,
|
|
332
|
+
statusCodes,
|
|
333
|
+
topPaths,
|
|
334
|
+
requestsPerSecond: Math.round(records.length / (windowMs / 1e3) * 100) / 100
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
function percentile(sorted, p) {
|
|
339
|
+
if (sorted.length === 0) return 0;
|
|
340
|
+
const idx = Math.ceil(p / 100 * sorted.length) - 1;
|
|
341
|
+
return sorted[Math.max(0, idx)];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/collectors/errors.ts
|
|
345
|
+
var ErrorCollector = class {
|
|
346
|
+
constructor(maxBuffer) {
|
|
347
|
+
this.buffer = [];
|
|
348
|
+
this.maxBuffer = maxBuffer;
|
|
349
|
+
}
|
|
350
|
+
capture(error, meta) {
|
|
351
|
+
try {
|
|
352
|
+
if (this.buffer.length >= this.maxBuffer) {
|
|
353
|
+
this.buffer.shift();
|
|
354
|
+
}
|
|
355
|
+
const err = typeof error === "string" ? new Error(error) : error;
|
|
356
|
+
const message = (err.message || "").slice(0, 4096);
|
|
357
|
+
const stack = err.stack ? err.stack.slice(0, 4096) : void 0;
|
|
358
|
+
this.buffer.push({
|
|
359
|
+
message,
|
|
360
|
+
stack,
|
|
361
|
+
type: err.constructor.name || "Error",
|
|
362
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
363
|
+
url: meta?.url ? String(meta.url).slice(0, 2048) : void 0,
|
|
364
|
+
method: meta?.method ? String(meta.method).slice(0, 10) : void 0,
|
|
365
|
+
statusCode: meta?.statusCode,
|
|
366
|
+
meta: meta ? Object.fromEntries(
|
|
367
|
+
Object.entries(meta).filter(([k]) => !["url", "method", "statusCode"].includes(k))
|
|
368
|
+
) : void 0
|
|
369
|
+
});
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
flush() {
|
|
374
|
+
const errors = this.buffer;
|
|
375
|
+
this.buffer = [];
|
|
376
|
+
return errors;
|
|
377
|
+
}
|
|
378
|
+
get size() {
|
|
379
|
+
return this.buffer.length;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/collectors/security.ts
|
|
384
|
+
var SQL_INJECTION_PATTERNS = [
|
|
385
|
+
/(\b(union|select|insert|update|delete|drop|alter|create|exec|execute)\b.*\b(from|into|table|database|where)\b)/i,
|
|
386
|
+
/(--|;|\/\*|\*\/|xp_|sp_|0x[0-9a-f]+)/i,
|
|
387
|
+
/('|\"|`).*(\bor\b|\band\b).*('|\"|`).*=/i,
|
|
388
|
+
/(\bwaitfor\b\s+\bdelay\b|\bsleep\b\s*\(|\bbenchmark\b\s*\()/i
|
|
389
|
+
];
|
|
390
|
+
var XSS_PATTERNS = [
|
|
391
|
+
/<script[\s>]/i,
|
|
392
|
+
/javascript\s*:/i,
|
|
393
|
+
/on(load|error|click|mouseover|focus|blur|submit|change|input)\s*=/i,
|
|
394
|
+
/<(iframe|object|embed|form|img)\b[^>]*(src|data|action)\s*=\s*['"]/i,
|
|
395
|
+
/\beval\s*\(/i,
|
|
396
|
+
/\bdocument\.(cookie|location|write)/i
|
|
397
|
+
];
|
|
398
|
+
var CMD_INJECTION_PATTERNS = [
|
|
399
|
+
/[;&|`$]\s*(cat|ls|rm|mv|cp|wget|curl|nc|bash|sh|python|perl|ruby|node)\b/i,
|
|
400
|
+
/\$\(.*\)/,
|
|
401
|
+
/`[^`]*`/,
|
|
402
|
+
/\|\|\s*\w+/,
|
|
403
|
+
/&&\s*(rm|cat|wget|curl)/i
|
|
404
|
+
];
|
|
405
|
+
var PATH_TRAVERSAL_PATTERNS = [
|
|
406
|
+
/\.\.\//,
|
|
407
|
+
/\.\.\\/,
|
|
408
|
+
/%2e%2e/i,
|
|
409
|
+
/%252e%252e/i,
|
|
410
|
+
/\.\.\%00/
|
|
411
|
+
];
|
|
412
|
+
var SUSPICIOUS_PATHS = [
|
|
413
|
+
/\/(wp-admin|wp-login|xmlrpc|wp-content|wp-includes)\b/i,
|
|
414
|
+
/\/(phpmyadmin|adminer|phpinfo|server-status|server-info)\b/i,
|
|
415
|
+
/\.(env|git|svn|htaccess|htpasswd|bak|sql|dump|log|ini|cfg|conf)$/i,
|
|
416
|
+
/\/(\.git|\.svn|\.env|\.aws|\.ssh|\.docker)\b/,
|
|
417
|
+
/\/(config|backup|admin|debug|console|shell|cmd)\b/i,
|
|
418
|
+
/\/(actuator|swagger|api-docs|graphiql)\b/i
|
|
419
|
+
];
|
|
420
|
+
var BOT_USER_AGENTS = [
|
|
421
|
+
/sqlmap/i,
|
|
422
|
+
/nikto/i,
|
|
423
|
+
/nmap/i,
|
|
424
|
+
/masscan/i,
|
|
425
|
+
/zap\//i,
|
|
426
|
+
/burp/i,
|
|
427
|
+
/dirbuster/i,
|
|
428
|
+
/gobuster/i,
|
|
429
|
+
/wfuzz/i,
|
|
430
|
+
/hydra/i,
|
|
431
|
+
/metasploit/i,
|
|
432
|
+
/nessus/i,
|
|
433
|
+
/openvas/i,
|
|
434
|
+
/acunetix/i,
|
|
435
|
+
/qualys/i,
|
|
436
|
+
/nuclei/i,
|
|
437
|
+
/whatweb/i,
|
|
438
|
+
/wpscan/i,
|
|
439
|
+
/joomscan/i,
|
|
440
|
+
/skipfish/i
|
|
441
|
+
];
|
|
442
|
+
var SENSITIVE_DATA_PATTERNS = [
|
|
443
|
+
// API keys / tokens
|
|
444
|
+
/(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token|secret[_-]?key)\s*[=:]\s*['"][a-zA-Z0-9_\-]{20,}/i,
|
|
445
|
+
// AWS keys
|
|
446
|
+
/AKIA[0-9A-Z]{16}/,
|
|
447
|
+
// Private keys
|
|
448
|
+
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/,
|
|
449
|
+
// JWT tokens
|
|
450
|
+
/eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/,
|
|
451
|
+
// Passwords in URLs
|
|
452
|
+
/[?&](password|passwd|pwd|secret|token)=[^&\s]{3,}/i
|
|
453
|
+
];
|
|
454
|
+
var SecurityCollector = class {
|
|
455
|
+
constructor(maxBuffer) {
|
|
456
|
+
this.events = [];
|
|
457
|
+
this.ipCounter = /* @__PURE__ */ new Map();
|
|
458
|
+
this.maxBuffer = maxBuffer;
|
|
459
|
+
}
|
|
460
|
+
/** Record a security event directly */
|
|
461
|
+
record(event) {
|
|
462
|
+
if (this.events.length >= this.maxBuffer) {
|
|
463
|
+
this.events.shift();
|
|
464
|
+
}
|
|
465
|
+
this.events.push({ ...event, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
466
|
+
if (event.ip) this.trackIp(event.ip, event.type);
|
|
467
|
+
}
|
|
468
|
+
/** Analyze an incoming HTTP request for security threats */
|
|
469
|
+
analyzeRequest(req) {
|
|
470
|
+
try {
|
|
471
|
+
this._analyzeRequest(req);
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
_analyzeRequest(req) {
|
|
476
|
+
const ip = req.ip || "unknown";
|
|
477
|
+
const ua = (typeof req.headers["user-agent"] === "string" ? req.headers["user-agent"] : "") || "";
|
|
478
|
+
const url = req.url;
|
|
479
|
+
const fullInput = `${url} ${req.body || ""} ${JSON.stringify(req.query || {})}`;
|
|
480
|
+
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
481
|
+
if (pattern.test(url)) {
|
|
482
|
+
this.record({
|
|
483
|
+
type: "suspicious_path",
|
|
484
|
+
severity: "high",
|
|
485
|
+
message: `Path traversal attempt: ${url.slice(0, 200)}`,
|
|
486
|
+
ip,
|
|
487
|
+
userAgent: ua,
|
|
488
|
+
url,
|
|
489
|
+
method: req.method
|
|
490
|
+
});
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const pattern of SUSPICIOUS_PATHS) {
|
|
495
|
+
if (pattern.test(url)) {
|
|
496
|
+
this.record({
|
|
497
|
+
type: "suspicious_path",
|
|
498
|
+
severity: "medium",
|
|
499
|
+
message: `Suspicious path access: ${url.slice(0, 200)}`,
|
|
500
|
+
ip,
|
|
501
|
+
userAgent: ua,
|
|
502
|
+
url,
|
|
503
|
+
method: req.method
|
|
504
|
+
});
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
509
|
+
if (pattern.test(fullInput)) {
|
|
510
|
+
this.record({
|
|
511
|
+
type: "sql_injection",
|
|
512
|
+
severity: "critical",
|
|
513
|
+
message: `SQL injection pattern detected in request`,
|
|
514
|
+
ip,
|
|
515
|
+
userAgent: ua,
|
|
516
|
+
url,
|
|
517
|
+
method: req.method,
|
|
518
|
+
payload: fullInput.slice(0, 500)
|
|
519
|
+
});
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const pattern of XSS_PATTERNS) {
|
|
524
|
+
if (pattern.test(fullInput)) {
|
|
525
|
+
this.record({
|
|
526
|
+
type: "xss_attempt",
|
|
527
|
+
severity: "high",
|
|
528
|
+
message: `XSS payload detected in request`,
|
|
529
|
+
ip,
|
|
530
|
+
userAgent: ua,
|
|
531
|
+
url,
|
|
532
|
+
method: req.method,
|
|
533
|
+
payload: fullInput.slice(0, 500)
|
|
534
|
+
});
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const pattern of CMD_INJECTION_PATTERNS) {
|
|
539
|
+
if (pattern.test(fullInput)) {
|
|
540
|
+
this.record({
|
|
541
|
+
type: "command_injection",
|
|
542
|
+
severity: "critical",
|
|
543
|
+
message: `Command injection pattern detected`,
|
|
544
|
+
ip,
|
|
545
|
+
userAgent: ua,
|
|
546
|
+
url,
|
|
547
|
+
method: req.method,
|
|
548
|
+
payload: fullInput.slice(0, 500)
|
|
549
|
+
});
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
for (const pattern of BOT_USER_AGENTS) {
|
|
554
|
+
if (pattern.test(ua)) {
|
|
555
|
+
this.record({
|
|
556
|
+
type: "bot_detected",
|
|
557
|
+
severity: "medium",
|
|
558
|
+
message: `Security scanner/bot detected: ${ua.slice(0, 100)}`,
|
|
559
|
+
ip,
|
|
560
|
+
userAgent: ua,
|
|
561
|
+
url,
|
|
562
|
+
method: req.method
|
|
563
|
+
});
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const contentLength = parseInt(String(req.headers["content-length"] || "0"), 10);
|
|
568
|
+
if (contentLength > 10 * 1024 * 1024) {
|
|
569
|
+
this.record({
|
|
570
|
+
type: "oversized_payload",
|
|
571
|
+
severity: "medium",
|
|
572
|
+
message: `Oversized request: ${(contentLength / 1024 / 1024).toFixed(1)}MB`,
|
|
573
|
+
ip,
|
|
574
|
+
userAgent: ua,
|
|
575
|
+
url,
|
|
576
|
+
method: req.method
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
for (const pattern of SENSITIVE_DATA_PATTERNS) {
|
|
580
|
+
if (pattern.test(url)) {
|
|
581
|
+
this.record({
|
|
582
|
+
type: "api_key_exposed",
|
|
583
|
+
severity: "critical",
|
|
584
|
+
message: `Sensitive data exposed in URL`,
|
|
585
|
+
ip,
|
|
586
|
+
url: url.slice(0, 100) + "...[REDACTED]",
|
|
587
|
+
method: req.method
|
|
588
|
+
});
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
this.checkBruteForce(ip);
|
|
593
|
+
}
|
|
594
|
+
/** Analyze response for sensitive data leaks */
|
|
595
|
+
analyzeResponse(data) {
|
|
596
|
+
try {
|
|
597
|
+
if (!data.body) return;
|
|
598
|
+
const contentType = data.headers?.["content-type"] || "";
|
|
599
|
+
if (!contentType.includes("json") && !contentType.includes("html") && !contentType.includes("text")) return;
|
|
600
|
+
const sample = data.body.slice(0, 10240);
|
|
601
|
+
for (const pattern of SENSITIVE_DATA_PATTERNS) {
|
|
602
|
+
if (pattern.test(sample)) {
|
|
603
|
+
this.record({
|
|
604
|
+
type: "sensitive_data_exposed",
|
|
605
|
+
severity: "critical",
|
|
606
|
+
message: `Sensitive data detected in response body for ${data.url}`,
|
|
607
|
+
url: data.url,
|
|
608
|
+
meta: { statusCode: data.statusCode }
|
|
609
|
+
});
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/** Record an authentication failure */
|
|
617
|
+
recordAuthFailure(data) {
|
|
618
|
+
this.record({
|
|
619
|
+
type: "auth_failure",
|
|
620
|
+
severity: "medium",
|
|
621
|
+
message: `Authentication failed${data.reason ? `: ${data.reason}` : ""}`,
|
|
622
|
+
...data
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/** Record a rate limit event */
|
|
626
|
+
recordRateLimit(data) {
|
|
627
|
+
this.record({
|
|
628
|
+
type: "rate_limit_hit",
|
|
629
|
+
severity: "low",
|
|
630
|
+
message: `Rate limit triggered${data.limit ? ` (limit: ${data.limit})` : ""}`,
|
|
631
|
+
...data
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/** Record a CORS violation */
|
|
635
|
+
recordCorsViolation(data) {
|
|
636
|
+
this.record({
|
|
637
|
+
type: "cors_violation",
|
|
638
|
+
severity: "medium",
|
|
639
|
+
message: `CORS policy blocked request from origin: ${data.origin || "unknown"}`,
|
|
640
|
+
...data
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/** Record a custom security event */
|
|
644
|
+
recordCustom(type, severity, message, meta) {
|
|
645
|
+
this.record({ type, severity, message, meta });
|
|
646
|
+
}
|
|
647
|
+
/** Flush and return aggregated security metrics */
|
|
648
|
+
flush() {
|
|
649
|
+
try {
|
|
650
|
+
if (this.events.length === 0 && this.ipCounter.size === 0) return null;
|
|
651
|
+
const events = this.events;
|
|
652
|
+
this.events = [];
|
|
653
|
+
const bySeverity = {
|
|
654
|
+
critical: 0,
|
|
655
|
+
high: 0,
|
|
656
|
+
medium: 0,
|
|
657
|
+
low: 0,
|
|
658
|
+
info: 0
|
|
659
|
+
};
|
|
660
|
+
const byType = {};
|
|
661
|
+
for (const e of events) {
|
|
662
|
+
bySeverity[e.severity]++;
|
|
663
|
+
byType[e.type] = (byType[e.type] || 0) + 1;
|
|
664
|
+
}
|
|
665
|
+
const topOffendingIps = [...this.ipCounter.entries()].map(([ip, data]) => ({ ip, count: data.count, types: [...data.types] })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
666
|
+
const cutoff = Date.now() - 36e5;
|
|
667
|
+
for (const [ip, data] of this.ipCounter.entries()) {
|
|
668
|
+
if (data.firstSeen < cutoff) this.ipCounter.delete(ip);
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
totalEvents: events.length,
|
|
672
|
+
bySeverity,
|
|
673
|
+
byType,
|
|
674
|
+
topOffendingIps,
|
|
675
|
+
events: events.slice(-50)
|
|
676
|
+
// Send last 50 events per report
|
|
677
|
+
};
|
|
678
|
+
} catch {
|
|
679
|
+
this.events = [];
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
trackIp(ip, type) {
|
|
684
|
+
const entry = this.ipCounter.get(ip);
|
|
685
|
+
if (entry) {
|
|
686
|
+
entry.count++;
|
|
687
|
+
entry.types.add(type);
|
|
688
|
+
} else {
|
|
689
|
+
this.ipCounter.set(ip, { count: 1, types: /* @__PURE__ */ new Set([type]), firstSeen: Date.now() });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
checkBruteForce(ip) {
|
|
693
|
+
const entry = this.ipCounter.get(ip);
|
|
694
|
+
if (!entry) return;
|
|
695
|
+
if (entry.count === 50) {
|
|
696
|
+
this.record({
|
|
697
|
+
type: "auth_brute_force",
|
|
698
|
+
severity: "critical",
|
|
699
|
+
message: `Possible brute-force/scan from IP: ${ip} (${entry.count} events)`,
|
|
700
|
+
ip
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// src/collectors/dep-audit.ts
|
|
707
|
+
import { execFile } from "child_process";
|
|
708
|
+
import { readFile } from "fs/promises";
|
|
709
|
+
import { join } from "path";
|
|
710
|
+
var DepAuditCollector = class {
|
|
711
|
+
constructor(auditIntervalMs, logger) {
|
|
712
|
+
this.lastResult = null;
|
|
713
|
+
this.lastAuditTime = 0;
|
|
714
|
+
this.auditing = false;
|
|
715
|
+
this.auditIntervalMs = auditIntervalMs;
|
|
716
|
+
this.logger = logger;
|
|
717
|
+
}
|
|
718
|
+
/** Run npm audit and parse results */
|
|
719
|
+
async collect() {
|
|
720
|
+
if (Date.now() - this.lastAuditTime < this.auditIntervalMs) {
|
|
721
|
+
return this.lastResult;
|
|
722
|
+
}
|
|
723
|
+
if (this.auditing) return this.lastResult;
|
|
724
|
+
this.auditing = true;
|
|
725
|
+
try {
|
|
726
|
+
const [auditResult, pkgInfo] = await Promise.all([
|
|
727
|
+
this.runNpmAudit(),
|
|
728
|
+
this.readPackageInfo()
|
|
729
|
+
]);
|
|
730
|
+
this.lastResult = {
|
|
731
|
+
totalDeps: pkgInfo.totalDeps,
|
|
732
|
+
totalVulnerabilities: auditResult.vulnerabilities.length,
|
|
733
|
+
bySeverity: auditResult.bySeverity,
|
|
734
|
+
vulnerabilities: auditResult.vulnerabilities.slice(0, 50),
|
|
735
|
+
// Top 50
|
|
736
|
+
outdatedCount: pkgInfo.outdatedCount,
|
|
737
|
+
lastAuditAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
738
|
+
};
|
|
739
|
+
this.lastAuditTime = Date.now();
|
|
740
|
+
this.logger.info(`Dep audit: ${this.lastResult.totalVulnerabilities} vulnerabilities found`);
|
|
741
|
+
return this.lastResult;
|
|
742
|
+
} catch (err) {
|
|
743
|
+
this.logger.error("Dep audit failed:", err instanceof Error ? err.message : err);
|
|
744
|
+
return this.lastResult;
|
|
745
|
+
} finally {
|
|
746
|
+
this.auditing = false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
runNpmAudit() {
|
|
750
|
+
return new Promise((resolve) => {
|
|
751
|
+
execFile("npm", ["audit", "--json"], { timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
|
|
752
|
+
try {
|
|
753
|
+
const data = JSON.parse(stdout || "{}");
|
|
754
|
+
const vulnerabilities = [];
|
|
755
|
+
const bySeverity = { critical: 0, high: 0, moderate: 0, low: 0 };
|
|
756
|
+
if (data.vulnerabilities) {
|
|
757
|
+
for (const [name, vuln] of Object.entries(data.vulnerabilities)) {
|
|
758
|
+
const severity = vuln.severity || "low";
|
|
759
|
+
bySeverity[severity] = (bySeverity[severity] || 0) + 1;
|
|
760
|
+
vulnerabilities.push({
|
|
761
|
+
package: name,
|
|
762
|
+
installedVersion: vuln.range || "unknown",
|
|
763
|
+
severity,
|
|
764
|
+
title: vuln.title || vuln.name || name,
|
|
765
|
+
url: vuln.url,
|
|
766
|
+
patchedVersions: vuln.fixAvailable?.version,
|
|
767
|
+
cwe: vuln.cwe ? Array.isArray(vuln.cwe) ? vuln.cwe : [vuln.cwe] : void 0
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 };
|
|
772
|
+
vulnerabilities.sort(
|
|
773
|
+
(a, b) => (severityOrder[a.severity] ?? 4) - (severityOrder[b.severity] ?? 4)
|
|
774
|
+
);
|
|
775
|
+
resolve({ vulnerabilities, bySeverity });
|
|
776
|
+
} catch {
|
|
777
|
+
resolve({ vulnerabilities: [], bySeverity: {} });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
async readPackageInfo() {
|
|
783
|
+
try {
|
|
784
|
+
const lockPath = join(process.cwd(), "package-lock.json");
|
|
785
|
+
const content = await readFile(lockPath, "utf-8");
|
|
786
|
+
const lock = JSON.parse(content);
|
|
787
|
+
const packages = lock.packages || {};
|
|
788
|
+
const totalDeps = Object.keys(packages).filter((k) => k !== "").length;
|
|
789
|
+
return { totalDeps, outdatedCount: 0 };
|
|
790
|
+
} catch {
|
|
791
|
+
try {
|
|
792
|
+
const pkgPath = join(process.cwd(), "package.json");
|
|
793
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
794
|
+
const pkg = JSON.parse(content);
|
|
795
|
+
const deps = Object.keys(pkg.dependencies || {}).length;
|
|
796
|
+
const devDeps = Object.keys(pkg.devDependencies || {}).length;
|
|
797
|
+
return { totalDeps: deps + devDeps, outdatedCount: 0 };
|
|
798
|
+
} catch {
|
|
799
|
+
return { totalDeps: 0, outdatedCount: 0 };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/collectors/runtime.ts
|
|
806
|
+
import v8 from "v8";
|
|
807
|
+
var lastLagCheck = process.hrtime.bigint();
|
|
808
|
+
var lagSamples = [];
|
|
809
|
+
function startLagSampling() {
|
|
810
|
+
const interval = setInterval(() => {
|
|
811
|
+
const now = process.hrtime.bigint();
|
|
812
|
+
const expectedMs = 100;
|
|
813
|
+
const actualMs = Number(now - lastLagCheck) / 1e6;
|
|
814
|
+
const lag = Math.max(0, actualMs - expectedMs);
|
|
815
|
+
lagSamples.push(lag);
|
|
816
|
+
if (lagSamples.length > 600) lagSamples.shift();
|
|
817
|
+
lastLagCheck = now;
|
|
818
|
+
}, 100);
|
|
819
|
+
interval.unref();
|
|
820
|
+
return interval;
|
|
821
|
+
}
|
|
822
|
+
function collectRuntimeMetrics() {
|
|
823
|
+
try {
|
|
824
|
+
const heapSpaces = v8.getHeapSpaceStatistics();
|
|
825
|
+
const sortedLag = [...lagSamples].sort((a, b) => a - b);
|
|
826
|
+
const avgLag = sortedLag.length > 0 ? sortedLag.reduce((a, b) => a + b, 0) / sortedLag.length : 0;
|
|
827
|
+
const p99Lag = sortedLag.length > 0 ? sortedLag[Math.ceil(sortedLag.length * 0.99) - 1] : 0;
|
|
828
|
+
lagSamples = [];
|
|
829
|
+
let activeHandles = 0;
|
|
830
|
+
let activeRequests = 0;
|
|
831
|
+
try {
|
|
832
|
+
activeHandles = process._getActiveHandles?.()?.length ?? 0;
|
|
833
|
+
activeRequests = process._getActiveRequests?.()?.length ?? 0;
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
let gcPausesMsTotal = 0;
|
|
837
|
+
let gcPausesCount = 0;
|
|
838
|
+
try {
|
|
839
|
+
const { performance: performance2 } = __require("perf_hooks");
|
|
840
|
+
const entries = performance2.getEntriesByType("gc");
|
|
841
|
+
for (const entry of entries) {
|
|
842
|
+
gcPausesMsTotal += entry.duration;
|
|
843
|
+
gcPausesCount++;
|
|
844
|
+
}
|
|
845
|
+
performance2.clearMarks();
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
const heapSpaceInfos = heapSpaces.map((space) => ({
|
|
849
|
+
name: space.space_name,
|
|
850
|
+
sizeMb: Math.round(space.space_size / 1048576 * 100) / 100,
|
|
851
|
+
usedMb: Math.round(space.space_used_size / 1048576 * 100) / 100,
|
|
852
|
+
availableMb: Math.round(space.space_available_size / 1048576 * 100) / 100
|
|
853
|
+
}));
|
|
854
|
+
return {
|
|
855
|
+
eventLoopLagMs: Math.round(avgLag * 100) / 100,
|
|
856
|
+
eventLoopLagP99Ms: Math.round(p99Lag * 100) / 100,
|
|
857
|
+
activeHandles,
|
|
858
|
+
activeRequests,
|
|
859
|
+
gcPausesMsTotal: Math.round(gcPausesMsTotal * 100) / 100,
|
|
860
|
+
gcPausesCount,
|
|
861
|
+
heapSpaces: heapSpaceInfos
|
|
862
|
+
};
|
|
863
|
+
} catch {
|
|
864
|
+
lagSamples = [];
|
|
865
|
+
return {
|
|
866
|
+
eventLoopLagMs: 0,
|
|
867
|
+
eventLoopLagP99Ms: 0,
|
|
868
|
+
activeHandles: 0,
|
|
869
|
+
activeRequests: 0,
|
|
870
|
+
gcPausesMsTotal: 0,
|
|
871
|
+
gcPausesCount: 0,
|
|
872
|
+
heapSpaces: []
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/collectors/env-security.ts
|
|
878
|
+
import { readFile as readFile2, access } from "fs/promises";
|
|
879
|
+
import { join as join2 } from "path";
|
|
880
|
+
import { execFile as execFile2 } from "child_process";
|
|
881
|
+
var SENSITIVE_ENV_PATTERNS = [
|
|
882
|
+
/^(DATABASE_URL|DB_PASSWORD|DB_HOST)/i,
|
|
883
|
+
/^(AWS_SECRET|AWS_ACCESS_KEY|AWS_SESSION_TOKEN)/i,
|
|
884
|
+
/^(STRIPE_SECRET|PAYPAL_SECRET)/i,
|
|
885
|
+
/^(PRIVATE_KEY|SECRET_KEY|ENCRYPTION_KEY|JWT_SECRET)/i,
|
|
886
|
+
/^(SMTP_PASSWORD|MAIL_PASSWORD|EMAIL_PASSWORD)/i,
|
|
887
|
+
/^(GITHUB_TOKEN|GITLAB_TOKEN|NPM_TOKEN)/i,
|
|
888
|
+
/^(REDIS_PASSWORD|REDIS_URL)/i,
|
|
889
|
+
/^(SENTRY_DSN)/i,
|
|
890
|
+
/^(TWILIO_AUTH_TOKEN|SENDGRID_API_KEY)/i
|
|
891
|
+
];
|
|
892
|
+
var EOL_NODE_MAJOR = [0, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 21];
|
|
893
|
+
var RECOMMENDED_HEADERS = [
|
|
894
|
+
{ header: "strict-transport-security", recommendation: "Add HSTS header: max-age=31536000; includeSubDomains" },
|
|
895
|
+
{ header: "x-content-type-options", recommendation: "Set to 'nosniff' to prevent MIME sniffing" },
|
|
896
|
+
{ header: "x-frame-options", recommendation: "Set to 'DENY' or 'SAMEORIGIN' to prevent clickjacking" },
|
|
897
|
+
{ header: "x-xss-protection", recommendation: "Set to '0' (let CSP handle it) or '1; mode=block'" },
|
|
898
|
+
{ header: "content-security-policy", recommendation: "Add CSP header to prevent XSS and data injection" },
|
|
899
|
+
{ header: "referrer-policy", recommendation: "Set to 'strict-origin-when-cross-origin'" },
|
|
900
|
+
{ header: "permissions-policy", recommendation: "Restrict browser features: camera, microphone, geolocation" },
|
|
901
|
+
{ header: "x-dns-prefetch-control", recommendation: "Set to 'off' for security-sensitive apps" },
|
|
902
|
+
{ header: "cross-origin-opener-policy", recommendation: "Set to 'same-origin' for cross-origin isolation" },
|
|
903
|
+
{ header: "cross-origin-resource-policy", recommendation: "Set to 'same-origin' or 'cross-origin' as needed" }
|
|
904
|
+
];
|
|
905
|
+
function getMemoryLimit() {
|
|
906
|
+
try {
|
|
907
|
+
for (const arg of process.execArgv) {
|
|
908
|
+
const match = arg.match(/--max-old-space-size=(\d+)/);
|
|
909
|
+
if (match) return parseInt(match[1]);
|
|
910
|
+
}
|
|
911
|
+
const v82 = __require("v8");
|
|
912
|
+
const heapStats = v82.getHeapStatistics();
|
|
913
|
+
return Math.round(heapStats.heap_size_limit / 1048576);
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function getDnsServers() {
|
|
919
|
+
try {
|
|
920
|
+
const dns = __require("dns");
|
|
921
|
+
return dns.getServers?.() || [];
|
|
922
|
+
} catch {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
var EnvSecurityCollector = class {
|
|
927
|
+
constructor() {
|
|
928
|
+
this.cachedReport = null;
|
|
929
|
+
this.lastCheck = 0;
|
|
930
|
+
}
|
|
931
|
+
/** Collect environment security report (cached for 5 minutes) */
|
|
932
|
+
async collect() {
|
|
933
|
+
if (this.cachedReport && Date.now() - this.lastCheck < 3e5) {
|
|
934
|
+
return this.cachedReport;
|
|
935
|
+
}
|
|
936
|
+
const nodeVersion = process.version;
|
|
937
|
+
const nodeMajor = parseInt(nodeVersion.replace("v", ""), 10);
|
|
938
|
+
const nodeVersionSecure = !EOL_NODE_MAJOR.includes(nodeMajor);
|
|
939
|
+
const envVarWarnings = this.checkEnvVars();
|
|
940
|
+
const debugModeEnabled = this.checkDebugMode();
|
|
941
|
+
const sourceMapExposed = await this.checkSourceMaps();
|
|
942
|
+
const frameworkInfo = await this.detectFramework();
|
|
943
|
+
const npmVersion = await this.getNpmVersion();
|
|
944
|
+
this.cachedReport = {
|
|
945
|
+
nodeVersion,
|
|
946
|
+
nodeVersionSecure,
|
|
947
|
+
npmVersion,
|
|
948
|
+
frameworkName: frameworkInfo.name,
|
|
949
|
+
frameworkVersion: frameworkInfo.version,
|
|
950
|
+
securityHeaders: [],
|
|
951
|
+
// Populated by response analysis
|
|
952
|
+
envVarWarnings,
|
|
953
|
+
debugModeEnabled,
|
|
954
|
+
sourceMapExposed,
|
|
955
|
+
// Runtime details
|
|
956
|
+
v8Version: process.versions.v8 || "",
|
|
957
|
+
uvVersion: process.versions.uv || "",
|
|
958
|
+
opensslVersion: process.versions.openssl || "",
|
|
959
|
+
zlibVersion: process.versions.zlib || "",
|
|
960
|
+
icuVersion: process.versions.icu || "",
|
|
961
|
+
// Process config
|
|
962
|
+
nodeEnv: process.env.NODE_ENV || "",
|
|
963
|
+
tz: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || "",
|
|
964
|
+
memoryLimitMb: getMemoryLimit(),
|
|
965
|
+
// Server info (detect from env/process)
|
|
966
|
+
port: parseInt(process.env.PORT || "0") || null,
|
|
967
|
+
host: process.env.HOST || process.env.HOSTNAME || "",
|
|
968
|
+
// DNS
|
|
969
|
+
dnsServers: getDnsServers()
|
|
970
|
+
};
|
|
971
|
+
this.lastCheck = Date.now();
|
|
972
|
+
return this.cachedReport;
|
|
973
|
+
}
|
|
974
|
+
/** Check response headers against security recommendations */
|
|
975
|
+
checkResponseHeaders(headers) {
|
|
976
|
+
const normalizedHeaders = {};
|
|
977
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
978
|
+
normalizedHeaders[key.toLowerCase()] = Array.isArray(val) ? val.join(", ") : val || "";
|
|
979
|
+
}
|
|
980
|
+
return RECOMMENDED_HEADERS.map(({ header, recommendation }) => ({
|
|
981
|
+
header,
|
|
982
|
+
present: header in normalizedHeaders,
|
|
983
|
+
value: normalizedHeaders[header],
|
|
984
|
+
recommendation: header in normalizedHeaders ? void 0 : recommendation
|
|
985
|
+
}));
|
|
986
|
+
}
|
|
987
|
+
checkEnvVars() {
|
|
988
|
+
const warnings = [];
|
|
989
|
+
const envKeys = Object.keys(process.env);
|
|
990
|
+
for (const key of envKeys) {
|
|
991
|
+
for (const pattern of SENSITIVE_ENV_PATTERNS) {
|
|
992
|
+
if (pattern.test(key)) {
|
|
993
|
+
warnings.push(`Sensitive env var '${key}' is set \u2014 ensure it's not exposed to client`);
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (process.getuid && process.getuid() === 0) {
|
|
999
|
+
warnings.push("Process is running as root \u2014 use a non-privileged user");
|
|
1000
|
+
}
|
|
1001
|
+
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
|
|
1002
|
+
warnings.push("NODE_ENV is not set to 'production' \u2014 ensure proper security config");
|
|
1003
|
+
}
|
|
1004
|
+
return warnings;
|
|
1005
|
+
}
|
|
1006
|
+
checkDebugMode() {
|
|
1007
|
+
return !!(process.env.DEBUG || process.env.NODE_DEBUG || process.execArgv.some((arg) => arg.includes("--inspect") || arg.includes("--debug")));
|
|
1008
|
+
}
|
|
1009
|
+
async checkSourceMaps() {
|
|
1010
|
+
const paths = [
|
|
1011
|
+
join2(process.cwd(), ".next", "static"),
|
|
1012
|
+
join2(process.cwd(), "dist"),
|
|
1013
|
+
join2(process.cwd(), "build")
|
|
1014
|
+
];
|
|
1015
|
+
for (const dir of paths) {
|
|
1016
|
+
try {
|
|
1017
|
+
await access(dir);
|
|
1018
|
+
return process.env.NODE_ENV === "production" && !!process.env.GENERATE_SOURCEMAP;
|
|
1019
|
+
} catch {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
async detectFramework() {
|
|
1026
|
+
try {
|
|
1027
|
+
const pkgPath = join2(process.cwd(), "package.json");
|
|
1028
|
+
const content = await readFile2(pkgPath, "utf-8");
|
|
1029
|
+
const pkg = JSON.parse(content);
|
|
1030
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1031
|
+
if (allDeps.next) return { name: "next", version: allDeps.next };
|
|
1032
|
+
if (allDeps.nuxt) return { name: "nuxt", version: allDeps.nuxt };
|
|
1033
|
+
if (allDeps.express) return { name: "express", version: allDeps.express };
|
|
1034
|
+
if (allDeps.fastify) return { name: "fastify", version: allDeps.fastify };
|
|
1035
|
+
if (allDeps.koa) return { name: "koa", version: allDeps.koa };
|
|
1036
|
+
if (allDeps.hono) return { name: "hono", version: allDeps.hono };
|
|
1037
|
+
if (allDeps["@nestjs/core"]) return { name: "nestjs", version: allDeps["@nestjs/core"] };
|
|
1038
|
+
if (allDeps.remix || allDeps["@remix-run/node"]) return { name: "remix", version: allDeps["@remix-run/node"] || allDeps.remix };
|
|
1039
|
+
if (allDeps.astro) return { name: "astro", version: allDeps.astro };
|
|
1040
|
+
if (allDeps["@sveltejs/kit"]) return { name: "sveltekit", version: allDeps["@sveltejs/kit"] };
|
|
1041
|
+
if (allDeps.svelte) return { name: "svelte", version: allDeps.svelte };
|
|
1042
|
+
if (allDeps["@adonisjs/core"]) return { name: "adonisjs", version: allDeps["@adonisjs/core"] };
|
|
1043
|
+
if (allDeps.meteor) return { name: "meteor", version: allDeps.meteor };
|
|
1044
|
+
if (allDeps.strapi) return { name: "strapi", version: allDeps.strapi };
|
|
1045
|
+
if (allDeps.keystone) return { name: "keystonejs", version: allDeps.keystone };
|
|
1046
|
+
if (allDeps.payload) return { name: "payload", version: allDeps.payload };
|
|
1047
|
+
return {};
|
|
1048
|
+
} catch {
|
|
1049
|
+
return {};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
getNpmVersion() {
|
|
1053
|
+
return new Promise((resolve) => {
|
|
1054
|
+
execFile2("npm", ["--version"], { timeout: 5e3 }, (err, stdout) => {
|
|
1055
|
+
resolve(err ? void 0 : stdout.trim());
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/reporter.ts
|
|
1062
|
+
import http from "http";
|
|
1063
|
+
import https from "https";
|
|
1064
|
+
import { URL } from "url";
|
|
1065
|
+
var MAX_PAYLOAD_BYTES = 512 * 1024;
|
|
1066
|
+
var MAX_STRING_LENGTH = 4096;
|
|
1067
|
+
var MAX_ARRAY_LENGTH = 200;
|
|
1068
|
+
function maskApiKey(key) {
|
|
1069
|
+
if (key.length <= 6) return "***";
|
|
1070
|
+
return key.slice(0, 6) + "***";
|
|
1071
|
+
}
|
|
1072
|
+
function truncStr(s, maxLen) {
|
|
1073
|
+
if (s == null) return s;
|
|
1074
|
+
return s.length > maxLen ? s.slice(0, maxLen) + "...[truncated]" : s;
|
|
1075
|
+
}
|
|
1076
|
+
function boundArray(arr, maxLen) {
|
|
1077
|
+
if (!arr) return arr;
|
|
1078
|
+
return arr.length > maxLen ? arr.slice(0, maxLen) : arr;
|
|
1079
|
+
}
|
|
1080
|
+
function sanitizePayload(payload) {
|
|
1081
|
+
const p = { ...payload };
|
|
1082
|
+
if (p.errors) {
|
|
1083
|
+
p.errors = boundArray(p.errors, MAX_ARRAY_LENGTH);
|
|
1084
|
+
for (let i = 0; i < p.errors.length; i++) {
|
|
1085
|
+
const e = { ...p.errors[i] };
|
|
1086
|
+
e.message = truncStr(e.message, MAX_STRING_LENGTH) || "";
|
|
1087
|
+
e.stack = truncStr(e.stack, MAX_STRING_LENGTH);
|
|
1088
|
+
p.errors[i] = e;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (p.security) {
|
|
1092
|
+
p.security = { ...p.security };
|
|
1093
|
+
p.security.events = boundArray(p.security.events, 50) || [];
|
|
1094
|
+
for (let i = 0; i < p.security.events.length; i++) {
|
|
1095
|
+
const ev = { ...p.security.events[i] };
|
|
1096
|
+
ev.message = truncStr(ev.message, MAX_STRING_LENGTH) || "";
|
|
1097
|
+
ev.payload = truncStr(ev.payload, 500);
|
|
1098
|
+
p.security.events[i] = ev;
|
|
1099
|
+
}
|
|
1100
|
+
p.security.topOffendingIps = boundArray(p.security.topOffendingIps, 10) || [];
|
|
1101
|
+
}
|
|
1102
|
+
if (p.depAudit) {
|
|
1103
|
+
p.depAudit = { ...p.depAudit };
|
|
1104
|
+
p.depAudit.vulnerabilities = boundArray(p.depAudit.vulnerabilities, 50) || [];
|
|
1105
|
+
}
|
|
1106
|
+
if (p.http) {
|
|
1107
|
+
p.http = { ...p.http };
|
|
1108
|
+
p.http.topPaths = boundArray(p.http.topPaths, 20) || [];
|
|
1109
|
+
}
|
|
1110
|
+
if (p.runtime) {
|
|
1111
|
+
p.runtime = { ...p.runtime };
|
|
1112
|
+
p.runtime.heapSpaces = boundArray(p.runtime.heapSpaces, 20);
|
|
1113
|
+
}
|
|
1114
|
+
if (p.envSecurity) {
|
|
1115
|
+
p.envSecurity = { ...p.envSecurity };
|
|
1116
|
+
p.envSecurity.envVarWarnings = boundArray(p.envSecurity.envVarWarnings, 50) || [];
|
|
1117
|
+
p.envSecurity.securityHeaders = boundArray(p.envSecurity.securityHeaders, 20) || [];
|
|
1118
|
+
}
|
|
1119
|
+
return p;
|
|
1120
|
+
}
|
|
1121
|
+
var Reporter = class {
|
|
1122
|
+
constructor(apiUrl, apiKey, logger) {
|
|
1123
|
+
this.queue = [];
|
|
1124
|
+
this.sending = false;
|
|
1125
|
+
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
1126
|
+
this.apiKey = apiKey;
|
|
1127
|
+
this.logger = logger;
|
|
1128
|
+
}
|
|
1129
|
+
async send(payload) {
|
|
1130
|
+
try {
|
|
1131
|
+
this.queue.push(payload);
|
|
1132
|
+
if (this.sending) return true;
|
|
1133
|
+
this.sending = true;
|
|
1134
|
+
try {
|
|
1135
|
+
while (this.queue.length > 0) {
|
|
1136
|
+
const batch = this.queue.splice(0, 10);
|
|
1137
|
+
await this.transmit(batch);
|
|
1138
|
+
}
|
|
1139
|
+
return true;
|
|
1140
|
+
} finally {
|
|
1141
|
+
this.sending = false;
|
|
1142
|
+
}
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
this.logger.error(
|
|
1145
|
+
`Failed to send report (key=${maskApiKey(this.apiKey)}):`,
|
|
1146
|
+
err instanceof Error ? err.message : err
|
|
1147
|
+
);
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async transmit(payloads) {
|
|
1152
|
+
const url = `${this.apiUrl}/api/tracker/report`;
|
|
1153
|
+
const sanitized = payloads.map(sanitizePayload);
|
|
1154
|
+
let body = JSON.stringify(sanitized.length === 1 ? sanitized[0] : { batch: sanitized });
|
|
1155
|
+
if (body.length > MAX_PAYLOAD_BYTES) {
|
|
1156
|
+
this.logger.warn(
|
|
1157
|
+
`Payload too large (${body.length} bytes), truncating arrays (key=${maskApiKey(this.apiKey)})`
|
|
1158
|
+
);
|
|
1159
|
+
for (const p of sanitized) {
|
|
1160
|
+
if (p.security) {
|
|
1161
|
+
p.security.events = p.security.events.slice(0, 10);
|
|
1162
|
+
}
|
|
1163
|
+
if (p.errors) {
|
|
1164
|
+
p.errors = p.errors.slice(0, 20);
|
|
1165
|
+
}
|
|
1166
|
+
if (p.depAudit) {
|
|
1167
|
+
p.depAudit.vulnerabilities = p.depAudit.vulnerabilities.slice(0, 10);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
body = JSON.stringify(sanitized.length === 1 ? sanitized[0] : { batch: sanitized });
|
|
1171
|
+
}
|
|
1172
|
+
this.logger.info(
|
|
1173
|
+
`Sending report to ${url} (${payloads.length} payload(s), ${body.length} bytes, key=${maskApiKey(this.apiKey)})`
|
|
1174
|
+
);
|
|
1175
|
+
return new Promise((resolve, reject) => {
|
|
1176
|
+
try {
|
|
1177
|
+
const parsed = new URL(url);
|
|
1178
|
+
const isHttps = parsed.protocol === "https:";
|
|
1179
|
+
const mod = isHttps ? https : http;
|
|
1180
|
+
const options = {
|
|
1181
|
+
hostname: parsed.hostname,
|
|
1182
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
1183
|
+
path: parsed.pathname + parsed.search,
|
|
1184
|
+
method: "POST",
|
|
1185
|
+
headers: {
|
|
1186
|
+
"Content-Type": "application/json",
|
|
1187
|
+
"Content-Length": Buffer.byteLength(body, "utf-8"),
|
|
1188
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1189
|
+
"User-Agent": "shieldpress-tracker/1.0.0"
|
|
1190
|
+
},
|
|
1191
|
+
timeout: 1e4
|
|
1192
|
+
};
|
|
1193
|
+
const req = mod.request(options, (res) => {
|
|
1194
|
+
let data = "";
|
|
1195
|
+
res.on("data", (chunk) => {
|
|
1196
|
+
if (data.length < 1024) {
|
|
1197
|
+
data += chunk;
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
res.on("end", () => {
|
|
1201
|
+
const statusCode = res.statusCode || 0;
|
|
1202
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
1203
|
+
this.logger.info("Report sent successfully");
|
|
1204
|
+
resolve();
|
|
1205
|
+
} else {
|
|
1206
|
+
reject(new Error(`HTTP ${statusCode}: ${data.slice(0, 200)}`));
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
res.on("error", (err) => {
|
|
1210
|
+
reject(new Error(`Response error: ${err.message}`));
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
req.setTimeout(1e4, () => {
|
|
1214
|
+
req.destroy(new Error("Request timed out after 10000ms"));
|
|
1215
|
+
});
|
|
1216
|
+
req.on("error", (err) => {
|
|
1217
|
+
reject(new Error(`Request error: ${err.message}`));
|
|
1218
|
+
});
|
|
1219
|
+
req.write(body);
|
|
1220
|
+
req.end();
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// src/tracker.ts
|
|
1229
|
+
var ShieldPressTracker = class {
|
|
1230
|
+
constructor(userConfig) {
|
|
1231
|
+
this.timer = null;
|
|
1232
|
+
this.lagTimer = null;
|
|
1233
|
+
this.started = false;
|
|
1234
|
+
this.headersChecked = false;
|
|
1235
|
+
this.config = resolveConfig(userConfig);
|
|
1236
|
+
this.logger = createLogger(this.config.debug);
|
|
1237
|
+
this.reporter = new Reporter(this.config.apiUrl, this.config.apiKey, this.logger);
|
|
1238
|
+
this.httpCollector = new HttpCollector();
|
|
1239
|
+
this.errorCollector = new ErrorCollector(this.config.maxErrorBuffer);
|
|
1240
|
+
this.securityCollector = new SecurityCollector(this.config.maxSecurityBuffer);
|
|
1241
|
+
this.depAuditCollector = new DepAuditCollector(this.config.depAuditInterval * 1e3, this.logger);
|
|
1242
|
+
this.envSecurityCollector = new EnvSecurityCollector();
|
|
1243
|
+
}
|
|
1244
|
+
/** Start the tracker — begins periodic reporting */
|
|
1245
|
+
start() {
|
|
1246
|
+
if (this.started) return this;
|
|
1247
|
+
this.started = true;
|
|
1248
|
+
this.logger.info(
|
|
1249
|
+
`Starting tracker for site=${this.config.siteId} env=${this.config.environment} interval=${this.config.reportInterval}s`
|
|
1250
|
+
);
|
|
1251
|
+
if (this.config.errorTracking) {
|
|
1252
|
+
process.on("uncaughtException", (err) => {
|
|
1253
|
+
this.errorCollector.capture(err, { type: "uncaughtException" });
|
|
1254
|
+
this.flush().catch(() => {
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
process.on("unhandledRejection", (reason) => {
|
|
1258
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
1259
|
+
this.errorCollector.capture(err, { type: "unhandledRejection" });
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
if (this.config.systemMetrics) {
|
|
1263
|
+
collectSystemMetrics();
|
|
1264
|
+
}
|
|
1265
|
+
if (this.config.runtimeMetrics) {
|
|
1266
|
+
this.lagTimer = startLagSampling();
|
|
1267
|
+
}
|
|
1268
|
+
this.timer = setInterval(() => {
|
|
1269
|
+
this.flush().catch((err) => {
|
|
1270
|
+
this.logger.error("Flush failed:", err);
|
|
1271
|
+
});
|
|
1272
|
+
}, this.config.reportInterval * 1e3);
|
|
1273
|
+
this.timer.unref();
|
|
1274
|
+
const shutdown = () => {
|
|
1275
|
+
this.flush().catch(() => {
|
|
1276
|
+
}).finally(() => process.exit(0));
|
|
1277
|
+
};
|
|
1278
|
+
process.on("SIGTERM", shutdown);
|
|
1279
|
+
process.on("SIGINT", shutdown);
|
|
1280
|
+
return this;
|
|
1281
|
+
}
|
|
1282
|
+
/** Stop the tracker */
|
|
1283
|
+
stop() {
|
|
1284
|
+
if (this.timer) {
|
|
1285
|
+
clearInterval(this.timer);
|
|
1286
|
+
this.timer = null;
|
|
1287
|
+
}
|
|
1288
|
+
if (this.lagTimer) {
|
|
1289
|
+
clearInterval(this.lagTimer);
|
|
1290
|
+
this.lagTimer = null;
|
|
1291
|
+
}
|
|
1292
|
+
this.started = false;
|
|
1293
|
+
this.logger.info("Tracker stopped");
|
|
1294
|
+
}
|
|
1295
|
+
/** Manually record an HTTP request */
|
|
1296
|
+
recordRequest(data) {
|
|
1297
|
+
if (!this.config.httpTracking) return;
|
|
1298
|
+
if (this.shouldIgnorePath(data.path)) return;
|
|
1299
|
+
this.httpCollector.record(data);
|
|
1300
|
+
}
|
|
1301
|
+
/** Analyze an incoming request for security threats */
|
|
1302
|
+
analyzeRequest(req) {
|
|
1303
|
+
if (!this.config.securityTracking) return;
|
|
1304
|
+
if (this.shouldIgnorePath(req.url)) return;
|
|
1305
|
+
this.securityCollector.analyzeRequest(req);
|
|
1306
|
+
}
|
|
1307
|
+
/** Analyze a response for security issues (headers, data leaks) */
|
|
1308
|
+
analyzeResponse(data) {
|
|
1309
|
+
if (!this.config.securityTracking) return;
|
|
1310
|
+
if (!this.headersChecked && data.headers && this.config.envSecurity) {
|
|
1311
|
+
const headerChecks = this.envSecurityCollector.checkResponseHeaders(data.headers);
|
|
1312
|
+
this.headersChecked = true;
|
|
1313
|
+
this.envSecurityCollector.collect().then((report) => {
|
|
1314
|
+
report.securityHeaders = headerChecks;
|
|
1315
|
+
}).catch(() => {
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
this.securityCollector.analyzeResponse(data);
|
|
1319
|
+
}
|
|
1320
|
+
/** Record an authentication failure */
|
|
1321
|
+
recordAuthFailure(data) {
|
|
1322
|
+
if (!this.config.securityTracking) return;
|
|
1323
|
+
this.securityCollector.recordAuthFailure(data);
|
|
1324
|
+
}
|
|
1325
|
+
/** Record a rate limit event */
|
|
1326
|
+
recordRateLimit(data) {
|
|
1327
|
+
if (!this.config.securityTracking) return;
|
|
1328
|
+
this.securityCollector.recordRateLimit(data);
|
|
1329
|
+
}
|
|
1330
|
+
/** Record a CORS violation */
|
|
1331
|
+
recordCorsViolation(data) {
|
|
1332
|
+
if (!this.config.securityTracking) return;
|
|
1333
|
+
this.securityCollector.recordCorsViolation(data);
|
|
1334
|
+
}
|
|
1335
|
+
/** Manually capture an error */
|
|
1336
|
+
captureError(error, meta) {
|
|
1337
|
+
if (!this.config.errorTracking) return;
|
|
1338
|
+
this.errorCollector.capture(error, meta);
|
|
1339
|
+
}
|
|
1340
|
+
/** Flush all collected data and send report */
|
|
1341
|
+
async flush() {
|
|
1342
|
+
const payload = {
|
|
1343
|
+
siteId: this.config.siteId,
|
|
1344
|
+
appName: this.config.appName,
|
|
1345
|
+
appVersion: this.config.appVersion,
|
|
1346
|
+
environment: this.config.environment,
|
|
1347
|
+
tags: this.config.tags,
|
|
1348
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1349
|
+
};
|
|
1350
|
+
if (this.config.systemMetrics) {
|
|
1351
|
+
payload.system = collectSystemMetrics();
|
|
1352
|
+
}
|
|
1353
|
+
if (this.config.httpTracking) {
|
|
1354
|
+
const httpMetrics = this.httpCollector.flush();
|
|
1355
|
+
if (httpMetrics) payload.http = httpMetrics;
|
|
1356
|
+
}
|
|
1357
|
+
if (this.config.errorTracking) {
|
|
1358
|
+
const errors = this.errorCollector.flush();
|
|
1359
|
+
if (errors.length > 0) payload.errors = errors;
|
|
1360
|
+
}
|
|
1361
|
+
if (this.config.securityTracking) {
|
|
1362
|
+
const security = this.securityCollector.flush();
|
|
1363
|
+
if (security) payload.security = security;
|
|
1364
|
+
}
|
|
1365
|
+
if (this.config.depAudit) {
|
|
1366
|
+
const depAudit = await this.depAuditCollector.collect().catch(() => null);
|
|
1367
|
+
if (depAudit) payload.depAudit = depAudit;
|
|
1368
|
+
}
|
|
1369
|
+
if (this.config.runtimeMetrics) {
|
|
1370
|
+
payload.runtime = collectRuntimeMetrics();
|
|
1371
|
+
}
|
|
1372
|
+
if (this.config.envSecurity) {
|
|
1373
|
+
const envReport = await this.envSecurityCollector.collect().catch(() => null);
|
|
1374
|
+
if (envReport) payload.envSecurity = envReport;
|
|
1375
|
+
}
|
|
1376
|
+
if (this.config.heartbeat) {
|
|
1377
|
+
payload.heartbeat = true;
|
|
1378
|
+
}
|
|
1379
|
+
return this.reporter.send(payload);
|
|
1380
|
+
}
|
|
1381
|
+
shouldIgnorePath(path) {
|
|
1382
|
+
return this.config.ignorePaths.some((pattern) => {
|
|
1383
|
+
if (pattern.endsWith("/*")) {
|
|
1384
|
+
return path.startsWith(pattern.slice(0, -1));
|
|
1385
|
+
}
|
|
1386
|
+
return path === pattern;
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// src/nextjs.ts
|
|
1392
|
+
var globalTracker = null;
|
|
1393
|
+
function initTracker(config) {
|
|
1394
|
+
if (globalTracker) return globalTracker;
|
|
1395
|
+
globalTracker = new ShieldPressTracker(config).start();
|
|
1396
|
+
return globalTracker;
|
|
1397
|
+
}
|
|
1398
|
+
function getTracker() {
|
|
1399
|
+
return globalTracker;
|
|
1400
|
+
}
|
|
1401
|
+
function withShieldPress(handler) {
|
|
1402
|
+
return async function shieldPressMiddleware(request) {
|
|
1403
|
+
const start = performance.now();
|
|
1404
|
+
const headers = {};
|
|
1405
|
+
request.headers.forEach((value, key) => {
|
|
1406
|
+
headers[key] = value;
|
|
1407
|
+
});
|
|
1408
|
+
globalTracker?.analyzeRequest({
|
|
1409
|
+
url: request.nextUrl.pathname + request.nextUrl.search,
|
|
1410
|
+
method: request.method,
|
|
1411
|
+
headers,
|
|
1412
|
+
ip: request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || void 0,
|
|
1413
|
+
query: Object.fromEntries(request.nextUrl.searchParams)
|
|
1414
|
+
});
|
|
1415
|
+
let response;
|
|
1416
|
+
try {
|
|
1417
|
+
if (handler) {
|
|
1418
|
+
response = await handler(request);
|
|
1419
|
+
} else {
|
|
1420
|
+
const { NextResponse: NR } = await import("next/server");
|
|
1421
|
+
response = NR.next();
|
|
1422
|
+
}
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
globalTracker?.captureError(err instanceof Error ? err : new Error(String(err)), {
|
|
1425
|
+
url: request.nextUrl.pathname,
|
|
1426
|
+
method: request.method
|
|
1427
|
+
});
|
|
1428
|
+
throw err;
|
|
1429
|
+
}
|
|
1430
|
+
const durationMs = performance.now() - start;
|
|
1431
|
+
globalTracker?.recordRequest({
|
|
1432
|
+
path: request.nextUrl.pathname,
|
|
1433
|
+
method: request.method,
|
|
1434
|
+
statusCode: response.status,
|
|
1435
|
+
durationMs: Math.round(durationMs * 100) / 100
|
|
1436
|
+
});
|
|
1437
|
+
if (response.status === 401) {
|
|
1438
|
+
globalTracker?.recordAuthFailure({
|
|
1439
|
+
ip: request.headers.get("x-forwarded-for") || void 0,
|
|
1440
|
+
userAgent: request.headers.get("user-agent") || void 0,
|
|
1441
|
+
url: request.nextUrl.pathname,
|
|
1442
|
+
method: request.method,
|
|
1443
|
+
reason: "HTTP 401 Unauthorized"
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
const responseHeaders = {};
|
|
1447
|
+
response.headers.forEach((value, key) => {
|
|
1448
|
+
responseHeaders[key] = value;
|
|
1449
|
+
});
|
|
1450
|
+
globalTracker?.analyzeResponse({
|
|
1451
|
+
url: request.nextUrl.pathname,
|
|
1452
|
+
statusCode: response.status,
|
|
1453
|
+
headers: responseHeaders
|
|
1454
|
+
});
|
|
1455
|
+
return response;
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function withTracking(handler) {
|
|
1459
|
+
return async function trackedHandler(request, context) {
|
|
1460
|
+
const start = performance.now();
|
|
1461
|
+
const headers = {};
|
|
1462
|
+
request.headers.forEach((value, key) => {
|
|
1463
|
+
headers[key] = value;
|
|
1464
|
+
});
|
|
1465
|
+
globalTracker?.analyzeRequest({
|
|
1466
|
+
url: request.nextUrl.pathname + request.nextUrl.search,
|
|
1467
|
+
method: request.method,
|
|
1468
|
+
headers,
|
|
1469
|
+
ip: request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || void 0,
|
|
1470
|
+
query: Object.fromEntries(request.nextUrl.searchParams)
|
|
1471
|
+
});
|
|
1472
|
+
let statusCode = 200;
|
|
1473
|
+
try {
|
|
1474
|
+
const response = await handler(request, context);
|
|
1475
|
+
statusCode = response.status;
|
|
1476
|
+
const durationMs = performance.now() - start;
|
|
1477
|
+
globalTracker?.recordRequest({
|
|
1478
|
+
path: request.nextUrl.pathname,
|
|
1479
|
+
method: request.method,
|
|
1480
|
+
statusCode,
|
|
1481
|
+
durationMs: Math.round(durationMs * 100) / 100
|
|
1482
|
+
});
|
|
1483
|
+
if (statusCode === 401) {
|
|
1484
|
+
globalTracker?.recordAuthFailure({
|
|
1485
|
+
ip: request.headers.get("x-forwarded-for") || void 0,
|
|
1486
|
+
url: request.nextUrl.pathname,
|
|
1487
|
+
method: request.method,
|
|
1488
|
+
reason: "HTTP 401 Unauthorized"
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
return response;
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
statusCode = 500;
|
|
1494
|
+
const durationMs = performance.now() - start;
|
|
1495
|
+
globalTracker?.recordRequest({
|
|
1496
|
+
path: request.nextUrl.pathname,
|
|
1497
|
+
method: request.method,
|
|
1498
|
+
statusCode,
|
|
1499
|
+
durationMs: Math.round(durationMs * 100) / 100
|
|
1500
|
+
});
|
|
1501
|
+
globalTracker?.captureError(err instanceof Error ? err : new Error(String(err)), {
|
|
1502
|
+
url: request.nextUrl.pathname,
|
|
1503
|
+
method: request.method,
|
|
1504
|
+
statusCode
|
|
1505
|
+
});
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
export {
|
|
1511
|
+
ShieldPressTracker,
|
|
1512
|
+
getTracker,
|
|
1513
|
+
initTracker,
|
|
1514
|
+
withShieldPress,
|
|
1515
|
+
withTracking
|
|
1516
|
+
};
|
|
1517
|
+
//# sourceMappingURL=nextjs.mjs.map
|