@ceon-oy/monitor-sdk 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -49,6 +49,10 @@ interface MonitorClientConfig {
49
49
  auditTimeoutMs?: number;
50
50
  /** Timeout for npm registry requests in ms (default: 5000, max: 30000) */
51
51
  registryTimeoutMs?: number;
52
+ /** Allow HTTP endpoints in production (default: false, requires explicit opt-in) */
53
+ allowInsecureHttp?: boolean;
54
+ /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
+ npmRegistryUrl?: string;
52
56
  }
53
57
  interface TechnologyItem {
54
58
  name: string;
@@ -117,6 +121,13 @@ interface VulnerabilityItem {
117
121
  isFixable?: boolean;
118
122
  isDirect?: boolean;
119
123
  }
124
+ interface VulnerabilitySummary {
125
+ critical: number;
126
+ high: number;
127
+ moderate: number;
128
+ low: number;
129
+ info: number;
130
+ }
120
131
  interface AuditResult {
121
132
  environment: string;
122
133
  totalDeps: number;
@@ -127,13 +138,7 @@ interface AuditSummary {
127
138
  scanId: string;
128
139
  processed: number;
129
140
  resolved: number;
130
- summary: {
131
- critical: number;
132
- high: number;
133
- moderate: number;
134
- low: number;
135
- info: number;
136
- };
141
+ summary: VulnerabilitySummary;
137
142
  }
138
143
  interface MultiAuditSummary {
139
144
  results: Array<{
@@ -141,21 +146,9 @@ interface MultiAuditSummary {
141
146
  scanId: string;
142
147
  processed: number;
143
148
  resolved: number;
144
- summary: {
145
- critical: number;
146
- high: number;
147
- moderate: number;
148
- low: number;
149
- info: number;
150
- };
149
+ summary: VulnerabilitySummary;
151
150
  }>;
152
- totalSummary: {
153
- critical: number;
154
- high: number;
155
- moderate: number;
156
- low: number;
157
- info: number;
158
- };
151
+ totalSummary: VulnerabilitySummary;
159
152
  }
160
153
 
161
154
  declare class MonitorClient {
@@ -188,6 +181,7 @@ declare class MonitorClient {
188
181
  private versionCheckEnabled;
189
182
  private auditTimeoutMs;
190
183
  private registryTimeoutMs;
184
+ private npmRegistryUrl;
191
185
  constructor(config: MonitorClientConfig);
192
186
  /**
193
187
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -198,6 +192,18 @@ declare class MonitorClient {
198
192
  * Removes potential API keys, tokens, and limits response length
199
193
  */
200
194
  private sanitizeErrorResponse;
195
+ /**
196
+ * Security: Sanitize file paths in log messages to prevent exposing full filesystem structure
197
+ * Truncates long paths and shows only the last few segments
198
+ */
199
+ private sanitizePath;
200
+ /**
201
+ * Execute a promise with a timeout. Properly cleans up the timer and supports cancellation.
202
+ * @param promiseFactory Factory function that receives an AbortSignal and returns a promise
203
+ * @param timeoutMs Timeout in milliseconds
204
+ * @param message Error message if timeout occurs
205
+ */
206
+ private withTimeout;
201
207
  captureError(error: Error, context?: ErrorContext): Promise<void>;
202
208
  captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>;
203
209
  flush(): Promise<void>;
@@ -239,9 +245,12 @@ declare class MonitorClient {
239
245
  private startFlushTimer;
240
246
  private stopFlushTimer;
241
247
  syncDependencies(): Promise<void>;
248
+ private performDependencySync;
242
249
  /**
243
250
  * Enrich technologies with latest version information from npm registry.
244
251
  * Only runs if versionCheckEnabled is true.
252
+ * @param technologies List of technologies to enrich
253
+ * @param signal Optional AbortSignal to cancel the operation
245
254
  */
246
255
  private enrichWithLatestVersions;
247
256
  syncTechnologies(technologies: TechnologyItem[]): Promise<void>;
@@ -251,10 +260,15 @@ declare class MonitorClient {
251
260
  /**
252
261
  * Fetch the latest version of a package from npm registry.
253
262
  * Returns null if the package cannot be found or the request fails.
263
+ * Uses npmRegistryUrl config option if provided.
264
+ * @param packageName The package name to fetch
265
+ * @param signal Optional AbortSignal to cancel the request
254
266
  */
255
267
  private fetchLatestVersion;
256
268
  /**
257
269
  * Fetch latest versions for multiple packages in parallel with concurrency limit.
270
+ * @param packageNames List of package names to fetch versions for
271
+ * @param signal Optional AbortSignal to cancel the operation
258
272
  */
259
273
  private fetchLatestVersions;
260
274
  private sendTechnologies;
@@ -320,6 +334,15 @@ declare class MonitorClient {
320
334
  timeWindowMinutes?: number;
321
335
  threshold?: number;
322
336
  }): Promise<BruteForceDetectionResult>;
337
+ /**
338
+ * Handle exec errors from audit commands. Returns stdout if available, null if timed out, or rethrows.
339
+ */
340
+ private handleAuditExecError;
341
+ /**
342
+ * Run a command with a reliable timeout using spawn.
343
+ * More reliable than execSync timeout in containerized environments.
344
+ */
345
+ private runCommandWithTimeout;
323
346
  /**
324
347
  * Run npm audit and send results to the monitoring server.
325
348
  * This scans the project for known vulnerabilities in dependencies.
@@ -342,6 +365,7 @@ declare class MonitorClient {
342
365
  * @returns Combined summary of all audit results
343
366
  */
344
367
  auditMultiplePaths(): Promise<MultiAuditSummary | null>;
368
+ private performMultiPathAudit;
345
369
  /**
346
370
  * Parse npm audit JSON output into vulnerability items
347
371
  */
package/dist/index.d.ts CHANGED
@@ -49,6 +49,10 @@ interface MonitorClientConfig {
49
49
  auditTimeoutMs?: number;
50
50
  /** Timeout for npm registry requests in ms (default: 5000, max: 30000) */
51
51
  registryTimeoutMs?: number;
52
+ /** Allow HTTP endpoints in production (default: false, requires explicit opt-in) */
53
+ allowInsecureHttp?: boolean;
54
+ /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
+ npmRegistryUrl?: string;
52
56
  }
53
57
  interface TechnologyItem {
54
58
  name: string;
@@ -117,6 +121,13 @@ interface VulnerabilityItem {
117
121
  isFixable?: boolean;
118
122
  isDirect?: boolean;
119
123
  }
124
+ interface VulnerabilitySummary {
125
+ critical: number;
126
+ high: number;
127
+ moderate: number;
128
+ low: number;
129
+ info: number;
130
+ }
120
131
  interface AuditResult {
121
132
  environment: string;
122
133
  totalDeps: number;
@@ -127,13 +138,7 @@ interface AuditSummary {
127
138
  scanId: string;
128
139
  processed: number;
129
140
  resolved: number;
130
- summary: {
131
- critical: number;
132
- high: number;
133
- moderate: number;
134
- low: number;
135
- info: number;
136
- };
141
+ summary: VulnerabilitySummary;
137
142
  }
138
143
  interface MultiAuditSummary {
139
144
  results: Array<{
@@ -141,21 +146,9 @@ interface MultiAuditSummary {
141
146
  scanId: string;
142
147
  processed: number;
143
148
  resolved: number;
144
- summary: {
145
- critical: number;
146
- high: number;
147
- moderate: number;
148
- low: number;
149
- info: number;
150
- };
149
+ summary: VulnerabilitySummary;
151
150
  }>;
152
- totalSummary: {
153
- critical: number;
154
- high: number;
155
- moderate: number;
156
- low: number;
157
- info: number;
158
- };
151
+ totalSummary: VulnerabilitySummary;
159
152
  }
160
153
 
161
154
  declare class MonitorClient {
@@ -188,6 +181,7 @@ declare class MonitorClient {
188
181
  private versionCheckEnabled;
189
182
  private auditTimeoutMs;
190
183
  private registryTimeoutMs;
184
+ private npmRegistryUrl;
191
185
  constructor(config: MonitorClientConfig);
192
186
  /**
193
187
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -198,6 +192,18 @@ declare class MonitorClient {
198
192
  * Removes potential API keys, tokens, and limits response length
199
193
  */
200
194
  private sanitizeErrorResponse;
195
+ /**
196
+ * Security: Sanitize file paths in log messages to prevent exposing full filesystem structure
197
+ * Truncates long paths and shows only the last few segments
198
+ */
199
+ private sanitizePath;
200
+ /**
201
+ * Execute a promise with a timeout. Properly cleans up the timer and supports cancellation.
202
+ * @param promiseFactory Factory function that receives an AbortSignal and returns a promise
203
+ * @param timeoutMs Timeout in milliseconds
204
+ * @param message Error message if timeout occurs
205
+ */
206
+ private withTimeout;
201
207
  captureError(error: Error, context?: ErrorContext): Promise<void>;
202
208
  captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>;
203
209
  flush(): Promise<void>;
@@ -239,9 +245,12 @@ declare class MonitorClient {
239
245
  private startFlushTimer;
240
246
  private stopFlushTimer;
241
247
  syncDependencies(): Promise<void>;
248
+ private performDependencySync;
242
249
  /**
243
250
  * Enrich technologies with latest version information from npm registry.
244
251
  * Only runs if versionCheckEnabled is true.
252
+ * @param technologies List of technologies to enrich
253
+ * @param signal Optional AbortSignal to cancel the operation
245
254
  */
246
255
  private enrichWithLatestVersions;
247
256
  syncTechnologies(technologies: TechnologyItem[]): Promise<void>;
@@ -251,10 +260,15 @@ declare class MonitorClient {
251
260
  /**
252
261
  * Fetch the latest version of a package from npm registry.
253
262
  * Returns null if the package cannot be found or the request fails.
263
+ * Uses npmRegistryUrl config option if provided.
264
+ * @param packageName The package name to fetch
265
+ * @param signal Optional AbortSignal to cancel the request
254
266
  */
255
267
  private fetchLatestVersion;
256
268
  /**
257
269
  * Fetch latest versions for multiple packages in parallel with concurrency limit.
270
+ * @param packageNames List of package names to fetch versions for
271
+ * @param signal Optional AbortSignal to cancel the operation
258
272
  */
259
273
  private fetchLatestVersions;
260
274
  private sendTechnologies;
@@ -320,6 +334,15 @@ declare class MonitorClient {
320
334
  timeWindowMinutes?: number;
321
335
  threshold?: number;
322
336
  }): Promise<BruteForceDetectionResult>;
337
+ /**
338
+ * Handle exec errors from audit commands. Returns stdout if available, null if timed out, or rethrows.
339
+ */
340
+ private handleAuditExecError;
341
+ /**
342
+ * Run a command with a reliable timeout using spawn.
343
+ * More reliable than execSync timeout in containerized environments.
344
+ */
345
+ private runCommandWithTimeout;
323
346
  /**
324
347
  * Run npm audit and send results to the monitoring server.
325
348
  * This scans the project for known vulnerabilities in dependencies.
@@ -342,6 +365,7 @@ declare class MonitorClient {
342
365
  * @returns Combined summary of all audit results
343
366
  */
344
367
  auditMultiplePaths(): Promise<MultiAuditSummary | null>;
368
+ private performMultiPathAudit;
345
369
  /**
346
370
  * Parse npm audit JSON output into vulnerability items
347
371
  */
package/dist/index.js CHANGED
@@ -47,6 +47,8 @@ var CONFIG_LIMITS = {
47
47
  // 64KB
48
48
  MAX_RETRY_MAP_SIZE: 1e3,
49
49
  // Prevent unbounded growth
50
+ RETRY_MAP_CLEANUP_PERCENTAGE: 0.1,
51
+ // Remove 10% of oldest entries when map is full
50
52
  AUDIT_MAX_BUFFER: 10 * 1024 * 1024,
51
53
  // 10MB
52
54
  AUDIT_TIMEOUT_MS: 6e4,
@@ -57,8 +59,16 @@ var CONFIG_LIMITS = {
57
59
  // 5 minutes
58
60
  REGISTRY_TIMEOUT_MS: 5e3,
59
61
  // 5 seconds (default)
60
- MAX_REGISTRY_TIMEOUT_MS: 3e4
62
+ MAX_REGISTRY_TIMEOUT_MS: 3e4,
61
63
  // 30 seconds (max configurable)
64
+ TECH_VERSION_FETCH_TIMEOUT_MS: 3e4,
65
+ // 30 seconds max for all version fetches
66
+ SYNC_DEPENDENCIES_TIMEOUT_MS: 6e4,
67
+ // 60 seconds max for all dependency syncs
68
+ AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
69
+ // 3 minutes max for all audit paths
70
+ REGISTRY_CONCURRENCY_LIMIT: 5
71
+ // Limit parallel requests to avoid rate limiting
62
72
  };
63
73
  var MonitorClient = class {
64
74
  constructor(config) {
@@ -75,8 +85,8 @@ var MonitorClient = class {
75
85
  if (!config.apiKey || config.apiKey.trim().length === 0) {
76
86
  throw new Error("[MonitorClient] API key is required");
77
87
  }
78
- if (!/^cm_[a-zA-Z0-9_-]+$/.test(config.apiKey)) {
79
- throw new Error("[MonitorClient] Invalid API key format. Expected format: cm_xxx");
88
+ if (!/^cm_[a-zA-Z0-9_-]{16,256}$/.test(config.apiKey)) {
89
+ throw new Error("[MonitorClient] Invalid API key format. Expected format: cm_xxx (16-256 characters after prefix)");
80
90
  }
81
91
  if (!config.endpoint || config.endpoint.trim().length === 0) {
82
92
  throw new Error("[MonitorClient] Endpoint URL is required");
@@ -87,6 +97,9 @@ var MonitorClient = class {
87
97
  throw new Error("[MonitorClient] Endpoint must use HTTP or HTTPS protocol");
88
98
  }
89
99
  if (url.protocol === "http:" && config.environment !== "development" && config.environment !== "test") {
100
+ if (!config.allowInsecureHttp) {
101
+ throw new Error("[MonitorClient] HTTP endpoints require allowInsecureHttp: true in non-development environments");
102
+ }
90
103
  console.warn("[MonitorClient] Warning: Using HTTP in non-development environment is not recommended");
91
104
  }
92
105
  } catch (err) {
@@ -116,8 +129,8 @@ var MonitorClient = class {
116
129
  "@typescript-eslint/*"
117
130
  ];
118
131
  this.excludePatterns = config.excludePatterns || defaultExcludePatterns;
119
- this.compiledExcludePatterns = this.excludePatterns.map((pattern) => {
120
- const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
132
+ this.compiledExcludePatterns = this.excludePatterns.filter((pattern) => pattern.length <= 100).map((pattern) => {
133
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*+/g, "[^/]*");
121
134
  return new RegExp(`^${regexPattern}$`);
122
135
  });
123
136
  this.autoAudit = config.autoAudit || false;
@@ -126,6 +139,7 @@ var MonitorClient = class {
126
139
  this.versionCheckEnabled = config.versionCheckEnabled ?? true;
127
140
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
128
141
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
142
+ this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
129
143
  this.startFlushTimer();
130
144
  if (this.trackDependencies) {
131
145
  this.syncDependencies().catch((err) => {
@@ -166,8 +180,43 @@ var MonitorClient = class {
166
180
  sanitized = sanitized.replace(/cm_[a-zA-Z0-9_-]+/g, "[REDACTED]");
167
181
  sanitized = sanitized.replace(/Bearer\s+[a-zA-Z0-9_.-]+/gi, "Bearer [REDACTED]");
168
182
  sanitized = sanitized.replace(/authorization['":\s]+[a-zA-Z0-9_.-]+/gi, "authorization: [REDACTED]");
183
+ sanitized = sanitized.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[JWT_REDACTED]");
184
+ sanitized = sanitized.replace(/AKIA[A-Z0-9]{16}/g, "[AWS_KEY_REDACTED]");
185
+ sanitized = sanitized.replace(/(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g, "[AWS_SECRET_REDACTED]");
186
+ sanitized = sanitized.replace(/gh[pousr]_[A-Za-z0-9_]{36,}/g, "[GITHUB_TOKEN_REDACTED]");
169
187
  return sanitized;
170
188
  }
189
+ /**
190
+ * Security: Sanitize file paths in log messages to prevent exposing full filesystem structure
191
+ * Truncates long paths and shows only the last few segments
192
+ */
193
+ sanitizePath(filePath, maxSegments = 3) {
194
+ if (!filePath) return "";
195
+ const segments = filePath.split(/[/\\]/);
196
+ if (segments.length <= maxSegments) {
197
+ return filePath;
198
+ }
199
+ return ".../" + segments.slice(-maxSegments).join("/");
200
+ }
201
+ /**
202
+ * Execute a promise with a timeout. Properly cleans up the timer and supports cancellation.
203
+ * @param promiseFactory Factory function that receives an AbortSignal and returns a promise
204
+ * @param timeoutMs Timeout in milliseconds
205
+ * @param message Error message if timeout occurs
206
+ */
207
+ withTimeout(promiseFactory, timeoutMs, message) {
208
+ const controller = new AbortController();
209
+ let timeoutId;
210
+ const timeoutPromise = new Promise((_, reject) => {
211
+ timeoutId = setTimeout(() => {
212
+ controller.abort();
213
+ reject(new Error(message));
214
+ }, timeoutMs);
215
+ });
216
+ return Promise.race([promiseFactory(controller.signal), timeoutPromise]).finally(() => {
217
+ clearTimeout(timeoutId);
218
+ });
219
+ }
171
220
  async captureError(error, context) {
172
221
  if (this.isClosed) return;
173
222
  const payload = {
@@ -222,7 +271,7 @@ var MonitorClient = class {
222
271
  const retries = this.retryCount.get(key) || 0;
223
272
  if (retries < this.maxRetries) {
224
273
  if (this.retryCount.size >= CONFIG_LIMITS.MAX_RETRY_MAP_SIZE) {
225
- const keysToRemove = Array.from(this.retryCount.keys()).slice(0, Math.ceil(CONFIG_LIMITS.MAX_RETRY_MAP_SIZE * 0.1));
274
+ const keysToRemove = Array.from(this.retryCount.keys()).slice(0, Math.ceil(CONFIG_LIMITS.MAX_RETRY_MAP_SIZE * CONFIG_LIMITS.RETRY_MAP_CLEANUP_PERCENTAGE));
226
275
  for (const oldKey of keysToRemove) {
227
276
  this.retryCount.delete(oldKey);
228
277
  }
@@ -328,6 +377,8 @@ var MonitorClient = class {
328
377
  * Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
329
378
  */
330
379
  async runScanAndTrackTime() {
380
+ const startTime = Date.now();
381
+ console.log("[MonitorClient] Starting vulnerability scan...");
331
382
  try {
332
383
  if (this.auditPaths && this.auditPaths.length > 0) {
333
384
  await this.auditMultiplePaths();
@@ -335,6 +386,8 @@ var MonitorClient = class {
335
386
  await this.auditDependencies();
336
387
  }
337
388
  this.lastScanTime = /* @__PURE__ */ new Date();
389
+ const duration = Date.now() - startTime;
390
+ console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
338
391
  } catch (err) {
339
392
  console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
340
393
  }
@@ -441,38 +494,56 @@ var MonitorClient = class {
441
494
  }
442
495
  }
443
496
  async syncDependencies() {
497
+ console.log("[MonitorClient] Starting technology sync...");
444
498
  try {
445
- if (this.dependencySources && this.dependencySources.length > 0) {
446
- for (const source of this.dependencySources) {
447
- const technologies = await this.readPackageJsonFromPath(source.path);
448
- if (technologies.length === 0) continue;
449
- const enrichedTechnologies = await this.enrichWithLatestVersions(technologies);
450
- await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
451
- console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
499
+ await this.withTimeout(
500
+ (signal) => this.performDependencySync(signal),
501
+ CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
502
+ "Technology sync timed out"
503
+ );
504
+ console.log("[MonitorClient] Technology sync completed successfully");
505
+ } catch (err) {
506
+ console.error("[MonitorClient] Technology sync failed:", err instanceof Error ? err.message : String(err));
507
+ }
508
+ }
509
+ async performDependencySync(signal) {
510
+ if (this.dependencySources && this.dependencySources.length > 0) {
511
+ for (const source of this.dependencySources) {
512
+ if (signal?.aborted) {
513
+ console.log("[MonitorClient] Technology sync cancelled");
514
+ return;
452
515
  }
453
- } else {
454
- const technologies = await this.readPackageJson();
455
- if (technologies.length === 0) return;
456
- const enrichedTechnologies = await this.enrichWithLatestVersions(technologies);
457
- await this.sendTechnologies(enrichedTechnologies);
458
- console.log(`[MonitorClient] Technology sync completed for ${this.environment}`);
516
+ const technologies = await this.readPackageJsonFromPath(source.path);
517
+ if (technologies.length === 0) continue;
518
+ const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
519
+ await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
520
+ console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
459
521
  }
460
- } catch (err) {
461
- console.error("[MonitorClient] Failed to sync dependencies:", err);
522
+ } else {
523
+ const technologies = await this.readPackageJson();
524
+ if (technologies.length === 0) return;
525
+ const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
526
+ await this.sendTechnologies(enrichedTechnologies);
527
+ console.log(`[MonitorClient] Technology sync completed for ${this.environment}`);
462
528
  }
463
529
  }
464
530
  /**
465
531
  * Enrich technologies with latest version information from npm registry.
466
532
  * Only runs if versionCheckEnabled is true.
533
+ * @param technologies List of technologies to enrich
534
+ * @param signal Optional AbortSignal to cancel the operation
467
535
  */
468
- async enrichWithLatestVersions(technologies) {
536
+ async enrichWithLatestVersions(technologies, signal) {
469
537
  if (!this.versionCheckEnabled) {
470
538
  return technologies;
471
539
  }
540
+ if (signal?.aborted) {
541
+ return technologies;
542
+ }
472
543
  try {
473
544
  console.log(`[MonitorClient] Fetching latest versions for ${technologies.length} packages...`);
474
545
  const packageNames = technologies.map((t) => t.name);
475
- const latestVersions = await this.fetchLatestVersions(packageNames);
546
+ const latestVersions = await this.fetchLatestVersions(packageNames, signal);
476
547
  const foundCount = [...latestVersions.values()].filter((v) => v !== null).length;
477
548
  console.log(`[MonitorClient] Fetched latest versions: ${foundCount}/${technologies.length} packages`);
478
549
  return technologies.map((tech) => ({
@@ -504,7 +575,7 @@ var MonitorClient = class {
504
575
  const resolvedPath = path.isAbsolute(packagePath) ? packagePath : path.join(baseDir, packagePath);
505
576
  const normalizedPath = path.normalize(resolvedPath);
506
577
  if (packagePath.includes("\0")) {
507
- console.warn("[MonitorClient] Suspicious path blocked:", packagePath);
578
+ console.warn("[MonitorClient] Suspicious path blocked:", this.sanitizePath(packagePath));
508
579
  return [];
509
580
  }
510
581
  if (!normalizedPath.endsWith("package.json")) {
@@ -517,7 +588,7 @@ var MonitorClient = class {
517
588
  try {
518
589
  const realPath = fs.realpathSync(normalizedPath);
519
590
  if (!realPath.endsWith("package.json")) {
520
- console.warn("[MonitorClient] Symlink points to non-package.json file:", packagePath);
591
+ console.warn("[MonitorClient] Symlink points to non-package.json file:", this.sanitizePath(packagePath));
521
592
  return [];
522
593
  }
523
594
  } catch {
@@ -553,13 +624,17 @@ var MonitorClient = class {
553
624
  /**
554
625
  * Fetch the latest version of a package from npm registry.
555
626
  * Returns null if the package cannot be found or the request fails.
627
+ * Uses npmRegistryUrl config option if provided.
628
+ * @param packageName The package name to fetch
629
+ * @param signal Optional AbortSignal to cancel the request
556
630
  */
557
- async fetchLatestVersion(packageName) {
631
+ async fetchLatestVersion(packageName, signal) {
558
632
  try {
633
+ if (signal?.aborted) return null;
559
634
  const encodedName = encodeURIComponent(packageName).replace("%40", "@");
560
- const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
635
+ const response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
561
636
  headers: { "Accept": "application/json" },
562
- signal: AbortSignal.timeout(this.registryTimeoutMs)
637
+ signal: signal ?? AbortSignal.timeout(this.registryTimeoutMs)
563
638
  });
564
639
  if (!response.ok) return null;
565
640
  const data = await response.json();
@@ -570,16 +645,25 @@ var MonitorClient = class {
570
645
  }
571
646
  /**
572
647
  * Fetch latest versions for multiple packages in parallel with concurrency limit.
648
+ * @param packageNames List of package names to fetch versions for
649
+ * @param signal Optional AbortSignal to cancel the operation
573
650
  */
574
- async fetchLatestVersions(packageNames) {
651
+ async fetchLatestVersions(packageNames, signal) {
575
652
  const results = /* @__PURE__ */ new Map();
576
- const concurrencyLimit = 5;
653
+ const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
654
+ const totalBatches = Math.ceil(packageNames.length / concurrencyLimit);
577
655
  for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
656
+ if (signal?.aborted) {
657
+ console.log("[MonitorClient] Version fetch cancelled");
658
+ break;
659
+ }
660
+ const batchNumber = Math.floor(i / concurrencyLimit) + 1;
661
+ console.log(`[MonitorClient] Fetching versions batch ${batchNumber}/${totalBatches}...`);
578
662
  const batch = packageNames.slice(i, i + concurrencyLimit);
579
663
  const batchResults = await Promise.all(
580
664
  batch.map(async (name) => ({
581
665
  name,
582
- version: await this.fetchLatestVersion(name)
666
+ version: await this.fetchLatestVersion(name, signal)
583
667
  }))
584
668
  );
585
669
  for (const { name, version } of batchResults) {
@@ -713,6 +797,67 @@ var MonitorClient = class {
713
797
  const result = await response.json();
714
798
  return result.data;
715
799
  }
800
+ /**
801
+ * Handle exec errors from audit commands. Returns stdout if available, null if timed out, or rethrows.
802
+ */
803
+ handleAuditExecError(err, packageManager) {
804
+ const execError = err;
805
+ if (execError.killed) {
806
+ console.error(`[MonitorClient] ${packageManager} audit timed out`);
807
+ return null;
808
+ }
809
+ if (execError.stdout) {
810
+ return execError.stdout;
811
+ }
812
+ throw err;
813
+ }
814
+ /**
815
+ * Run a command with a reliable timeout using spawn.
816
+ * More reliable than execSync timeout in containerized environments.
817
+ */
818
+ async runCommandWithTimeout(command, args, options) {
819
+ const childProcess = await import("child_process");
820
+ const { spawn } = childProcess;
821
+ return new Promise((resolve) => {
822
+ const proc = spawn(command, args, {
823
+ cwd: options.cwd,
824
+ stdio: ["pipe", "pipe", "pipe"],
825
+ shell: false
826
+ });
827
+ let stdout = "";
828
+ let stderr = "";
829
+ let timedOut = false;
830
+ let killed = false;
831
+ const timeoutId = setTimeout(() => {
832
+ timedOut = true;
833
+ killed = true;
834
+ proc.kill("SIGTERM");
835
+ setTimeout(() => {
836
+ if (!proc.killed) {
837
+ proc.kill("SIGKILL");
838
+ }
839
+ }, 1e3);
840
+ }, options.timeout);
841
+ proc.stdout?.on("data", (data) => {
842
+ if (stdout.length < options.maxBuffer) {
843
+ stdout += data.toString();
844
+ }
845
+ });
846
+ proc.stderr?.on("data", (data) => {
847
+ if (stderr.length < options.maxBuffer) {
848
+ stderr += data.toString();
849
+ }
850
+ });
851
+ proc.on("close", () => {
852
+ clearTimeout(timeoutId);
853
+ resolve({ stdout, timedOut });
854
+ });
855
+ proc.on("error", () => {
856
+ clearTimeout(timeoutId);
857
+ resolve({ stdout, timedOut: killed });
858
+ });
859
+ });
860
+ }
716
861
  /**
717
862
  * Run npm audit and send results to the monitoring server.
718
863
  * This scans the project for known vulnerabilities in dependencies.
@@ -726,14 +871,11 @@ var MonitorClient = class {
726
871
  console.warn("[MonitorClient] auditDependencies only works in Node.js server environment");
727
872
  return null;
728
873
  }
729
- let execSync;
730
874
  let path;
731
875
  let fs;
732
876
  try {
733
- const childProcess = await import("child_process");
734
877
  const pathModule = await import("path");
735
878
  const fsModule = await import("fs");
736
- execSync = childProcess.execSync;
737
879
  path = pathModule;
738
880
  fs = fsModule;
739
881
  } catch {
@@ -744,7 +886,7 @@ var MonitorClient = class {
744
886
  const environment = options.environment || this.environment;
745
887
  try {
746
888
  let projectPath = options.projectPath || process.cwd();
747
- if (projectPath.includes("\0") || /[;&|`$(){}[\]<>]/.test(projectPath)) {
889
+ if (projectPath.includes("\0") || /[;&|`$(){}[\]<>\n\r]/.test(projectPath)) {
748
890
  console.error("[MonitorClient] Invalid projectPath: contains forbidden characters");
749
891
  return null;
750
892
  }
@@ -771,31 +913,21 @@ var MonitorClient = class {
771
913
  return null;
772
914
  }
773
915
  const packageManager = this.detectPackageManager(projectPath, fs, path);
774
- console.log(`[MonitorClient] Auditing ${projectPath} using ${packageManager}`);
916
+ console.log(`[MonitorClient] Auditing ${this.sanitizePath(projectPath)} using ${packageManager} (timeout: ${this.auditTimeoutMs}ms)`);
775
917
  let auditOutput;
776
918
  let vulnerabilities;
777
919
  let totalDeps = 0;
778
920
  if (packageManager === "yarn") {
779
- try {
780
- auditOutput = execSync("yarn audit --json", {
781
- cwd: projectPath,
782
- encoding: "utf-8",
783
- stdio: ["pipe", "pipe", "pipe"],
784
- maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
785
- timeout: this.auditTimeoutMs
786
- });
787
- } catch (err) {
788
- const execError = err;
789
- if (execError.killed) {
790
- console.error("[MonitorClient] yarn audit timed out");
791
- return null;
792
- }
793
- if (execError.stdout) {
794
- auditOutput = execError.stdout;
795
- } else {
796
- throw err;
797
- }
921
+ const result2 = await this.runCommandWithTimeout("yarn", ["audit", "--json"], {
922
+ cwd: projectPath,
923
+ timeout: this.auditTimeoutMs,
924
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
925
+ });
926
+ if (result2.timedOut) {
927
+ console.error(`[MonitorClient] yarn audit timed out after ${this.auditTimeoutMs}ms`);
928
+ return null;
798
929
  }
930
+ auditOutput = result2.stdout;
799
931
  vulnerabilities = this.parseYarnAuditOutput(auditOutput);
800
932
  const lines = auditOutput.trim().split("\n");
801
933
  for (const line of lines) {
@@ -810,50 +942,30 @@ var MonitorClient = class {
810
942
  }
811
943
  } else if (packageManager === "pnpm") {
812
944
  console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
813
- try {
814
- auditOutput = execSync("npm audit --json", {
815
- cwd: projectPath,
816
- encoding: "utf-8",
817
- stdio: ["pipe", "pipe", "pipe"],
818
- maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
819
- timeout: this.auditTimeoutMs
820
- });
821
- } catch (err) {
822
- const execError = err;
823
- if (execError.killed) {
824
- console.error("[MonitorClient] npm audit timed out");
825
- return null;
826
- }
827
- if (execError.stdout) {
828
- auditOutput = execError.stdout;
829
- } else {
830
- throw err;
831
- }
945
+ const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
946
+ cwd: projectPath,
947
+ timeout: this.auditTimeoutMs,
948
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
949
+ });
950
+ if (result2.timedOut) {
951
+ console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
952
+ return null;
832
953
  }
954
+ auditOutput = result2.stdout;
833
955
  const auditData = JSON.parse(auditOutput);
834
956
  vulnerabilities = this.parseNpmAuditOutput(auditData);
835
957
  totalDeps = auditData.metadata?.dependencies?.total || 0;
836
958
  } else {
837
- try {
838
- auditOutput = execSync("npm audit --json", {
839
- cwd: projectPath,
840
- encoding: "utf-8",
841
- stdio: ["pipe", "pipe", "pipe"],
842
- maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
843
- timeout: this.auditTimeoutMs
844
- });
845
- } catch (err) {
846
- const execError = err;
847
- if (execError.killed) {
848
- console.error("[MonitorClient] npm audit timed out");
849
- return null;
850
- }
851
- if (execError.stdout) {
852
- auditOutput = execError.stdout;
853
- } else {
854
- throw err;
855
- }
959
+ const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
960
+ cwd: projectPath,
961
+ timeout: this.auditTimeoutMs,
962
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
963
+ });
964
+ if (result2.timedOut) {
965
+ console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
966
+ return null;
856
967
  }
968
+ auditOutput = result2.stdout;
857
969
  const auditData = JSON.parse(auditOutput);
858
970
  vulnerabilities = this.parseNpmAuditOutput(auditData);
859
971
  totalDeps = auditData.metadata?.dependencies?.total || 0;
@@ -897,6 +1009,24 @@ var MonitorClient = class {
897
1009
  console.warn("[MonitorClient] No auditPaths configured");
898
1010
  return null;
899
1011
  }
1012
+ console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
1013
+ try {
1014
+ const result = await this.withTimeout(
1015
+ () => this.performMultiPathAudit(),
1016
+ CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
1017
+ "Multi-path audit timed out"
1018
+ );
1019
+ if (result) {
1020
+ console.log(`[MonitorClient] Multi-path audit complete: ${result.results.length} paths scanned`);
1021
+ }
1022
+ return result;
1023
+ } catch (err) {
1024
+ console.error("[MonitorClient] Multi-path audit failed:", err instanceof Error ? err.message : String(err));
1025
+ return null;
1026
+ }
1027
+ }
1028
+ async performMultiPathAudit() {
1029
+ if (!this.auditPaths) return null;
900
1030
  const results = [];
901
1031
  const totalSummary = {
902
1032
  critical: 0,
@@ -906,7 +1036,7 @@ var MonitorClient = class {
906
1036
  info: 0
907
1037
  };
908
1038
  for (const auditPath of this.auditPaths) {
909
- console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${auditPath.path}`);
1039
+ console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${this.sanitizePath(auditPath.path)}`);
910
1040
  try {
911
1041
  const summary = await this.auditDependencies({
912
1042
  projectPath: auditPath.path,
@@ -934,7 +1064,6 @@ var MonitorClient = class {
934
1064
  console.warn("[MonitorClient] No audit results collected from any path");
935
1065
  return null;
936
1066
  }
937
- console.log(`[MonitorClient] Multi-path audit complete: ${results.length} paths scanned`);
938
1067
  return {
939
1068
  results,
940
1069
  totalSummary
package/dist/index.mjs CHANGED
@@ -11,6 +11,8 @@ var CONFIG_LIMITS = {
11
11
  // 64KB
12
12
  MAX_RETRY_MAP_SIZE: 1e3,
13
13
  // Prevent unbounded growth
14
+ RETRY_MAP_CLEANUP_PERCENTAGE: 0.1,
15
+ // Remove 10% of oldest entries when map is full
14
16
  AUDIT_MAX_BUFFER: 10 * 1024 * 1024,
15
17
  // 10MB
16
18
  AUDIT_TIMEOUT_MS: 6e4,
@@ -21,8 +23,16 @@ var CONFIG_LIMITS = {
21
23
  // 5 minutes
22
24
  REGISTRY_TIMEOUT_MS: 5e3,
23
25
  // 5 seconds (default)
24
- MAX_REGISTRY_TIMEOUT_MS: 3e4
26
+ MAX_REGISTRY_TIMEOUT_MS: 3e4,
25
27
  // 30 seconds (max configurable)
28
+ TECH_VERSION_FETCH_TIMEOUT_MS: 3e4,
29
+ // 30 seconds max for all version fetches
30
+ SYNC_DEPENDENCIES_TIMEOUT_MS: 6e4,
31
+ // 60 seconds max for all dependency syncs
32
+ AUDIT_MULTI_PATH_TIMEOUT_MS: 18e4,
33
+ // 3 minutes max for all audit paths
34
+ REGISTRY_CONCURRENCY_LIMIT: 5
35
+ // Limit parallel requests to avoid rate limiting
26
36
  };
27
37
  var MonitorClient = class {
28
38
  constructor(config) {
@@ -39,8 +49,8 @@ var MonitorClient = class {
39
49
  if (!config.apiKey || config.apiKey.trim().length === 0) {
40
50
  throw new Error("[MonitorClient] API key is required");
41
51
  }
42
- if (!/^cm_[a-zA-Z0-9_-]+$/.test(config.apiKey)) {
43
- throw new Error("[MonitorClient] Invalid API key format. Expected format: cm_xxx");
52
+ if (!/^cm_[a-zA-Z0-9_-]{16,256}$/.test(config.apiKey)) {
53
+ throw new Error("[MonitorClient] Invalid API key format. Expected format: cm_xxx (16-256 characters after prefix)");
44
54
  }
45
55
  if (!config.endpoint || config.endpoint.trim().length === 0) {
46
56
  throw new Error("[MonitorClient] Endpoint URL is required");
@@ -51,6 +61,9 @@ var MonitorClient = class {
51
61
  throw new Error("[MonitorClient] Endpoint must use HTTP or HTTPS protocol");
52
62
  }
53
63
  if (url.protocol === "http:" && config.environment !== "development" && config.environment !== "test") {
64
+ if (!config.allowInsecureHttp) {
65
+ throw new Error("[MonitorClient] HTTP endpoints require allowInsecureHttp: true in non-development environments");
66
+ }
54
67
  console.warn("[MonitorClient] Warning: Using HTTP in non-development environment is not recommended");
55
68
  }
56
69
  } catch (err) {
@@ -80,8 +93,8 @@ var MonitorClient = class {
80
93
  "@typescript-eslint/*"
81
94
  ];
82
95
  this.excludePatterns = config.excludePatterns || defaultExcludePatterns;
83
- this.compiledExcludePatterns = this.excludePatterns.map((pattern) => {
84
- const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
96
+ this.compiledExcludePatterns = this.excludePatterns.filter((pattern) => pattern.length <= 100).map((pattern) => {
97
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*+/g, "[^/]*");
85
98
  return new RegExp(`^${regexPattern}$`);
86
99
  });
87
100
  this.autoAudit = config.autoAudit || false;
@@ -90,6 +103,7 @@ var MonitorClient = class {
90
103
  this.versionCheckEnabled = config.versionCheckEnabled ?? true;
91
104
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
92
105
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
106
+ this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
93
107
  this.startFlushTimer();
94
108
  if (this.trackDependencies) {
95
109
  this.syncDependencies().catch((err) => {
@@ -130,8 +144,43 @@ var MonitorClient = class {
130
144
  sanitized = sanitized.replace(/cm_[a-zA-Z0-9_-]+/g, "[REDACTED]");
131
145
  sanitized = sanitized.replace(/Bearer\s+[a-zA-Z0-9_.-]+/gi, "Bearer [REDACTED]");
132
146
  sanitized = sanitized.replace(/authorization['":\s]+[a-zA-Z0-9_.-]+/gi, "authorization: [REDACTED]");
147
+ sanitized = sanitized.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[JWT_REDACTED]");
148
+ sanitized = sanitized.replace(/AKIA[A-Z0-9]{16}/g, "[AWS_KEY_REDACTED]");
149
+ sanitized = sanitized.replace(/(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g, "[AWS_SECRET_REDACTED]");
150
+ sanitized = sanitized.replace(/gh[pousr]_[A-Za-z0-9_]{36,}/g, "[GITHUB_TOKEN_REDACTED]");
133
151
  return sanitized;
134
152
  }
153
+ /**
154
+ * Security: Sanitize file paths in log messages to prevent exposing full filesystem structure
155
+ * Truncates long paths and shows only the last few segments
156
+ */
157
+ sanitizePath(filePath, maxSegments = 3) {
158
+ if (!filePath) return "";
159
+ const segments = filePath.split(/[/\\]/);
160
+ if (segments.length <= maxSegments) {
161
+ return filePath;
162
+ }
163
+ return ".../" + segments.slice(-maxSegments).join("/");
164
+ }
165
+ /**
166
+ * Execute a promise with a timeout. Properly cleans up the timer and supports cancellation.
167
+ * @param promiseFactory Factory function that receives an AbortSignal and returns a promise
168
+ * @param timeoutMs Timeout in milliseconds
169
+ * @param message Error message if timeout occurs
170
+ */
171
+ withTimeout(promiseFactory, timeoutMs, message) {
172
+ const controller = new AbortController();
173
+ let timeoutId;
174
+ const timeoutPromise = new Promise((_, reject) => {
175
+ timeoutId = setTimeout(() => {
176
+ controller.abort();
177
+ reject(new Error(message));
178
+ }, timeoutMs);
179
+ });
180
+ return Promise.race([promiseFactory(controller.signal), timeoutPromise]).finally(() => {
181
+ clearTimeout(timeoutId);
182
+ });
183
+ }
135
184
  async captureError(error, context) {
136
185
  if (this.isClosed) return;
137
186
  const payload = {
@@ -186,7 +235,7 @@ var MonitorClient = class {
186
235
  const retries = this.retryCount.get(key) || 0;
187
236
  if (retries < this.maxRetries) {
188
237
  if (this.retryCount.size >= CONFIG_LIMITS.MAX_RETRY_MAP_SIZE) {
189
- const keysToRemove = Array.from(this.retryCount.keys()).slice(0, Math.ceil(CONFIG_LIMITS.MAX_RETRY_MAP_SIZE * 0.1));
238
+ const keysToRemove = Array.from(this.retryCount.keys()).slice(0, Math.ceil(CONFIG_LIMITS.MAX_RETRY_MAP_SIZE * CONFIG_LIMITS.RETRY_MAP_CLEANUP_PERCENTAGE));
190
239
  for (const oldKey of keysToRemove) {
191
240
  this.retryCount.delete(oldKey);
192
241
  }
@@ -292,6 +341,8 @@ var MonitorClient = class {
292
341
  * Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
293
342
  */
294
343
  async runScanAndTrackTime() {
344
+ const startTime = Date.now();
345
+ console.log("[MonitorClient] Starting vulnerability scan...");
295
346
  try {
296
347
  if (this.auditPaths && this.auditPaths.length > 0) {
297
348
  await this.auditMultiplePaths();
@@ -299,6 +350,8 @@ var MonitorClient = class {
299
350
  await this.auditDependencies();
300
351
  }
301
352
  this.lastScanTime = /* @__PURE__ */ new Date();
353
+ const duration = Date.now() - startTime;
354
+ console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
302
355
  } catch (err) {
303
356
  console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
304
357
  }
@@ -405,38 +458,56 @@ var MonitorClient = class {
405
458
  }
406
459
  }
407
460
  async syncDependencies() {
461
+ console.log("[MonitorClient] Starting technology sync...");
408
462
  try {
409
- if (this.dependencySources && this.dependencySources.length > 0) {
410
- for (const source of this.dependencySources) {
411
- const technologies = await this.readPackageJsonFromPath(source.path);
412
- if (technologies.length === 0) continue;
413
- const enrichedTechnologies = await this.enrichWithLatestVersions(technologies);
414
- await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
415
- console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
463
+ await this.withTimeout(
464
+ (signal) => this.performDependencySync(signal),
465
+ CONFIG_LIMITS.SYNC_DEPENDENCIES_TIMEOUT_MS,
466
+ "Technology sync timed out"
467
+ );
468
+ console.log("[MonitorClient] Technology sync completed successfully");
469
+ } catch (err) {
470
+ console.error("[MonitorClient] Technology sync failed:", err instanceof Error ? err.message : String(err));
471
+ }
472
+ }
473
+ async performDependencySync(signal) {
474
+ if (this.dependencySources && this.dependencySources.length > 0) {
475
+ for (const source of this.dependencySources) {
476
+ if (signal?.aborted) {
477
+ console.log("[MonitorClient] Technology sync cancelled");
478
+ return;
416
479
  }
417
- } else {
418
- const technologies = await this.readPackageJson();
419
- if (technologies.length === 0) return;
420
- const enrichedTechnologies = await this.enrichWithLatestVersions(technologies);
421
- await this.sendTechnologies(enrichedTechnologies);
422
- console.log(`[MonitorClient] Technology sync completed for ${this.environment}`);
480
+ const technologies = await this.readPackageJsonFromPath(source.path);
481
+ if (technologies.length === 0) continue;
482
+ const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
483
+ await this.sendTechnologiesWithEnvironment(enrichedTechnologies, source.environment);
484
+ console.log(`[MonitorClient] Technology sync completed for ${source.environment}`);
423
485
  }
424
- } catch (err) {
425
- console.error("[MonitorClient] Failed to sync dependencies:", err);
486
+ } else {
487
+ const technologies = await this.readPackageJson();
488
+ if (technologies.length === 0) return;
489
+ const enrichedTechnologies = await this.enrichWithLatestVersions(technologies, signal);
490
+ await this.sendTechnologies(enrichedTechnologies);
491
+ console.log(`[MonitorClient] Technology sync completed for ${this.environment}`);
426
492
  }
427
493
  }
428
494
  /**
429
495
  * Enrich technologies with latest version information from npm registry.
430
496
  * Only runs if versionCheckEnabled is true.
497
+ * @param technologies List of technologies to enrich
498
+ * @param signal Optional AbortSignal to cancel the operation
431
499
  */
432
- async enrichWithLatestVersions(technologies) {
500
+ async enrichWithLatestVersions(technologies, signal) {
433
501
  if (!this.versionCheckEnabled) {
434
502
  return technologies;
435
503
  }
504
+ if (signal?.aborted) {
505
+ return technologies;
506
+ }
436
507
  try {
437
508
  console.log(`[MonitorClient] Fetching latest versions for ${technologies.length} packages...`);
438
509
  const packageNames = technologies.map((t) => t.name);
439
- const latestVersions = await this.fetchLatestVersions(packageNames);
510
+ const latestVersions = await this.fetchLatestVersions(packageNames, signal);
440
511
  const foundCount = [...latestVersions.values()].filter((v) => v !== null).length;
441
512
  console.log(`[MonitorClient] Fetched latest versions: ${foundCount}/${technologies.length} packages`);
442
513
  return technologies.map((tech) => ({
@@ -468,7 +539,7 @@ var MonitorClient = class {
468
539
  const resolvedPath = path.isAbsolute(packagePath) ? packagePath : path.join(baseDir, packagePath);
469
540
  const normalizedPath = path.normalize(resolvedPath);
470
541
  if (packagePath.includes("\0")) {
471
- console.warn("[MonitorClient] Suspicious path blocked:", packagePath);
542
+ console.warn("[MonitorClient] Suspicious path blocked:", this.sanitizePath(packagePath));
472
543
  return [];
473
544
  }
474
545
  if (!normalizedPath.endsWith("package.json")) {
@@ -481,7 +552,7 @@ var MonitorClient = class {
481
552
  try {
482
553
  const realPath = fs.realpathSync(normalizedPath);
483
554
  if (!realPath.endsWith("package.json")) {
484
- console.warn("[MonitorClient] Symlink points to non-package.json file:", packagePath);
555
+ console.warn("[MonitorClient] Symlink points to non-package.json file:", this.sanitizePath(packagePath));
485
556
  return [];
486
557
  }
487
558
  } catch {
@@ -517,13 +588,17 @@ var MonitorClient = class {
517
588
  /**
518
589
  * Fetch the latest version of a package from npm registry.
519
590
  * Returns null if the package cannot be found or the request fails.
591
+ * Uses npmRegistryUrl config option if provided.
592
+ * @param packageName The package name to fetch
593
+ * @param signal Optional AbortSignal to cancel the request
520
594
  */
521
- async fetchLatestVersion(packageName) {
595
+ async fetchLatestVersion(packageName, signal) {
522
596
  try {
597
+ if (signal?.aborted) return null;
523
598
  const encodedName = encodeURIComponent(packageName).replace("%40", "@");
524
- const response = await fetch(`https://registry.npmjs.org/${encodedName}`, {
599
+ const response = await fetch(`${this.npmRegistryUrl}/${encodedName}`, {
525
600
  headers: { "Accept": "application/json" },
526
- signal: AbortSignal.timeout(this.registryTimeoutMs)
601
+ signal: signal ?? AbortSignal.timeout(this.registryTimeoutMs)
527
602
  });
528
603
  if (!response.ok) return null;
529
604
  const data = await response.json();
@@ -534,16 +609,25 @@ var MonitorClient = class {
534
609
  }
535
610
  /**
536
611
  * Fetch latest versions for multiple packages in parallel with concurrency limit.
612
+ * @param packageNames List of package names to fetch versions for
613
+ * @param signal Optional AbortSignal to cancel the operation
537
614
  */
538
- async fetchLatestVersions(packageNames) {
615
+ async fetchLatestVersions(packageNames, signal) {
539
616
  const results = /* @__PURE__ */ new Map();
540
- const concurrencyLimit = 5;
617
+ const concurrencyLimit = CONFIG_LIMITS.REGISTRY_CONCURRENCY_LIMIT;
618
+ const totalBatches = Math.ceil(packageNames.length / concurrencyLimit);
541
619
  for (let i = 0; i < packageNames.length; i += concurrencyLimit) {
620
+ if (signal?.aborted) {
621
+ console.log("[MonitorClient] Version fetch cancelled");
622
+ break;
623
+ }
624
+ const batchNumber = Math.floor(i / concurrencyLimit) + 1;
625
+ console.log(`[MonitorClient] Fetching versions batch ${batchNumber}/${totalBatches}...`);
542
626
  const batch = packageNames.slice(i, i + concurrencyLimit);
543
627
  const batchResults = await Promise.all(
544
628
  batch.map(async (name) => ({
545
629
  name,
546
- version: await this.fetchLatestVersion(name)
630
+ version: await this.fetchLatestVersion(name, signal)
547
631
  }))
548
632
  );
549
633
  for (const { name, version } of batchResults) {
@@ -677,6 +761,67 @@ var MonitorClient = class {
677
761
  const result = await response.json();
678
762
  return result.data;
679
763
  }
764
+ /**
765
+ * Handle exec errors from audit commands. Returns stdout if available, null if timed out, or rethrows.
766
+ */
767
+ handleAuditExecError(err, packageManager) {
768
+ const execError = err;
769
+ if (execError.killed) {
770
+ console.error(`[MonitorClient] ${packageManager} audit timed out`);
771
+ return null;
772
+ }
773
+ if (execError.stdout) {
774
+ return execError.stdout;
775
+ }
776
+ throw err;
777
+ }
778
+ /**
779
+ * Run a command with a reliable timeout using spawn.
780
+ * More reliable than execSync timeout in containerized environments.
781
+ */
782
+ async runCommandWithTimeout(command, args, options) {
783
+ const childProcess = await import("child_process");
784
+ const { spawn } = childProcess;
785
+ return new Promise((resolve) => {
786
+ const proc = spawn(command, args, {
787
+ cwd: options.cwd,
788
+ stdio: ["pipe", "pipe", "pipe"],
789
+ shell: false
790
+ });
791
+ let stdout = "";
792
+ let stderr = "";
793
+ let timedOut = false;
794
+ let killed = false;
795
+ const timeoutId = setTimeout(() => {
796
+ timedOut = true;
797
+ killed = true;
798
+ proc.kill("SIGTERM");
799
+ setTimeout(() => {
800
+ if (!proc.killed) {
801
+ proc.kill("SIGKILL");
802
+ }
803
+ }, 1e3);
804
+ }, options.timeout);
805
+ proc.stdout?.on("data", (data) => {
806
+ if (stdout.length < options.maxBuffer) {
807
+ stdout += data.toString();
808
+ }
809
+ });
810
+ proc.stderr?.on("data", (data) => {
811
+ if (stderr.length < options.maxBuffer) {
812
+ stderr += data.toString();
813
+ }
814
+ });
815
+ proc.on("close", () => {
816
+ clearTimeout(timeoutId);
817
+ resolve({ stdout, timedOut });
818
+ });
819
+ proc.on("error", () => {
820
+ clearTimeout(timeoutId);
821
+ resolve({ stdout, timedOut: killed });
822
+ });
823
+ });
824
+ }
680
825
  /**
681
826
  * Run npm audit and send results to the monitoring server.
682
827
  * This scans the project for known vulnerabilities in dependencies.
@@ -690,14 +835,11 @@ var MonitorClient = class {
690
835
  console.warn("[MonitorClient] auditDependencies only works in Node.js server environment");
691
836
  return null;
692
837
  }
693
- let execSync;
694
838
  let path;
695
839
  let fs;
696
840
  try {
697
- const childProcess = await import("child_process");
698
841
  const pathModule = await import("path");
699
842
  const fsModule = await import("fs");
700
- execSync = childProcess.execSync;
701
843
  path = pathModule;
702
844
  fs = fsModule;
703
845
  } catch {
@@ -708,7 +850,7 @@ var MonitorClient = class {
708
850
  const environment = options.environment || this.environment;
709
851
  try {
710
852
  let projectPath = options.projectPath || process.cwd();
711
- if (projectPath.includes("\0") || /[;&|`$(){}[\]<>]/.test(projectPath)) {
853
+ if (projectPath.includes("\0") || /[;&|`$(){}[\]<>\n\r]/.test(projectPath)) {
712
854
  console.error("[MonitorClient] Invalid projectPath: contains forbidden characters");
713
855
  return null;
714
856
  }
@@ -735,31 +877,21 @@ var MonitorClient = class {
735
877
  return null;
736
878
  }
737
879
  const packageManager = this.detectPackageManager(projectPath, fs, path);
738
- console.log(`[MonitorClient] Auditing ${projectPath} using ${packageManager}`);
880
+ console.log(`[MonitorClient] Auditing ${this.sanitizePath(projectPath)} using ${packageManager} (timeout: ${this.auditTimeoutMs}ms)`);
739
881
  let auditOutput;
740
882
  let vulnerabilities;
741
883
  let totalDeps = 0;
742
884
  if (packageManager === "yarn") {
743
- try {
744
- auditOutput = execSync("yarn audit --json", {
745
- cwd: projectPath,
746
- encoding: "utf-8",
747
- stdio: ["pipe", "pipe", "pipe"],
748
- maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
749
- timeout: this.auditTimeoutMs
750
- });
751
- } catch (err) {
752
- const execError = err;
753
- if (execError.killed) {
754
- console.error("[MonitorClient] yarn audit timed out");
755
- return null;
756
- }
757
- if (execError.stdout) {
758
- auditOutput = execError.stdout;
759
- } else {
760
- throw err;
761
- }
885
+ const result2 = await this.runCommandWithTimeout("yarn", ["audit", "--json"], {
886
+ cwd: projectPath,
887
+ timeout: this.auditTimeoutMs,
888
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
889
+ });
890
+ if (result2.timedOut) {
891
+ console.error(`[MonitorClient] yarn audit timed out after ${this.auditTimeoutMs}ms`);
892
+ return null;
762
893
  }
894
+ auditOutput = result2.stdout;
763
895
  vulnerabilities = this.parseYarnAuditOutput(auditOutput);
764
896
  const lines = auditOutput.trim().split("\n");
765
897
  for (const line of lines) {
@@ -774,50 +906,30 @@ var MonitorClient = class {
774
906
  }
775
907
  } else if (packageManager === "pnpm") {
776
908
  console.log("[MonitorClient] pnpm detected, using npm audit (pnpm compatible)");
777
- try {
778
- auditOutput = execSync("npm audit --json", {
779
- cwd: projectPath,
780
- encoding: "utf-8",
781
- stdio: ["pipe", "pipe", "pipe"],
782
- maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
783
- timeout: this.auditTimeoutMs
784
- });
785
- } catch (err) {
786
- const execError = err;
787
- if (execError.killed) {
788
- console.error("[MonitorClient] npm audit timed out");
789
- return null;
790
- }
791
- if (execError.stdout) {
792
- auditOutput = execError.stdout;
793
- } else {
794
- throw err;
795
- }
909
+ const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
910
+ cwd: projectPath,
911
+ timeout: this.auditTimeoutMs,
912
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
913
+ });
914
+ if (result2.timedOut) {
915
+ console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
916
+ return null;
796
917
  }
918
+ auditOutput = result2.stdout;
797
919
  const auditData = JSON.parse(auditOutput);
798
920
  vulnerabilities = this.parseNpmAuditOutput(auditData);
799
921
  totalDeps = auditData.metadata?.dependencies?.total || 0;
800
922
  } else {
801
- try {
802
- auditOutput = execSync("npm audit --json", {
803
- cwd: projectPath,
804
- encoding: "utf-8",
805
- stdio: ["pipe", "pipe", "pipe"],
806
- maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
807
- timeout: this.auditTimeoutMs
808
- });
809
- } catch (err) {
810
- const execError = err;
811
- if (execError.killed) {
812
- console.error("[MonitorClient] npm audit timed out");
813
- return null;
814
- }
815
- if (execError.stdout) {
816
- auditOutput = execError.stdout;
817
- } else {
818
- throw err;
819
- }
923
+ const result2 = await this.runCommandWithTimeout("npm", ["audit", "--json"], {
924
+ cwd: projectPath,
925
+ timeout: this.auditTimeoutMs,
926
+ maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER
927
+ });
928
+ if (result2.timedOut) {
929
+ console.error(`[MonitorClient] npm audit timed out after ${this.auditTimeoutMs}ms`);
930
+ return null;
820
931
  }
932
+ auditOutput = result2.stdout;
821
933
  const auditData = JSON.parse(auditOutput);
822
934
  vulnerabilities = this.parseNpmAuditOutput(auditData);
823
935
  totalDeps = auditData.metadata?.dependencies?.total || 0;
@@ -861,6 +973,24 @@ var MonitorClient = class {
861
973
  console.warn("[MonitorClient] No auditPaths configured");
862
974
  return null;
863
975
  }
976
+ console.log(`[MonitorClient] Starting multi-path audit (${this.auditPaths.length} paths)...`);
977
+ try {
978
+ const result = await this.withTimeout(
979
+ () => this.performMultiPathAudit(),
980
+ CONFIG_LIMITS.AUDIT_MULTI_PATH_TIMEOUT_MS,
981
+ "Multi-path audit timed out"
982
+ );
983
+ if (result) {
984
+ console.log(`[MonitorClient] Multi-path audit complete: ${result.results.length} paths scanned`);
985
+ }
986
+ return result;
987
+ } catch (err) {
988
+ console.error("[MonitorClient] Multi-path audit failed:", err instanceof Error ? err.message : String(err));
989
+ return null;
990
+ }
991
+ }
992
+ async performMultiPathAudit() {
993
+ if (!this.auditPaths) return null;
864
994
  const results = [];
865
995
  const totalSummary = {
866
996
  critical: 0,
@@ -870,7 +1000,7 @@ var MonitorClient = class {
870
1000
  info: 0
871
1001
  };
872
1002
  for (const auditPath of this.auditPaths) {
873
- console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${auditPath.path}`);
1003
+ console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${this.sanitizePath(auditPath.path)}`);
874
1004
  try {
875
1005
  const summary = await this.auditDependencies({
876
1006
  projectPath: auditPath.path,
@@ -898,7 +1028,6 @@ var MonitorClient = class {
898
1028
  console.warn("[MonitorClient] No audit results collected from any path");
899
1029
  return null;
900
1030
  }
901
- console.log(`[MonitorClient] Multi-path audit complete: ${results.length} paths scanned`);
902
1031
  return {
903
1032
  results,
904
1033
  totalSummary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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",