@ceon-oy/monitor-sdk 1.0.7 → 1.0.10

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 CHANGED
@@ -97,6 +97,10 @@ interface MonitorClientConfig {
97
97
  }[];
98
98
  excludePatterns?: string[]; // Glob patterns to exclude (e.g., '@types/*')
99
99
  autoAudit?: boolean; // Enable automatic vulnerability scanning (default: false)
100
+ auditPaths?: { // Multiple directories for vulnerability scanning
101
+ path: string; // Directory path (e.g., '.', '../client')
102
+ environment: string; // Environment label (e.g., 'server', 'client')
103
+ }[];
100
104
  }
101
105
  ```
102
106
 
@@ -227,6 +231,47 @@ With `autoAudit` enabled:
227
231
 
228
232
  Configure the scan interval in the Ceon Monitor dashboard under the project's Vulnerabilities page.
229
233
 
234
+ #### Multi-Directory Vulnerability Auditing
235
+
236
+ For projects with separate server and client directories (or monorepos), use `auditPaths` to scan multiple directories:
237
+
238
+ ```typescript
239
+ const monitor = new MonitorClient({
240
+ apiKey: process.env.CEON_MONITOR_API_KEY!,
241
+ endpoint: 'https://monitor.example.com',
242
+ autoAudit: true,
243
+ auditPaths: [
244
+ { path: '.', environment: 'server' },
245
+ { path: '../client', environment: 'client' },
246
+ ],
247
+ // Also track dependencies from both
248
+ trackDependencies: true,
249
+ dependencySources: [
250
+ { path: './package.json', environment: 'server' },
251
+ { path: '../client/package.json', environment: 'client' },
252
+ ],
253
+ });
254
+ ```
255
+
256
+ With `auditPaths` configured:
257
+ - Each directory is scanned independently using `npm audit`
258
+ - Results are tagged with the environment label (e.g., 'server', 'client')
259
+ - All vulnerabilities appear in a single project dashboard
260
+ - Use the environment filter in the dashboard to view specific environments
261
+
262
+ You can also manually trigger a multi-directory audit:
263
+
264
+ ```typescript
265
+ const result = await monitor.auditMultiplePaths();
266
+ if (result) {
267
+ console.log('Audit results by environment:');
268
+ for (const env of result.results) {
269
+ console.log(` ${env.environment}: ${env.processed} vulnerabilities`);
270
+ }
271
+ console.log('Total:', result.totalSummary);
272
+ }
273
+ ```
274
+
230
275
  #### Manual Scheduled Auditing
231
276
 
232
277
  If you prefer manual control over scheduling:
@@ -725,6 +770,10 @@ Checks for brute force attacks.
725
770
 
726
771
  Runs npm audit and sends results to the server.
727
772
 
773
+ #### `auditMultiplePaths(): Promise<MultiAuditSummary | null>`
774
+
775
+ Runs npm audit on all directories configured in `auditPaths` and returns a combined summary.
776
+
728
777
  #### `flush(): Promise<void>`
729
778
 
730
779
  Immediately sends all queued errors.
@@ -787,6 +836,22 @@ interface SecurityEvent {
787
836
  identifier?: string;
788
837
  metadata?: Record<string, unknown>;
789
838
  }
839
+
840
+ interface AuditPath {
841
+ path: string; // Directory path (e.g., '.', '../client')
842
+ environment: string; // Environment label (e.g., 'server', 'client')
843
+ }
844
+
845
+ interface MultiAuditSummary {
846
+ results: Array<{
847
+ environment: string;
848
+ scanId: string;
849
+ processed: number;
850
+ resolved: number;
851
+ summary: { critical: number; high: number; moderate: number; low: number; info: number };
852
+ }>;
853
+ totalSummary: { critical: number; high: number; moderate: number; low: number; info: number };
854
+ }
790
855
  ```
791
856
 
792
857
  ## License
package/dist/index.d.mts CHANGED
@@ -6,6 +6,12 @@ interface DependencySource {
6
6
  path: string;
7
7
  environment: string;
8
8
  }
