@ceon-oy/monitor-sdk 1.0.1
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 +261 -0
- package/dist/index.d.mts +241 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.js +605 -0
- package/dist/index.mjs +575 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
MonitorClient: () => MonitorClient
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/MonitorClient.ts
|
|
38
|
+
var MonitorClient = class {
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.queue = [];
|
|
41
|
+
this.flushTimer = null;
|
|
42
|
+
this.isClosed = false;
|
|
43
|
+
this.retryCount = /* @__PURE__ */ new Map();
|
|
44
|
+
this.isFlushInProgress = false;
|
|
45
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
46
|
+
throw new Error("[MonitorClient] API key is required");
|
|
47
|
+
}
|
|
48
|
+
if (!/^cm_[a-zA-Z0-9_-]+$/.test(config.apiKey)) {
|
|
49
|
+
throw new Error("[MonitorClient] Invalid API key format. Expected format: cm_xxx");
|
|
50
|
+
}
|
|
51
|
+
if (!config.endpoint || config.endpoint.trim().length === 0) {
|
|
52
|
+
throw new Error("[MonitorClient] Endpoint URL is required");
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(config.endpoint);
|
|
56
|
+
if (!["https:", "http:"].includes(url.protocol)) {
|
|
57
|
+
throw new Error("[MonitorClient] Endpoint must use HTTP or HTTPS protocol");
|
|
58
|
+
}
|
|
59
|
+
if (url.protocol === "http:" && config.environment !== "development" && config.environment !== "test") {
|
|
60
|
+
console.warn("[MonitorClient] Warning: Using HTTP in non-development environment is not recommended");
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof Error && err.message.includes("[MonitorClient]")) {
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
throw new Error("[MonitorClient] Invalid endpoint URL");
|
|
67
|
+
}
|
|
68
|
+
this.apiKey = config.apiKey;
|
|
69
|
+
this.endpoint = config.endpoint.replace(/\/$/, "");
|
|
70
|
+
this.environment = config.environment || "production";
|
|
71
|
+
this.batchSize = Math.max(1, config.batchSize || 10);
|
|
72
|
+
this.flushIntervalMs = Math.max(1e3, config.flushIntervalMs || 5e3);
|
|
73
|
+
this.trackDependencies = config.trackDependencies || false;
|
|
74
|
+
this.packageJsonPath = config.packageJsonPath;
|
|
75
|
+
this.dependencySources = config.dependencySources;
|
|
76
|
+
this.maxQueueSize = config.maxQueueSize || 1e3;
|
|
77
|
+
this.maxRetries = config.maxRetries || 3;
|
|
78
|
+
this.requestTimeoutMs = config.requestTimeoutMs || 1e4;
|
|
79
|
+
const defaultExcludePatterns = [
|
|
80
|
+
"@types/*",
|
|
81
|
+
"eslint*",
|
|
82
|
+
"prettier*",
|
|
83
|
+
"*-loader",
|
|
84
|
+
"*-plugin",
|
|
85
|
+
"@eslint/*",
|
|
86
|
+
"@typescript-eslint/*"
|
|
87
|
+
];
|
|
88
|
+
this.excludePatterns = config.excludePatterns || defaultExcludePatterns;
|
|
89
|
+
this.startFlushTimer();
|
|
90
|
+
if (this.trackDependencies) {
|
|
91
|
+
this.syncDependencies().catch((err) => {
|
|
92
|
+
console.error("[MonitorClient] Failed to sync dependencies:", err instanceof Error ? err.message : String(err));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async captureError(error, context) {
|
|
97
|
+
if (this.isClosed) return;
|
|
98
|
+
const payload = {
|
|
99
|
+
severity: context?.severity || "ERROR",
|
|
100
|
+
message: error.message,
|
|
101
|
+
stack: error.stack,
|
|
102
|
+
environment: this.environment,
|
|
103
|
+
route: context?.route,
|
|
104
|
+
method: context?.method,
|
|
105
|
+
statusCode: context?.statusCode,
|
|
106
|
+
userAgent: context?.userAgent,
|
|
107
|
+
ip: context?.ip,
|
|
108
|
+
requestId: context?.requestId,
|
|
109
|
+
metadata: context?.metadata
|
|
110
|
+
};
|
|
111
|
+
this.enqueue(payload);
|
|
112
|
+
}
|
|
113
|
+
async captureMessage(message, severity = "INFO", context) {
|
|
114
|
+
if (this.isClosed) return;
|
|
115
|
+
const payload = {
|
|
116
|
+
severity,
|
|
117
|
+
message,
|
|
118
|
+
environment: this.environment,
|
|
119
|
+
route: context?.route,
|
|
120
|
+
method: context?.method,
|
|
121
|
+
statusCode: context?.statusCode,
|
|
122
|
+
userAgent: context?.userAgent,
|
|
123
|
+
ip: context?.ip,
|
|
124
|
+
requestId: context?.requestId,
|
|
125
|
+
metadata: context?.metadata
|
|
126
|
+
};
|
|
127
|
+
this.enqueue(payload);
|
|
128
|
+
}
|
|
129
|
+
async flush() {
|
|
130
|
+
if (this.isFlushInProgress || this.queue.length === 0) return;
|
|
131
|
+
this.isFlushInProgress = true;
|
|
132
|
+
const errors = [...this.queue];
|
|
133
|
+
this.queue = [];
|
|
134
|
+
try {
|
|
135
|
+
if (errors.length === 1) {
|
|
136
|
+
await this.sendSingle(errors[0]);
|
|
137
|
+
} else {
|
|
138
|
+
await this.sendBatch(errors);
|
|
139
|
+
}
|
|
140
|
+
for (const error of errors) {
|
|
141
|
+
this.retryCount.delete(this.getErrorKey(error));
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error("[MonitorClient] Failed to send errors:", err instanceof Error ? err.message : String(err));
|
|
145
|
+
for (const error of errors) {
|
|
146
|
+
const key = this.getErrorKey(error);
|
|
147
|
+
const retries = this.retryCount.get(key) || 0;
|
|
148
|
+
if (retries < this.maxRetries) {
|
|
149
|
+
this.retryCount.set(key, retries + 1);
|
|
150
|
+
if (this.queue.length < this.maxQueueSize) {
|
|
151
|
+
this.queue.push(error);
|
|
152
|
+
} else {
|
|
153
|
+
console.warn("[MonitorClient] Queue full, dropping error");
|
|
154
|
+
this.retryCount.delete(key);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
console.warn("[MonitorClient] Max retries exceeded, dropping error");
|
|
158
|
+
this.retryCount.delete(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
this.isFlushInProgress = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
getErrorKey(error) {
|
|
166
|
+
return `${error.message}-${error.stack?.substring(0, 100) || ""}-${error.route || ""}`;
|
|
167
|
+
}
|
|
168
|
+
async close() {
|
|
169
|
+
this.isClosed = true;
|
|
170
|
+
this.stopFlushTimer();
|
|
171
|
+
await this.flush();
|
|
172
|
+
}
|
|
173
|
+
enqueue(payload) {
|
|
174
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
175
|
+
console.warn("[MonitorClient] Queue full, dropping oldest error");
|
|
176
|
+
this.queue.shift();
|
|
177
|
+
}
|
|
178
|
+
this.queue.push(payload);
|
|
179
|
+
if (this.queue.length >= this.batchSize) {
|
|
180
|
+
this.flush();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Fetch with timeout to prevent hanging requests
|
|
185
|
+
*/
|
|
186
|
+
async fetchWithTimeout(url, options, timeoutMs = this.requestTimeoutMs) {
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
189
|
+
try {
|
|
190
|
+
const response = await fetch(url, {
|
|
191
|
+
...options,
|
|
192
|
+
signal: controller.signal
|
|
193
|
+
});
|
|
194
|
+
return response;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
197
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timeoutId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async sendSingle(error) {
|
|
205
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/errors`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: {
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify(error)
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
const errorText = await response.text().catch(() => "");
|
|
215
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async sendBatch(errors) {
|
|
219
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/errors/batch`, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
224
|
+
},
|
|
225
|
+
body: JSON.stringify({ errors })
|
|
226
|
+
});
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const errorText = await response.text().catch(() => "");
|
|
229
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
startFlushTimer() {
|
|
233
|
+
this.flushTimer = setInterval(() => {
|
|
234
|
+
this.flush();
|
|
235
|
+
}, this.flushIntervalMs);
|
|
236
|
+
}
|
|
237
|
+
stopFlushTimer() {
|
|
238
|
+
if (this.flushTimer) {
|
|
239
|
+
clearInterval(this.flushTimer);
|
|
240
|
+
this.flushTimer = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async syncDependencies() {
|
|
244
|
+
try {
|
|
245
|
+
if (this.dependencySources && this.dependencySources.length > 0) {
|
|
246
|
+
for (const source of this.dependencySources) {
|
|
247
|
+
const technologies = await this.readPackageJsonFromPath(source.path);
|
|
248
|
+
if (technologies.length === 0) continue;
|
|
249
|
+
await this.sendTechnologiesWithEnvironment(technologies, source.environment);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
const technologies = await this.readPackageJson();
|
|
253
|
+
if (technologies.length === 0) return;
|
|
254
|
+
await this.sendTechnologies(technologies);
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error("[MonitorClient] Failed to sync dependencies:", err);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async syncTechnologies(technologies) {
|
|
261
|
+
await this.sendTechnologies(technologies);
|
|
262
|
+
}
|
|
263
|
+
async readPackageJson() {
|
|
264
|
+
const packagePath = this.packageJsonPath || "package.json";
|
|
265
|
+
return this.readPackageJsonFromPath(packagePath);
|
|
266
|
+
}
|
|
267
|
+
async readPackageJsonFromPath(packagePath) {
|
|
268
|
+
if (typeof process === "undefined" || typeof process.cwd !== "function") {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const fsModule = await import("fs");
|
|
273
|
+
const pathModule = await import("path");
|
|
274
|
+
const fs = fsModule.default || fsModule;
|
|
275
|
+
const path = pathModule.default || pathModule;
|
|
276
|
+
const baseDir = process.cwd();
|
|
277
|
+
const resolvedPath = path.isAbsolute(packagePath) ? packagePath : path.join(baseDir, packagePath);
|
|
278
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
279
|
+
const normalizedBase = path.normalize(baseDir);
|
|
280
|
+
if (!path.isAbsolute(packagePath)) {
|
|
281
|
+
if (!normalizedPath.startsWith(normalizedBase)) {
|
|
282
|
+
console.warn("[MonitorClient] Path traversal attempt blocked:", packagePath);
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (packagePath.includes("\0") || /\.\.[\\/]/.test(packagePath)) {
|
|
287
|
+
console.warn("[MonitorClient] Suspicious path blocked:", packagePath);
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
if (!normalizedPath.endsWith("package.json")) {
|
|
291
|
+
console.warn("[MonitorClient] Path must point to package.json");
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
const packageJson = JSON.parse(fs.readFileSync(normalizedPath, "utf-8"));
|
|
298
|
+
const technologies = [];
|
|
299
|
+
const deps = {
|
|
300
|
+
...packageJson.dependencies,
|
|
301
|
+
...packageJson.devDependencies
|
|
302
|
+
};
|
|
303
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
304
|
+
if (typeof version === "string") {
|
|
305
|
+
if (this.shouldExclude(name)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
technologies.push({
|
|
309
|
+
name,
|
|
310
|
+
version: version.replace(/^[\^~]/, "")
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return technologies;
|
|
315
|
+
} catch {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
shouldExclude(packageName) {
|
|
320
|
+
for (const pattern of this.excludePatterns) {
|
|
321
|
+
const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
322
|
+
if (new RegExp(`^${regexPattern}$`).test(packageName)) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
async sendTechnologies(technologies) {
|
|
329
|
+
await this.sendTechnologiesWithEnvironment(technologies, this.environment);
|
|
330
|
+
}
|
|
331
|
+
async sendTechnologiesWithEnvironment(technologies, environment) {
|
|
332
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/technologies/sync`, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: {
|
|
335
|
+
"Content-Type": "application/json",
|
|
336
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
337
|
+
},
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
environment,
|
|
340
|
+
technologies
|
|
341
|
+
})
|
|
342
|
+
});
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const errorText = await response.text().catch(() => "");
|
|
345
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Capture a security event (auth failures, rate limits, suspicious activity, etc.)
|
|
350
|
+
* Returns brute force warning if pattern is detected
|
|
351
|
+
*/
|
|
352
|
+
async captureSecurityEvent(input) {
|
|
353
|
+
if (this.isClosed) return {};
|
|
354
|
+
const payload = {
|
|
355
|
+
...input,
|
|
356
|
+
environment: this.environment
|
|
357
|
+
};
|
|
358
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security`, {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: {
|
|
361
|
+
"Content-Type": "application/json",
|
|
362
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify(payload)
|
|
365
|
+
});
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
const errorText = await response.text().catch(() => "");
|
|
368
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
369
|
+
}
|
|
370
|
+
const result = await response.json();
|
|
371
|
+
return { warning: result.warning };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Capture a login failure event (convenience method)
|
|
375
|
+
*/
|
|
376
|
+
async captureLoginFailure(options) {
|
|
377
|
+
return this.captureSecurityEvent({
|
|
378
|
+
eventType: `login_failed_${options.authMethod || "other"}`,
|
|
379
|
+
category: "AUTHENTICATION",
|
|
380
|
+
severity: "MEDIUM",
|
|
381
|
+
ip: options.ip,
|
|
382
|
+
identifier: options.identifier,
|
|
383
|
+
endpoint: options.endpoint,
|
|
384
|
+
userAgent: options.userAgent,
|
|
385
|
+
metadata: { reason: options.reason, authMethod: options.authMethod }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Capture a successful login event
|
|
390
|
+
*/
|
|
391
|
+
async captureLoginSuccess(options) {
|
|
392
|
+
await this.captureSecurityEvent({
|
|
393
|
+
eventType: `login_success_${options.authMethod || "other"}`,
|
|
394
|
+
category: "AUTHENTICATION",
|
|
395
|
+
severity: "LOW",
|
|
396
|
+
ip: options.ip,
|
|
397
|
+
identifier: options.identifier,
|
|
398
|
+
endpoint: options.endpoint,
|
|
399
|
+
userAgent: options.userAgent,
|
|
400
|
+
metadata: { authMethod: options.authMethod }
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Capture a rate limit event
|
|
405
|
+
*/
|
|
406
|
+
async captureRateLimit(options) {
|
|
407
|
+
await this.captureSecurityEvent({
|
|
408
|
+
eventType: "rate_limit_exceeded",
|
|
409
|
+
category: "RATE_LIMIT",
|
|
410
|
+
severity: "MEDIUM",
|
|
411
|
+
ip: options.ip,
|
|
412
|
+
identifier: options.identifier,
|
|
413
|
+
endpoint: options.endpoint,
|
|
414
|
+
userAgent: options.userAgent,
|
|
415
|
+
metadata: { limit: options.limit, window: options.window }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Capture an authorization failure (user tried to access unauthorized resource)
|
|
420
|
+
*/
|
|
421
|
+
async captureAuthorizationFailure(options) {
|
|
422
|
+
await this.captureSecurityEvent({
|
|
423
|
+
eventType: "authorization_denied",
|
|
424
|
+
category: "AUTHORIZATION",
|
|
425
|
+
severity: "MEDIUM",
|
|
426
|
+
ip: options.ip,
|
|
427
|
+
identifier: options.identifier,
|
|
428
|
+
endpoint: options.endpoint,
|
|
429
|
+
userAgent: options.userAgent,
|
|
430
|
+
metadata: { resource: options.resource, action: options.action }
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Check if an IP or identifier has triggered brute force detection
|
|
435
|
+
*/
|
|
436
|
+
async checkBruteForce(options) {
|
|
437
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security/detect/brute-force`, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: {
|
|
440
|
+
"Content-Type": "application/json",
|
|
441
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify(options)
|
|
444
|
+
});
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
const errorText = await response.text().catch(() => "");
|
|
447
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
448
|
+
}
|
|
449
|
+
const result = await response.json();
|
|
450
|
+
return result.data;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Run npm audit and send results to the monitoring server.
|
|
454
|
+
* This scans the project for known vulnerabilities in dependencies.
|
|
455
|
+
*
|
|
456
|
+
* @param options.projectPath - Path to the project directory (defaults to cwd)
|
|
457
|
+
* @param options.environment - Environment label (defaults to client environment)
|
|
458
|
+
* @returns Audit summary with vulnerability counts
|
|
459
|
+
*/
|
|
460
|
+
async auditDependencies(options = {}) {
|
|
461
|
+
if (typeof require === "undefined") {
|
|
462
|
+
console.warn("[MonitorClient] auditDependencies only works in Node.js environment");
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const startTime = Date.now();
|
|
466
|
+
const environment = options.environment || this.environment;
|
|
467
|
+
try {
|
|
468
|
+
const { execSync } = require("child_process");
|
|
469
|
+
const path = require("path");
|
|
470
|
+
const fs = require("fs");
|
|
471
|
+
let projectPath = options.projectPath || process.cwd();
|
|
472
|
+
if (projectPath.includes("\0") || /[;&|`$(){}[\]<>]/.test(projectPath)) {
|
|
473
|
+
console.error("[MonitorClient] Invalid projectPath: contains forbidden characters");
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
projectPath = path.resolve(projectPath);
|
|
477
|
+
try {
|
|
478
|
+
const stats = fs.statSync(projectPath);
|
|
479
|
+
if (!stats.isDirectory()) {
|
|
480
|
+
console.error("[MonitorClient] projectPath is not a directory");
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
console.error("[MonitorClient] projectPath does not exist");
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
488
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
489
|
+
console.error("[MonitorClient] No package.json found in projectPath");
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
let auditOutput;
|
|
493
|
+
try {
|
|
494
|
+
auditOutput = execSync("npm audit --json", {
|
|
495
|
+
cwd: projectPath,
|
|
496
|
+
encoding: "utf-8",
|
|
497
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
498
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
499
|
+
// 10MB buffer for large outputs
|
|
500
|
+
timeout: 6e4
|
|
501
|
+
// 60 second timeout
|
|
502
|
+
});
|
|
503
|
+
} catch (err) {
|
|
504
|
+
const execError = err;
|
|
505
|
+
if (execError.killed) {
|
|
506
|
+
console.error("[MonitorClient] npm audit timed out");
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
if (execError.stdout) {
|
|
510
|
+
auditOutput = execError.stdout;
|
|
511
|
+
} else {
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const auditData = JSON.parse(auditOutput);
|
|
516
|
+
const vulnerabilities = this.parseNpmAuditOutput(auditData);
|
|
517
|
+
const totalDeps = auditData.metadata?.dependencies?.total || 0;
|
|
518
|
+
const scanDurationMs = Date.now() - startTime;
|
|
519
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/vulnerabilities/audit`, {
|
|
520
|
+
method: "POST",
|
|
521
|
+
headers: {
|
|
522
|
+
"Content-Type": "application/json",
|
|
523
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
524
|
+
},
|
|
525
|
+
body: JSON.stringify({
|
|
526
|
+
environment,
|
|
527
|
+
totalDeps,
|
|
528
|
+
vulnerabilities,
|
|
529
|
+
scanDurationMs
|
|
530
|
+
})
|
|
531
|
+
});
|
|
532
|
+
if (!response.ok) {
|
|
533
|
+
const errorText = await response.text().catch(() => "");
|
|
534
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
535
|
+
}
|
|
536
|
+
const result = await response.json();
|
|
537
|
+
return result.data;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
console.error("[MonitorClient] Failed to audit dependencies:", err instanceof Error ? err.message : String(err));
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Parse npm audit JSON output into vulnerability items
|
|
545
|
+
*/
|
|
546
|
+
parseNpmAuditOutput(auditData) {
|
|
547
|
+
const vulnerabilities = [];
|
|
548
|
+
if (!auditData.vulnerabilities) {
|
|
549
|
+
return vulnerabilities;
|
|
550
|
+
}
|
|
551
|
+
for (const [packageName, vuln] of Object.entries(auditData.vulnerabilities)) {
|
|
552
|
+
const viaDetails = vuln.via.find(
|
|
553
|
+
(v) => typeof v === "object" && "title" in v
|
|
554
|
+
);
|
|
555
|
+
if (viaDetails) {
|
|
556
|
+
vulnerabilities.push({
|
|
557
|
+
packageName,
|
|
558
|
+
severity: vuln.severity.toLowerCase(),
|
|
559
|
+
title: viaDetails.title,
|
|
560
|
+
url: viaDetails.url,
|
|
561
|
+
vulnerableRange: viaDetails.range,
|
|
562
|
+
installedVersion: vuln.nodes?.[0]?.split("@").pop(),
|
|
563
|
+
patchedVersions: this.getFixVersion(vuln.fixAvailable),
|
|
564
|
+
path: vuln.nodes?.join(" > "),
|
|
565
|
+
recommendation: this.getRecommendation(vuln.fixAvailable),
|
|
566
|
+
cwe: viaDetails.cwe,
|
|
567
|
+
cvss: viaDetails.cvss?.score,
|
|
568
|
+
isFixable: Boolean(vuln.fixAvailable),
|
|
569
|
+
isDirect: vuln.isDirect
|
|
570
|
+
});
|
|
571
|
+
} else {
|
|
572
|
+
const viaNames = vuln.via.filter((v) => typeof v === "string");
|
|
573
|
+
vulnerabilities.push({
|
|
574
|
+
packageName,
|
|
575
|
+
severity: vuln.severity.toLowerCase(),
|
|
576
|
+
title: `Depends on vulnerable ${viaNames.join(", ")}`,
|
|
577
|
+
vulnerableRange: vuln.range,
|
|
578
|
+
path: vuln.nodes?.join(" > "),
|
|
579
|
+
isFixable: Boolean(vuln.fixAvailable),
|
|
580
|
+
isDirect: vuln.isDirect
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return vulnerabilities;
|
|
585
|
+
}
|
|
586
|
+
getFixVersion(fixAvailable) {
|
|
587
|
+
if (typeof fixAvailable === "object" && fixAvailable !== null) {
|
|
588
|
+
return fixAvailable.version;
|
|
589
|
+
}
|
|
590
|
+
return void 0;
|
|
591
|
+
}
|
|
592
|
+
getRecommendation(fixAvailable) {
|
|
593
|
+
if (typeof fixAvailable === "object" && fixAvailable !== null) {
|
|
594
|
+
const majorWarning = fixAvailable.isSemVerMajor ? " (breaking change)" : "";
|
|
595
|
+
return `Update ${fixAvailable.name} to ${fixAvailable.version}${majorWarning}`;
|
|
596
|
+
} else if (fixAvailable === true) {
|
|
597
|
+
return "Run npm audit fix";
|
|
598
|
+
}
|
|
599
|
+
return "No fix available";
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
603
|
+
0 && (module.exports = {
|
|
604
|
+
MonitorClient
|
|
605
|
+
});
|