9
+ interface AuditPath {
10
+ /** Directory path to run npm audit in (e.g., '.', '../client') */
11
+ path: string;
12
+ /** Environment label for vulnerabilities from this path (e.g., 'server', 'client') */
13
+ environment: string;
14
+ }
9
15
  interface MonitorClientConfig {
10
16
  /** API key for authentication (required, format: cm_xxx) */
11
17
  apiKey: string;
@@ -33,6 +39,8 @@ interface MonitorClientConfig {
33
39
  requestTimeoutMs?: number;
34
40
  /** Enable automatic vulnerability scanning based on server-configured interval (default: false) */
35
41
  autoAudit?: boolean;
42
+ /** Multiple directories to audit for vulnerabilities (runs npm audit in each) */
43
+ auditPaths?: AuditPath[];
36
44
  }
37
45
  interface TechnologyItem {
38
46
  name: string;
@@ -118,6 +126,28 @@ interface AuditSummary {
118
126
  info: number;
119
127
  };
120
128
  }
129
+ interface MultiAuditSummary {
130
+ results: Array<{
131
+ environment: string;
132
+ scanId: string;
133
+ processed: number;
134
+ resolved: number;
135
+ summary: {
136
+ critical: number;
137
+ high: number;
138
+ moderate: number;
139
+ low: number;
140
+ info: number;
141
+ };
142
+ }>;
143
+ totalSummary: {
144
+ critical: number;
145
+ high: number;
146
+ moderate: number;
147
+ low: number;
148
+ info: number;
149
+ };
150
+ }
121
151
 
122
152
  declare class MonitorClient {
123
153
  private apiKey;
@@ -132,6 +162,8 @@ declare class MonitorClient {
132
162
  private packageJsonPath?;
133
163
  private dependencySources?;
134
164
  private excludePatterns;
165
+ private compiledExcludePatterns;
166
+ private auditPaths?;
135
167
  private maxQueueSize;
136
168
  private maxRetries;
137
169
  private retryCount;
@@ -142,7 +174,17 @@ declare class MonitorClient {
142
174
  private settingsPollingTimer;
143
175
  private lastScanTime;
144
176
  private lastKnownScanRequestedAt;
177
+ private lastKnownTechScanRequestedAt;
145
178
  constructor(config: MonitorClientConfig);
179
+ /**
180
+ * Security: Validate and sanitize metadata to prevent oversized payloads
181
+ */
182
+ private sanitizeMetadata;
183
+ /**
184
+ * Security: Sanitize error response text to prevent sensitive data exposure
185
+ * Removes potential API keys, tokens, and limits response length
186
+ */
187
+ private sanitizeErrorResponse;
146
188
  captureError(error: Error, context?: ErrorContext): Promise<void>;
147
189
  captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>;
148
190
  flush(): Promise<void>;
@@ -151,12 +193,13 @@ declare class MonitorClient {
151
193
  private stopAuditIntervalTimer;
152
194
  /**
153
195
  * Fetch project settings from the monitoring server.
154
- * Returns configuration including vulnerability scan interval and scan request timestamp.
196
+ * Returns configuration including vulnerability scan interval and scan request timestamps.
155
197
  */
156
198
  fetchProjectSettings(): Promise<{
157
199
  name: string;
158
200
  vulnerabilityScanIntervalHours: number;
159
201
  scanRequestedAt: string | null;
202
+ techScanRequestedAt: string | null;
160
203
  } | null>;
161
204
  /**
162
205
  * Setup automatic vulnerability scanning based on server-configured interval.
@@ -166,10 +209,11 @@ declare class MonitorClient {
166
209
  private setupAutoAudit;
167
210
  /**
168
211
  * Run a vulnerability scan and track the time it was run.
212
+ * Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
169
213
  */
170
214
  private runScanAndTrackTime;
171
215
  /**
172
- * Check if the server has requested an on-demand scan.
216
+ * Check if the server has requested an on-demand scan (vulnerability or technology).
173
217
  */
174
218
  private checkForScanRequest;
175
219
  private enqueue;
@@ -261,6 +305,16 @@ declare class MonitorClient {
261
305
  projectPath?: string;
262
306
  environment?: string;
263
307
  }): Promise<AuditSummary | null>;
308
+ /**
309
+ * Run npm audit on multiple directories and send results to the monitoring server.
310
+ * This is useful for monorepos or projects with separate client/server directories.
311
+ *
312
+ * Uses the auditPaths configuration to determine which directories to scan.
313
+ * Each path is scanned independently and results are tagged with their environment label.
314
+ *
315
+ * @returns Combined summary of all audit results
316
+ */
317
+ auditMultiplePaths(): Promise<MultiAuditSummary | null>;
264
318
  /**
265
319
  * Parse npm audit JSON output into vulnerability items
266
320
  */
@@ -269,4 +323,4 @@ declare class MonitorClient {
269
323
  private getRecommendation;
270
324
  }
271
325
 
272
- export { type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, MonitorClient, type MonitorClientConfig, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
326
+ export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,12 @@ interface DependencySource {
6
6
  path: string;
7
7
  environment: string;
8
8
  }
9
+ interface AuditPath {
10
+ /** Directory path to run npm audit in (e.g., '.', '../client') */
11
+ path: string;
12
+ /** Environment label for vulnerabilities from this path (e.g., 'server', 'client') */
13
+ environment: string;
14
+ }
9
15
  interface MonitorClientConfig {
10
16
  /** API key for authentication (required, format: cm_xxx) */
11
17
  apiKey: string;
@@ -33,6 +39,8 @@ interface MonitorClientConfig {
33
39
  requestTimeoutMs?: number;
34
40
  /** Enable automatic vulnerability scanning based on server-configured interval (default: false) */
35
41
  autoAudit?: boolean;
42
+ /** Multiple directories to audit for vulnerabilities (runs npm audit in each) */
43
+ auditPaths?: AuditPath[];
36
44
  }
37
45
  interface TechnologyItem {
38
46
  name: string;
@@ -118,6 +126,28 @@ interface AuditSummary {
118
126
  info: number;
119
127
  };
120
128
  }
129
+ interface MultiAuditSummary {
130
+ results: Array<{
131
+ environment: string;
132
+ scanId: string;
133
+ processed: number;
134
+ resolved: number;
135
+ summary: {
136
+ critical: number;
137
+ high: number;
138
+ moderate: number;
139
+ low: number;
140
+ info: number;
141
+ };
142
+ }>;
143
+ totalSummary: {
144
+ critical: number;
145
+ high: number;
146
+ moderate: number;
147
+ low: number;
148
+ info: number;
149
+ };
150
+ }
121
151
 
122
152
  declare class MonitorClient {
123
153
  private apiKey;
@@ -132,6 +162,8 @@ declare class MonitorClient {
132
162
  private packageJsonPath?;
133
163
  private dependencySources?;
134
164
  private excludePatterns;
165
+ private compiledExcludePatterns;
166
+ private auditPaths?;
135
167
  private maxQueueSize;
136
168
  private maxRetries;
137
169
  private retryCount;
@@ -142,7 +174,17 @@ declare class MonitorClient {
142
174
  private settingsPollingTimer;
143
175
  private lastScanTime;
144
176
  private lastKnownScanRequestedAt;
177
+ private lastKnownTechScanRequestedAt;
145
178
  constructor(config: MonitorClientConfig);
179
+ /**
180
+ * Security: Validate and sanitize metadata to prevent oversized payloads
181
+ */
182
+ private sanitizeMetadata;
183
+ /**
184
+ * Security: Sanitize error response text to prevent sensitive data exposure
185
+ * Removes potential API keys, tokens, and limits response length
186
+ */
187
+ private sanitizeErrorResponse;
146
188
  captureError(error: Error, context?: ErrorContext): Promise<void>;
147
189
  captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>;
148
190
  flush(): Promise<void>;
@@ -151,12 +193,13 @@ declare class MonitorClient {
151
193
  private stopAuditIntervalTimer;
152
194
  /**
153
195
  * Fetch project settings from the monitoring server.
154
- * Returns configuration including vulnerability scan interval and scan request timestamp.
196
+ * Returns configuration including vulnerability scan interval and scan request timestamps.
155
197
  */
156
198
  fetchProjectSettings(): Promise<{
157
199
  name: string;
158
200
  vulnerabilityScanIntervalHours: number;
159
201
  scanRequestedAt: string | null;
202
+ techScanRequestedAt: string | null;
160
203
  } | null>;
161
204
  /**
162
205
  * Setup automatic vulnerability scanning based on server-configured interval.
@@ -166,10 +209,11 @@ declare class MonitorClient {
166
209
  private setupAutoAudit;
167
210
  /**
168
211
  * Run a vulnerability scan and track the time it was run.
212
+ * Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
169
213
  */
170
214
  private runScanAndTrackTime;
171
215
  /**
172
- * Check if the server has requested an on-demand scan.
216
+ * Check if the server has requested an on-demand scan (vulnerability or technology).
173
217
  */
174
218
  private checkForScanRequest;
175
219
  private enqueue;
@@ -261,6 +305,16 @@ declare class MonitorClient {
261
305
  projectPath?: string;
262
306
  environment?: string;
263
307
  }): Promise<AuditSummary | null>;
308
+ /**
309
+ * Run npm audit on multiple directories and send results to the monitoring server.
310
+ * This is useful for monorepos or projects with separate client/server directories.
311
+ *
312
+ * Uses the auditPaths configuration to determine which directories to scan.
313
+ * Each path is scanned independently and results are tagged with their environment label.
314
+ *
315
+ * @returns Combined summary of all audit results
316
+ */
317
+ auditMultiplePaths(): Promise<MultiAuditSummary | null>;
264
318
  /**
265
319
  * Parse npm audit JSON output into vulnerability items
266
320
  */
@@ -269,4 +323,4 @@ declare class MonitorClient {
269
323
  private getRecommendation;
270
324
  }
271
325
 
272
- export { type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, MonitorClient, type MonitorClientConfig, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
326
+ export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.js CHANGED
@@ -35,6 +35,25 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/MonitorClient.ts
38
+ var CONFIG_LIMITS = {
39
+ MAX_BATCH_SIZE: 1e3,
40
+ MAX_FLUSH_INTERVAL_MS: 3e5,
41
+ // 5 minutes
42
+ MAX_QUEUE_SIZE: 1e4,
43
+ MAX_RETRIES: 10,
44
+ MAX_REQUEST_TIMEOUT_MS: 6e4,
45
+ // 1 minute
46
+ MAX_METADATA_SIZE_BYTES: 65536,
47
+ // 64KB
48
+ MAX_RETRY_MAP_SIZE: 1e3,
49
+ // Prevent unbounded growth
50
+ AUDIT_MAX_BUFFER: 10 * 1024 * 1024,
51
+ // 10MB
52
+ AUDIT_TIMEOUT_MS: 6e4,
53
+ // 60 seconds
54
+ SETTINGS_POLL_INTERVAL_MS: 5 * 60 * 1e3
55
+ // 5 minutes
56
+ };
38
57
  var MonitorClient = class {
39
58
  constructor(config) {
40
59
  this.queue = [];
@@ -46,6 +65,7 @@ var MonitorClient = class {
46
65
  this.settingsPollingTimer = null;
47
66
  this.lastScanTime = null;
48
67
  this.lastKnownScanRequestedAt = null;
68
+ this.lastKnownTechScanRequestedAt = null;
49
69
  if (!config.apiKey || config.apiKey.trim().length === 0) {
50
70
  throw new Error("[MonitorClient] API key is required");
51
71
  }
@@ -72,14 +92,14 @@ var MonitorClient = class {
72
92
  this.apiKey = config.apiKey;
73
93
  this.endpoint = config.endpoint.replace(/\/$/, "");
74
94
  this.environment = config.environment || "production";
75
- this.batchSize = Math.max(1, config.batchSize || 10);
76
- this.flushIntervalMs = Math.max(1e3, config.flushIntervalMs || 5e3);
95
+ this.batchSize = Math.min(CONFIG_LIMITS.MAX_BATCH_SIZE, Math.max(1, config.batchSize || 10));
96
+ this.flushIntervalMs = Math.min(CONFIG_LIMITS.MAX_FLUSH_INTERVAL_MS, Math.max(1e3, config.flushIntervalMs || 5e3));
77
97
  this.trackDependencies = config.trackDependencies || false;
78
98
  this.packageJsonPath = config.packageJsonPath;
79
99
  this.dependencySources = config.dependencySources;
80
- this.maxQueueSize = config.maxQueueSize || 1e3;
81
- this.maxRetries = config.maxRetries || 3;
82
- this.requestTimeoutMs = config.requestTimeoutMs || 1e4;
100
+ this.maxQueueSize = Math.min(CONFIG_LIMITS.MAX_QUEUE_SIZE, config.maxQueueSize || 1e3);
101
+ this.maxRetries = Math.min(CONFIG_LIMITS.MAX_RETRIES, config.maxRetries || 3);
102
+ this.requestTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REQUEST_TIMEOUT_MS, config.requestTimeoutMs || 1e4);
83
103
  const defaultExcludePatterns = [
84
104
  "@types/*",
85
105
  "eslint*",
@@ -90,7 +110,12 @@ var MonitorClient = class {
90
110
  "@typescript-eslint/*"
91
111
  ];
92
112
  this.excludePatterns = config.excludePatterns || defaultExcludePatterns;
113
+ this.compiledExcludePatterns = this.excludePatterns.map((pattern) => {
114
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
115
+ return new RegExp(`^${regexPattern}$`);
116
+ });
93
117
  this.autoAudit = config.autoAudit || false;
118
+ this.auditPaths = config.auditPaths;
94
119
  this.startFlushTimer();
95
120
  if (this.trackDependencies) {
96
121
  this.syncDependencies().catch((err) => {
@@ -103,6 +128,36 @@ var MonitorClient = class {
103
128
  });
104
129
  }
105
130
  }
131
+ /**
132
+ * Security: Validate and sanitize metadata to prevent oversized payloads
133
+ */
134
+ sanitizeMetadata(metadata) {
135
+ if (!metadata) return void 0;
136
+ try {
137
+ const jsonStr = JSON.stringify(metadata);
138
+ if (jsonStr.length > CONFIG_LIMITS.MAX_METADATA_SIZE_BYTES) {
139
+ console.warn(`[MonitorClient] Metadata exceeds size limit (${CONFIG_LIMITS.MAX_METADATA_SIZE_BYTES} bytes), truncating`);
140
+ return { _truncated: true, _originalSize: jsonStr.length };
141
+ }
142
+ return metadata;
143
+ } catch {
144
+ console.warn("[MonitorClient] Metadata contains non-serializable data, skipping");
145
+ return { _error: "non-serializable" };
146
+ }
147
+ }
148
+ /**
149
+ * Security: Sanitize error response text to prevent sensitive data exposure
150
+ * Removes potential API keys, tokens, and limits response length
151
+ */
152
+ sanitizeErrorResponse(errorText) {
153
+ if (!errorText) return "";
154
+ const maxLength = 500;
155
+ let sanitized = errorText.length > maxLength ? errorText.substring(0, maxLength) + "..." : errorText;
156
+ sanitized = sanitized.replace(/cm_[a-zA-Z0-9_-]+/g, "[REDACTED]");
157
+ sanitized = sanitized.replace(/Bearer\s+[a-zA-Z0-9_.-]+/gi, "Bearer [REDACTED]");
158
+ sanitized = sanitized.replace(/authorization['":\s]+[a-zA-Z0-9_.-]+/gi, "authorization: [REDACTED]");
159
+ return sanitized;
160
+ }
106
161
  async captureError(error, context) {
107
162
  if (this.isClosed) return;
108
163
  const payload = {
@@ -116,7 +171,7 @@ var MonitorClient = class {
116
171
  userAgent: context?.userAgent,
117
172
  ip: context?.ip,
118
173
  requestId: context?.requestId,
119
- metadata: context?.metadata
174
+ metadata: this.sanitizeMetadata(context?.metadata)
120
175
  };
121
176
  this.enqueue(payload);
122
177
  }
@@ -132,7 +187,7 @@ var MonitorClient = class {
132
187
  userAgent: context?.userAgent,
133
188
  ip: context?.ip,
134
189
  requestId: context?.requestId,
135
- metadata: context?.metadata
190
+ metadata: this.sanitizeMetadata(context?.metadata)
136
191
  };
137
192
  this.enqueue(payload);
138
193
  }
@@ -156,6 +211,12 @@ var MonitorClient = class {
156
211
  const key = this.getErrorKey(error);
157
212
  const retries = this.retryCount.get(key) || 0;
158
213
  if (retries < this.maxRetries) {
214
+ if (this.retryCount.size >= CONFIG_LIMITS.MAX_RETRY_MAP_SIZE) {
215
+ const keysToRemove = Array.from(this.retryCount.keys()).slice(0, Math.ceil(CONFIG_LIMITS.MAX_RETRY_MAP_SIZE * 0.1));
216
+ for (const oldKey of keysToRemove) {
217
+ this.retryCount.delete(oldKey);
218
+ }
219
+ }
159
220
  this.retryCount.set(key, retries + 1);
160
221
  if (this.queue.length < this.maxQueueSize) {
161
222
  this.queue.push(error);
@@ -193,7 +254,7 @@ var MonitorClient = class {
193
254
  }
194
255
  /**
195
256
  * Fetch project settings from the monitoring server.
196
- * Returns configuration including vulnerability scan interval and scan request timestamp.
257
+ * Returns configuration including vulnerability scan interval and scan request timestamps.
197
258
  */
198
259
  async fetchProjectSettings() {
199
260
  try {
@@ -229,6 +290,9 @@ var MonitorClient = class {
229
290
  if (settings.scanRequestedAt) {
230
291
  this.lastKnownScanRequestedAt = new Date(settings.scanRequestedAt);
231
292
  }
293
+ if (settings.techScanRequestedAt) {
294
+ this.lastKnownTechScanRequestedAt = new Date(settings.techScanRequestedAt);
295
+ }
232
296
  const intervalHours = settings.vulnerabilityScanIntervalHours;
233
297
  if (intervalHours <= 0) {
234
298
  console.log("[MonitorClient] Scheduled vulnerability scanning disabled by server configuration");
@@ -237,37 +301,56 @@ var MonitorClient = class {
237
301
  await this.runScanAndTrackTime();
238
302
  const intervalMs = intervalHours * 60 * 60 * 1e3;
239
303
  this.auditIntervalTimer = setInterval(() => {
240
- this.runScanAndTrackTime();
304
+ this.runScanAndTrackTime().catch((err) => {
305
+ console.error("[MonitorClient] Auto audit scan failed:", err);
306
+ });
241
307
  }, intervalMs);
242
308
  }
243
309
  console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
244
310
  this.settingsPollingTimer = setInterval(() => {
245
- this.checkForScanRequest();
246
- }, 5 * 60 * 1e3);
311
+ this.checkForScanRequest().catch((err) => {
312
+ console.error("[MonitorClient] Scan request check failed:", err);
313
+ });
314
+ }, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
247
315
  }
248
316
  /**
249
317
  * Run a vulnerability scan and track the time it was run.
318
+ * Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
250
319
  */
251
320
  async runScanAndTrackTime() {
252
321
  try {
253
- await this.auditDependencies();
322
+ if (this.auditPaths && this.auditPaths.length > 0) {
323
+ await this.auditMultiplePaths();
324
+ } else {
325
+ await this.auditDependencies();
326
+ }
254
327
  this.lastScanTime = /* @__PURE__ */ new Date();
255
328
  } catch (err) {
256
329
  console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
257
330
  }
258
331
  }
259
332
  /**
260
- * Check if the server has requested an on-demand scan.
333
+ * Check if the server has requested an on-demand scan (vulnerability or technology).
261
334
  */
262
335
  async checkForScanRequest() {
263
336
  try {
264
337
  const settings = await this.fetchProjectSettings();
265
- if (!settings || !settings.scanRequestedAt) return;
266
- const scanRequestedAt = new Date(settings.scanRequestedAt);
267
- if (!this.lastKnownScanRequestedAt || scanRequestedAt > this.lastKnownScanRequestedAt) {
268
- console.log("[MonitorClient] On-demand scan requested by server");
269
- this.lastKnownScanRequestedAt = scanRequestedAt;
270
- await this.runScanAndTrackTime();
338
+ if (!settings) return;
339
+ if (settings.scanRequestedAt) {
340
+ const scanRequestedAt = new Date(settings.scanRequestedAt);
341
+ if (!this.lastKnownScanRequestedAt || scanRequestedAt > this.lastKnownScanRequestedAt) {
342
+ console.log("[MonitorClient] On-demand vulnerability scan requested by server");
343
+ this.lastKnownScanRequestedAt = scanRequestedAt;
344
+ await this.runScanAndTrackTime();
345
+ }
346
+ }
347
+ if (settings.techScanRequestedAt) {
348
+ const techScanRequestedAt = new Date(settings.techScanRequestedAt);
349
+ if (!this.lastKnownTechScanRequestedAt || techScanRequestedAt > this.lastKnownTechScanRequestedAt) {
350
+ console.log("[MonitorClient] On-demand technology scan requested by server");
351
+ this.lastKnownTechScanRequestedAt = techScanRequestedAt;
352
+ await this.syncDependencies();
353
+ }
271
354
  }
272
355
  } catch (err) {
273
356
  console.error("[MonitorClient] Failed to check for scan request:", err instanceof Error ? err.message : String(err));
@@ -280,7 +363,9 @@ var MonitorClient = class {
280
363
  }
281
364
  this.queue.push(payload);
282
365
  if (this.queue.length >= this.batchSize) {
283
- this.flush();
366
+ this.flush().catch((err) => {
367
+ console.error("[MonitorClient] Flush failed:", err);
368
+ });
284
369
  }
285
370
  }
286
371
  /**
@@ -315,7 +400,7 @@ var MonitorClient = class {
315
400
  });
316
401
  if (!response.ok) {
317
402
  const errorText = await response.text().catch(() => "");
318
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
403
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
319
404
  }
320
405
  }
321
406
  async sendBatch(errors) {
@@ -329,12 +414,14 @@ var MonitorClient = class {
329
414
  });
330
415
  if (!response.ok) {
331
416
  const errorText = await response.text().catch(() => "");
332
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
417
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
333
418
  }
334
419
  }
335
420
  startFlushTimer() {
336
421
  this.flushTimer = setInterval(() => {
337
- this.flush();
422
+ this.flush().catch((err) => {
423
+ console.error("[MonitorClient] Scheduled flush failed:", err);
424
+ });
338
425
  }, this.flushIntervalMs);
339
426
  }
340
427
  stopFlushTimer() {
@@ -397,6 +484,16 @@ var MonitorClient = class {
397
484
  if (!fs.existsSync(normalizedPath)) {
398
485
  return [];
399
486
  }
487
+ try {
488
+ const realPath = fs.realpathSync(normalizedPath);
489
+ const realBase = fs.realpathSync(normalizedBase);
490
+ if (!path.isAbsolute(packagePath) && !realPath.startsWith(realBase)) {
491
+ console.warn("[MonitorClient] Symlink points outside allowed directory:", packagePath);
492
+ return [];
493
+ }
494
+ } catch {
495
+ return [];
496
+ }
400
497
  const packageJson = JSON.parse(fs.readFileSync(normalizedPath, "utf-8"));
401
498
  const technologies = [];
402
499
  const deps = {
@@ -420,9 +517,8 @@ var MonitorClient = class {
420
517
  }
421
518
  }
422
519
  shouldExclude(packageName) {
423
- for (const pattern of this.excludePatterns) {
424
- const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
425
- if (new RegExp(`^${regexPattern}$`).test(packageName)) {
520
+ for (const regex of this.compiledExcludePatterns) {
521
+ if (regex.test(packageName)) {
426
522
  return true;
427
523
  }
428
524
  }
@@ -445,7 +541,7 @@ var MonitorClient = class {
445
541
  });
446
542
  if (!response.ok) {
447
543
  const errorText = await response.text().catch(() => "");
448
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
544
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
449
545
  }
450
546
  }
451
547
  /**
@@ -456,6 +552,7 @@ var MonitorClient = class {
456
552
  if (this.isClosed) return {};
457
553
  const payload = {
458
554
  ...input,
555
+ metadata: this.sanitizeMetadata(input.metadata),
459
556
  environment: this.environment
460
557
  };
461
558
  const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security`, {
@@ -468,7 +565,7 @@ var MonitorClient = class {
468
565
  });
469
566
  if (!response.ok) {
470
567
  const errorText = await response.text().catch(() => "");
471
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
568
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
472
569
  }
473
570
  const result = await response.json();
474
571
  return { warning: result.warning };
@@ -547,7 +644,7 @@ var MonitorClient = class {
547
644
  });
548
645
  if (!response.ok) {
549
646
  const errorText = await response.text().catch(() => "");
550
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
647
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
551
648
  }
552
649
  const result = await response.json();
553
650
  return result.data;
@@ -588,6 +685,12 @@ var MonitorClient = class {
588
685
  return null;
589
686
  }
590
687
  projectPath = path.resolve(projectPath);
688
+ try {
689
+ projectPath = fs.realpathSync(projectPath);
690
+ } catch {
691
+ console.error("[MonitorClient] projectPath does not exist or is inaccessible");
692
+ return null;
693
+ }
591
694
  try {
592
695
  const stats = fs.statSync(projectPath);
593
696
  if (!stats.isDirectory()) {
@@ -598,21 +701,19 @@ var MonitorClient = class {
598
701
  console.error("[MonitorClient] projectPath does not exist");
599
702
  return null;
600
703
  }
601
- const packageJsonPath = path.join(projectPath, "package.json");
602
- if (!fs.existsSync(packageJsonPath)) {
603
- console.error("[MonitorClient] No package.json found in projectPath");
604
- return null;
605
- }
606
704
  let auditOutput;
607
705
  try {
706
+ const packageJsonPath = path.join(projectPath, "package.json");
707
+ if (!fs.existsSync(packageJsonPath)) {
708
+ console.error("[MonitorClient] No package.json found in projectPath");
709
+ return null;
710
+ }
608
711
  auditOutput = execSync("npm audit --json", {
609
712
  cwd: projectPath,
610
713
  encoding: "utf-8",
611
714
  stdio: ["pipe", "pipe", "pipe"],
612
- maxBuffer: 10 * 1024 * 1024,
613
- // 10MB buffer for large outputs
614
- timeout: 6e4
615
- // 60 second timeout
715
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
716
+ timeout: CONFIG_LIMITS.AUDIT_TIMEOUT_MS
616
717
  });
617
718
  } catch (err) {
618
719
  const execError = err;
@@ -654,6 +755,63 @@ var MonitorClient = class {
654
755
  return null;
655
756
  }
656
757
  }
758
+ /**
759
+ * Run npm audit on multiple directories and send results to the monitoring server.
760
+ * This is useful for monorepos or projects with separate client/server directories.
761
+ *
762
+ * Uses the auditPaths configuration to determine which directories to scan.
763
+ * Each path is scanned independently and results are tagged with their environment label.
764
+ *
765
+ * @returns Combined summary of all audit results
766
+ */
767
+ async auditMultiplePaths() {
768
+ if (!this.auditPaths || this.auditPaths.length === 0) {
769
+ console.warn("[MonitorClient] No auditPaths configured");
770
+ return null;
771
+ }
772
+ const results = [];
773
+ const totalSummary = {
774
+ critical: 0,
775
+ high: 0,
776
+ moderate: 0,
777
+ low: 0,
778
+ info: 0
779
+ };
780
+ for (const auditPath of this.auditPaths) {
781
+ console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${auditPath.path}`);
782
+ try {
783
+ const summary = await this.auditDependencies({
784
+ projectPath: auditPath.path,
785
+ environment: auditPath.environment
786
+ });
787
+ if (summary) {
788
+ results.push({
789
+ environment: auditPath.environment,
790
+ scanId: summary.scanId,
791
+ processed: summary.processed,
792
+ resolved: summary.resolved,
793
+ summary: summary.summary
794
+ });
795
+ totalSummary.critical += summary.summary.critical;
796
+ totalSummary.high += summary.summary.high;
797
+ totalSummary.moderate += summary.summary.moderate;
798
+ totalSummary.low += summary.summary.low;
799
+ totalSummary.info += summary.summary.info;
800
+ }
801
+ } catch (err) {
802
+ console.error(`[MonitorClient] Failed to audit ${auditPath.environment}:`, err instanceof Error ? err.message : String(err));
803
+ }
804
+ }
805
+ if (results.length === 0) {
806
+ console.warn("[MonitorClient] No audit results collected from any path");
807
+ return null;
808
+ }
809
+ console.log(`[MonitorClient] Multi-path audit complete: ${results.length} paths scanned`);
810
+ return {
811
+ results,
812
+ totalSummary
813
+ };
814
+ }
657
815
  /**
658
816
  * Parse npm audit JSON output into vulnerability items
659
817
  */
@@ -663,6 +821,9 @@ var MonitorClient = class {
663
821
  return vulnerabilities;
664
822
  }
665
823
  for (const [packageName, vuln] of Object.entries(auditData.vulnerabilities)) {
824
+ if (!vuln.via || !Array.isArray(vuln.via)) {
825
+ continue;
826
+ }
666
827
  const viaDetails = vuln.via.find(
667
828
  (v) => typeof v === "object" && "title" in v
668
829
  );
package/dist/index.mjs CHANGED
@@ -1,4 +1,23 @@
1
1
  // src/MonitorClient.ts
2
+ var CONFIG_LIMITS = {
3
+ MAX_BATCH_SIZE: 1e3,
4
+ MAX_FLUSH_INTERVAL_MS: 3e5,
5
+ // 5 minutes
6
+ MAX_QUEUE_SIZE: 1e4,
7
+ MAX_RETRIES: 10,
8
+ MAX_REQUEST_TIMEOUT_MS: 6e4,
9
+ // 1 minute
10
+ MAX_METADATA_SIZE_BYTES: 65536,
11
+ // 64KB
12
+ MAX_RETRY_MAP_SIZE: 1e3,
13
+ // Prevent unbounded growth
14
+ AUDIT_MAX_BUFFER: 10 * 1024 * 1024,
15
+ // 10MB
16
+ AUDIT_TIMEOUT_MS: 6e4,
17
+ // 60 seconds
18
+ SETTINGS_POLL_INTERVAL_MS: 5 * 60 * 1e3
19
+ // 5 minutes
20
+ };
2
21
  var MonitorClient = class {
3
22
  constructor(config) {
4
23
  this.queue = [];
@@ -10,6 +29,7 @@ var MonitorClient = class {
10
29
  this.settingsPollingTimer = null;
11
30
  this.lastScanTime = null;
12
31
  this.lastKnownScanRequestedAt = null;
32
+ this.lastKnownTechScanRequestedAt = null;
13
33
  if (!config.apiKey || config.apiKey.trim().length === 0) {
14
34
  throw new Error("[MonitorClient] API key is required");
15
35
  }
@@ -36,14 +56,14 @@ var MonitorClient = class {
36
56
  this.apiKey = config.apiKey;
37
57
  this.endpoint = config.endpoint.replace(/\/$/, "");
38
58
  this.environment = config.environment || "production";
39
- this.batchSize = Math.max(1, config.batchSize || 10);
40
- this.flushIntervalMs = Math.max(1e3, config.flushIntervalMs || 5e3);
59
+ this.batchSize = Math.min(CONFIG_LIMITS.MAX_BATCH_SIZE, Math.max(1, config.batchSize || 10));
60
+ this.flushIntervalMs = Math.min(CONFIG_LIMITS.MAX_FLUSH_INTERVAL_MS, Math.max(1e3, config.flushIntervalMs || 5e3));
41
61
  this.trackDependencies = config.trackDependencies || false;
42
62
  this.packageJsonPath = config.packageJsonPath;
43
63
  this.dependencySources = config.dependencySources;
44
- this.maxQueueSize = config.maxQueueSize || 1e3;
45
- this.maxRetries = config.maxRetries || 3;
46
- this.requestTimeoutMs = config.requestTimeoutMs || 1e4;
64
+ this.maxQueueSize = Math.min(CONFIG_LIMITS.MAX_QUEUE_SIZE, config.maxQueueSize || 1e3);
65
+ this.maxRetries = Math.min(CONFIG_LIMITS.MAX_RETRIES, config.maxRetries || 3);
66
+ this.requestTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REQUEST_TIMEOUT_MS, config.requestTimeoutMs || 1e4);
47
67
  const defaultExcludePatterns = [
48
68
  "@types/*",
49
69
  "eslint*",
@@ -54,7 +74,12 @@ var MonitorClient = class {
54
74
  "@typescript-eslint/*"
55
75
  ];
56
76
  this.excludePatterns = config.excludePatterns || defaultExcludePatterns;
77
+ this.compiledExcludePatterns = this.excludePatterns.map((pattern) => {
78
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
79
+ return new RegExp(`^${regexPattern}$`);
80
+ });
57
81
  this.autoAudit = config.autoAudit || false;
82
+ this.auditPaths = config.auditPaths;
58
83
  this.startFlushTimer();
59
84
  if (this.trackDependencies) {
60
85
  this.syncDependencies().catch((err) => {
@@ -67,6 +92,36 @@ var MonitorClient = class {
67
92
  });
68
93
  }
69
94
  }
95
+ /**
96
+ * Security: Validate and sanitize metadata to prevent oversized payloads
97
+ */
98
+ sanitizeMetadata(metadata) {
99
+ if (!metadata) return void 0;
100
+ try {
101
+ const jsonStr = JSON.stringify(metadata);
102
+ if (jsonStr.length > CONFIG_LIMITS.MAX_METADATA_SIZE_BYTES) {
103
+ console.warn(`[MonitorClient] Metadata exceeds size limit (${CONFIG_LIMITS.MAX_METADATA_SIZE_BYTES} bytes), truncating`);
104
+ return { _truncated: true, _originalSize: jsonStr.length };
105
+ }
106
+ return metadata;
107
+ } catch {
108
+ console.warn("[MonitorClient] Metadata contains non-serializable data, skipping");
109
+ return { _error: "non-serializable" };
110
+ }
111
+ }
112
+ /**
113
+ * Security: Sanitize error response text to prevent sensitive data exposure
114
+ * Removes potential API keys, tokens, and limits response length
115
+ */
116
+ sanitizeErrorResponse(errorText) {
117
+ if (!errorText) return "";
118
+ const maxLength = 500;
119
+ let sanitized = errorText.length > maxLength ? errorText.substring(0, maxLength) + "..." : errorText;
120
+ sanitized = sanitized.replace(/cm_[a-zA-Z0-9_-]+/g, "[REDACTED]");
121
+ sanitized = sanitized.replace(/Bearer\s+[a-zA-Z0-9_.-]+/gi, "Bearer [REDACTED]");
122
+ sanitized = sanitized.replace(/authorization['":\s]+[a-zA-Z0-9_.-]+/gi, "authorization: [REDACTED]");
123
+ return sanitized;
124
+ }
70
125
  async captureError(error, context) {
71
126
  if (this.isClosed) return;
72
127
  const payload = {
@@ -80,7 +135,7 @@ var MonitorClient = class {
80
135
  userAgent: context?.userAgent,
81
136
  ip: context?.ip,
82
137
  requestId: context?.requestId,
83
- metadata: context?.metadata
138
+ metadata: this.sanitizeMetadata(context?.metadata)
84
139
  };
85
140
  this.enqueue(payload);
86
141
  }
@@ -96,7 +151,7 @@ var MonitorClient = class {
96
151
  userAgent: context?.userAgent,
97
152
  ip: context?.ip,
98
153
  requestId: context?.requestId,
99
- metadata: context?.metadata
154
+ metadata: this.sanitizeMetadata(context?.metadata)
100
155
  };
101
156
  this.enqueue(payload);
102
157
  }
@@ -120,6 +175,12 @@ var MonitorClient = class {
120
175
  const key = this.getErrorKey(error);
121
176
  const retries = this.retryCount.get(key) || 0;
122
177
  if (retries < this.maxRetries) {
178
+ if (this.retryCount.size >= CONFIG_LIMITS.MAX_RETRY_MAP_SIZE) {
179
+ const keysToRemove = Array.from(this.retryCount.keys()).slice(0, Math.ceil(CONFIG_LIMITS.MAX_RETRY_MAP_SIZE * 0.1));
180
+ for (const oldKey of keysToRemove) {
181
+ this.retryCount.delete(oldKey);
182
+ }
183
+ }
123
184
  this.retryCount.set(key, retries + 1);
124
185
  if (this.queue.length < this.maxQueueSize) {
125
186
  this.queue.push(error);
@@ -157,7 +218,7 @@ var MonitorClient = class {
157
218
  }
158
219
  /**
159
220
  * Fetch project settings from the monitoring server.
160
- * Returns configuration including vulnerability scan interval and scan request timestamp.
221
+ * Returns configuration including vulnerability scan interval and scan request timestamps.
161
222
  */
162
223
  async fetchProjectSettings() {
163
224
  try {
@@ -193,6 +254,9 @@ var MonitorClient = class {
193
254
  if (settings.scanRequestedAt) {
194
255
  this.lastKnownScanRequestedAt = new Date(settings.scanRequestedAt);
195
256
  }
257
+ if (settings.techScanRequestedAt) {
258
+ this.lastKnownTechScanRequestedAt = new Date(settings.techScanRequestedAt);
259
+ }
196
260
  const intervalHours = settings.vulnerabilityScanIntervalHours;
197
261
  if (intervalHours <= 0) {
198
262
  console.log("[MonitorClient] Scheduled vulnerability scanning disabled by server configuration");
@@ -201,37 +265,56 @@ var MonitorClient = class {
201
265
  await this.runScanAndTrackTime();
202
266
  const intervalMs = intervalHours * 60 * 60 * 1e3;
203
267
  this.auditIntervalTimer = setInterval(() => {
204
- this.runScanAndTrackTime();
268
+ this.runScanAndTrackTime().catch((err) => {
269
+ console.error("[MonitorClient] Auto audit scan failed:", err);
270
+ });
205
271
  }, intervalMs);
206
272
  }
207
273
  console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
208
274
  this.settingsPollingTimer = setInterval(() => {
209
- this.checkForScanRequest();
210
- }, 5 * 60 * 1e3);
275
+ this.checkForScanRequest().catch((err) => {
276
+ console.error("[MonitorClient] Scan request check failed:", err);
277
+ });
278
+ }, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
211
279
  }
212
280
  /**
213
281
  * Run a vulnerability scan and track the time it was run.
282
+ * Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
214
283
  */
215
284
  async runScanAndTrackTime() {
216
285
  try {
217
- await this.auditDependencies();
286
+ if (this.auditPaths && this.auditPaths.length > 0) {
287
+ await this.auditMultiplePaths();
288
+ } else {
289
+ await this.auditDependencies();
290
+ }
218
291
  this.lastScanTime = /* @__PURE__ */ new Date();
219
292
  } catch (err) {
220
293
  console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
221
294
  }
222
295
  }
223
296
  /**
224
- * Check if the server has requested an on-demand scan.
297
+ * Check if the server has requested an on-demand scan (vulnerability or technology).
225
298
  */
226
299
  async checkForScanRequest() {
227
300
  try {
228
301
  const settings = await this.fetchProjectSettings();
229
- if (!settings || !settings.scanRequestedAt) return;
230
- const scanRequestedAt = new Date(settings.scanRequestedAt);
231
- if (!this.lastKnownScanRequestedAt || scanRequestedAt > this.lastKnownScanRequestedAt) {
232
- console.log("[MonitorClient] On-demand scan requested by server");
233
- this.lastKnownScanRequestedAt = scanRequestedAt;
234
- await this.runScanAndTrackTime();
302
+ if (!settings) return;
303
+ if (settings.scanRequestedAt) {
304
+ const scanRequestedAt = new Date(settings.scanRequestedAt);
305
+ if (!this.lastKnownScanRequestedAt || scanRequestedAt > this.lastKnownScanRequestedAt) {
306
+ console.log("[MonitorClient] On-demand vulnerability scan requested by server");
307
+ this.lastKnownScanRequestedAt = scanRequestedAt;
308
+ await this.runScanAndTrackTime();
309
+ }
310
+ }
311
+ if (settings.techScanRequestedAt) {
312
+ const techScanRequestedAt = new Date(settings.techScanRequestedAt);
313
+ if (!this.lastKnownTechScanRequestedAt || techScanRequestedAt > this.lastKnownTechScanRequestedAt) {
314
+ console.log("[MonitorClient] On-demand technology scan requested by server");
315
+ this.lastKnownTechScanRequestedAt = techScanRequestedAt;
316
+ await this.syncDependencies();
317
+ }
235
318
  }
236
319
  } catch (err) {
237
320
  console.error("[MonitorClient] Failed to check for scan request:", err instanceof Error ? err.message : String(err));
@@ -244,7 +327,9 @@ var MonitorClient = class {
244
327
  }
245
328
  this.queue.push(payload);
246
329
  if (this.queue.length >= this.batchSize) {
247
- this.flush();
330
+ this.flush().catch((err) => {
331
+ console.error("[MonitorClient] Flush failed:", err);
332
+ });
248
333
  }
249
334
  }
250
335
  /**
@@ -279,7 +364,7 @@ var MonitorClient = class {
279
364
  });
280
365
  if (!response.ok) {
281
366
  const errorText = await response.text().catch(() => "");
282
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
367
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
283
368
  }
284
369
  }
285
370
  async sendBatch(errors) {
@@ -293,12 +378,14 @@ var MonitorClient = class {
293
378
  });
294
379
  if (!response.ok) {
295
380
  const errorText = await response.text().catch(() => "");
296
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
381
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
297
382
  }
298
383
  }
299
384
  startFlushTimer() {
300
385
  this.flushTimer = setInterval(() => {
301
- this.flush();
386
+ this.flush().catch((err) => {
387
+ console.error("[MonitorClient] Scheduled flush failed:", err);
388
+ });
302
389
  }, this.flushIntervalMs);
303
390
  }
304
391
  stopFlushTimer() {
@@ -361,6 +448,16 @@ var MonitorClient = class {
361
448
  if (!fs.existsSync(normalizedPath)) {
362
449
  return [];
363
450
  }
451
+ try {
452
+ const realPath = fs.realpathSync(normalizedPath);
453
+ const realBase = fs.realpathSync(normalizedBase);
454
+ if (!path.isAbsolute(packagePath) && !realPath.startsWith(realBase)) {
455
+ console.warn("[MonitorClient] Symlink points outside allowed directory:", packagePath);
456
+ return [];
457
+ }
458
+ } catch {
459
+ return [];
460
+ }
364
461
  const packageJson = JSON.parse(fs.readFileSync(normalizedPath, "utf-8"));
365
462
  const technologies = [];
366
463
  const deps = {
@@ -384,9 +481,8 @@ var MonitorClient = class {
384
481
  }
385
482
  }
386
483
  shouldExclude(packageName) {
387
- for (const pattern of this.excludePatterns) {
388
- const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
389
- if (new RegExp(`^${regexPattern}$`).test(packageName)) {
484
+ for (const regex of this.compiledExcludePatterns) {
485
+ if (regex.test(packageName)) {
390
486
  return true;
391
487
  }
392
488
  }
@@ -409,7 +505,7 @@ var MonitorClient = class {
409
505
  });
410
506
  if (!response.ok) {
411
507
  const errorText = await response.text().catch(() => "");
412
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
508
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
413
509
  }
414
510
  }
415
511
  /**
@@ -420,6 +516,7 @@ var MonitorClient = class {
420
516
  if (this.isClosed) return {};
421
517
  const payload = {
422
518
  ...input,
519
+ metadata: this.sanitizeMetadata(input.metadata),
423
520
  environment: this.environment
424
521
  };
425
522
  const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security`, {
@@ -432,7 +529,7 @@ var MonitorClient = class {
432
529
  });
433
530
  if (!response.ok) {
434
531
  const errorText = await response.text().catch(() => "");
435
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
532
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
436
533
  }
437
534
  const result = await response.json();
438
535
  return { warning: result.warning };
@@ -511,7 +608,7 @@ var MonitorClient = class {
511
608
  });
512
609
  if (!response.ok) {
513
610
  const errorText = await response.text().catch(() => "");
514
- throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
611
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
515
612
  }
516
613
  const result = await response.json();
517
614
  return result.data;
@@ -552,6 +649,12 @@ var MonitorClient = class {
552
649
  return null;
553
650
  }
554
651
  projectPath = path.resolve(projectPath);
652
+ try {
653
+ projectPath = fs.realpathSync(projectPath);
654
+ } catch {
655
+ console.error("[MonitorClient] projectPath does not exist or is inaccessible");
656
+ return null;
657
+ }
555
658
  try {
556
659
  const stats = fs.statSync(projectPath);
557
660
  if (!stats.isDirectory()) {
@@ -562,21 +665,19 @@ var MonitorClient = class {
562
665
  console.error("[MonitorClient] projectPath does not exist");
563
666
  return null;
564
667
  }
565
- const packageJsonPath = path.join(projectPath, "package.json");
566
- if (!fs.existsSync(packageJsonPath)) {
567
- console.error("[MonitorClient] No package.json found in projectPath");
568
- return null;
569
- }
570
668
  let auditOutput;
571
669
  try {
670
+ const packageJsonPath = path.join(projectPath, "package.json");
671
+ if (!fs.existsSync(packageJsonPath)) {
672
+ console.error("[MonitorClient] No package.json found in projectPath");
673
+ return null;
674
+ }
572
675
  auditOutput = execSync("npm audit --json", {
573
676
  cwd: projectPath,
574
677
  encoding: "utf-8",
575
678
  stdio: ["pipe", "pipe", "pipe"],
576
- maxBuffer: 10 * 1024 * 1024,
577
- // 10MB buffer for large outputs
578
- timeout: 6e4
579
- // 60 second timeout
679
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
680
+ timeout: CONFIG_LIMITS.AUDIT_TIMEOUT_MS
580
681
  });
581
682
  } catch (err) {
582
683
  const execError = err;
@@ -618,6 +719,63 @@ var MonitorClient = class {
618
719
  return null;
619
720
  }
620
721
  }
722
+ /**
723
+ * Run npm audit on multiple directories and send results to the monitoring server.
724
+ * This is useful for monorepos or projects with separate client/server directories.
725
+ *
726
+ * Uses the auditPaths configuration to determine which directories to scan.
727
+ * Each path is scanned independently and results are tagged with their environment label.
728
+ *
729
+ * @returns Combined summary of all audit results
730
+ */
731
+ async auditMultiplePaths() {
732
+ if (!this.auditPaths || this.auditPaths.length === 0) {
733
+ console.warn("[MonitorClient] No auditPaths configured");
734
+ return null;
735
+ }
736
+ const results = [];
737
+ const totalSummary = {
738
+ critical: 0,
739
+ high: 0,
740
+ moderate: 0,
741
+ low: 0,
742
+ info: 0
743
+ };
744
+ for (const auditPath of this.auditPaths) {
745
+ console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${auditPath.path}`);
746
+ try {
747
+ const summary = await this.auditDependencies({
748
+ projectPath: auditPath.path,
749
+ environment: auditPath.environment
750
+ });
751
+ if (summary) {
752
+ results.push({
753
+ environment: auditPath.environment,
754
+ scanId: summary.scanId,
755
+ processed: summary.processed,
756
+ resolved: summary.resolved,
757
+ summary: summary.summary
758
+ });
759
+ totalSummary.critical += summary.summary.critical;
760
+ totalSummary.high += summary.summary.high;
761
+ totalSummary.moderate += summary.summary.moderate;
762
+ totalSummary.low += summary.summary.low;
763
+ totalSummary.info += summary.summary.info;
764
+ }
765
+ } catch (err) {
766
+ console.error(`[MonitorClient] Failed to audit ${auditPath.environment}:`, err instanceof Error ? err.message : String(err));
767
+ }
768
+ }
769
+ if (results.length === 0) {
770
+ console.warn("[MonitorClient] No audit results collected from any path");
771
+ return null;
772
+ }
773
+ console.log(`[MonitorClient] Multi-path audit complete: ${results.length} paths scanned`);
774
+ return {
775
+ results,
776
+ totalSummary
777
+ };
778
+ }
621
779
  /**
622
780
  * Parse npm audit JSON output into vulnerability items
623
781
  */
@@ -627,6 +785,9 @@ var MonitorClient = class {
627
785
  return vulnerabilities;
628
786
  }
629
787
  for (const [packageName, vuln] of Object.entries(auditData.vulnerabilities)) {
788
+ if (!vuln.via || !Array.isArray(vuln.via)) {
789
+ continue;
790
+ }
630
791
  const viaDetails = vuln.via.find(
631
792
  (v) => typeof v === "object" && "title" in v
632
793
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "Client SDK for Ceon Monitor - Error tracking, health monitoring, security events, and vulnerability scanning",
5
5
  "author": "Ceon",
6
6
  "license": "MIT",