@ceon-oy/monitor-sdk 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -53,7 +53,7 @@ interface MonitorClientConfig {
53
53
  allowInsecureHttp?: boolean;
54
54
  /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
55
  npmRegistryUrl?: string;
56
- /** Enable SDK-based health check polling (default: false) */
56
+ /** Enable SDK-based health check polling (default: true) */
57
57
  healthCheckEnabled?: boolean;
58
58
  /** Interval to fetch health endpoints from server in ms (default: 60000) */
59
59
  healthCheckFetchIntervalMs?: number;
@@ -226,6 +226,11 @@ declare class MonitorClient {
226
226
  private healthCheckTimers;
227
227
  private healthCheckResultsQueue;
228
228
  private healthCheckFlushTimer;
229
+ private sdkErrorQueue;
230
+ private sdkErrorFlushTimer;
231
+ private sdkErrorsInCurrentWindow;
232
+ private sdkErrorWindowResetTimer;
233
+ private metricsCollectionTimer;
229
234
  constructor(config: MonitorClientConfig);
230
235
  /**
231
236
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -253,6 +258,15 @@ declare class MonitorClient {
253
258
  flush(): Promise<void>;
254
259
  private getErrorKey;
255
260
  close(): Promise<void>;
261
+ /**
262
+ * Queue an SDK error to be reported to the server's system errors page.
263
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
264
+ * Throttled to max 20 errors per minute to prevent flooding.
265
+ */
266
+ private reportError;
267
+ private startSdkErrorFlushTimer;
268
+ private stopSdkErrorFlushTimer;
269
+ private flushSdkErrors;
256
270
  private stopAuditIntervalTimer;
257
271
  /**
258
272
  * Fetch project settings from the monitoring server.
@@ -263,6 +277,12 @@ declare class MonitorClient {
263
277
  vulnerabilityScanIntervalHours: number;
264
278
  scanRequestedAt: string | null;
265
279
  techScanRequestedAt: string | null;
280
+ metricsEnabled?: boolean;
281
+ metricsIntervalSeconds?: number;
282
+ metricsDiskPaths?: string[];
283
+ metricsCpuThreshold?: number;
284
+ metricsRamThreshold?: number;
285
+ metricsDiskThreshold?: number;
266
286
  } | null>;
267
287
  /**
268
288
  * Setup automatic vulnerability scanning based on server-configured interval.
@@ -465,6 +485,46 @@ declare class MonitorClient {
465
485
  * Stop all health check timers
466
486
  */
467
487
  private stopHealthCheckTimers;
488
+ /**
489
+ * Start system metrics collection based on server-configured settings.
490
+ * Only runs if metricsEnabled is true in project settings.
491
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
492
+ */
493
+ setupSystemMetricsCollection(): Promise<void>;
494
+ private stopMetricsCollection;
495
+ /**
496
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
497
+ * and submit to the monitoring server.
498
+ *
499
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
500
+ * Zero dependencies — all built-in Node.js 18.15+
501
+ */
502
+ private collectAndSubmitMetrics;
503
+ /**
504
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
505
+ * Returns a percentage (0–100).
506
+ */
507
+ private measureCpuPercent;
508
+ /**
509
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
510
+ */
511
+ private collectDiskMetrics;
512
+ /**
513
+ * Submit a system metric snapshot to the monitoring server.
514
+ */
515
+ submitSystemMetric(metric: {
516
+ hostname: string;
517
+ cpuPercent: number;
518
+ memoryTotal: number;
519
+ memoryUsed: number;
520
+ memoryPercent: number;
521
+ disks: Array<{
522
+ path: string;
523
+ total: number;
524
+ used: number;
525
+ percent: number;
526
+ }>;
527
+ }): Promise<void>;
468
528
  }
469
529
 
470
530
  export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.d.ts CHANGED
@@ -53,7 +53,7 @@ interface MonitorClientConfig {
53
53
  allowInsecureHttp?: boolean;
54
54
  /** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
55
55
  npmRegistryUrl?: string;
56
- /** Enable SDK-based health check polling (default: false) */
56
+ /** Enable SDK-based health check polling (default: true) */
57
57
  healthCheckEnabled?: boolean;
58
58
  /** Interval to fetch health endpoints from server in ms (default: 60000) */
59
59
  healthCheckFetchIntervalMs?: number;
@@ -226,6 +226,11 @@ declare class MonitorClient {
226
226
  private healthCheckTimers;
227
227
  private healthCheckResultsQueue;
228
228
  private healthCheckFlushTimer;
229
+ private sdkErrorQueue;
230
+ private sdkErrorFlushTimer;
231
+ private sdkErrorsInCurrentWindow;
232
+ private sdkErrorWindowResetTimer;
233
+ private metricsCollectionTimer;
229
234
  constructor(config: MonitorClientConfig);
230
235
  /**
231
236
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -253,6 +258,15 @@ declare class MonitorClient {
253
258
  flush(): Promise<void>;
254
259
  private getErrorKey;
255
260
  close(): Promise<void>;
261
+ /**
262
+ * Queue an SDK error to be reported to the server's system errors page.
263
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
264
+ * Throttled to max 20 errors per minute to prevent flooding.
265
+ */
266
+ private reportError;
267
+ private startSdkErrorFlushTimer;
268
+ private stopSdkErrorFlushTimer;
269
+ private flushSdkErrors;
256
270
  private stopAuditIntervalTimer;
257
271
  /**
258
272
  * Fetch project settings from the monitoring server.
@@ -263,6 +277,12 @@ declare class MonitorClient {
263
277
  vulnerabilityScanIntervalHours: number;
264
278
  scanRequestedAt: string | null;
265
279
  techScanRequestedAt: string | null;
280
+ metricsEnabled?: boolean;
281
+ metricsIntervalSeconds?: number;
282
+ metricsDiskPaths?: string[];
283
+ metricsCpuThreshold?: number;
284
+ metricsRamThreshold?: number;
285
+ metricsDiskThreshold?: number;
266
286
  } | null>;
267
287
  /**
268
288
  * Setup automatic vulnerability scanning based on server-configured interval.
@@ -465,6 +485,46 @@ declare class MonitorClient {
465
485
  * Stop all health check timers
466
486
  */
467
487
  private stopHealthCheckTimers;
488
+ /**
489
+ * Start system metrics collection based on server-configured settings.
490
+ * Only runs if metricsEnabled is true in project settings.
491
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
492
+ */
493
+ setupSystemMetricsCollection(): Promise<void>;
494
+ private stopMetricsCollection;
495
+ /**
496
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
497
+ * and submit to the monitoring server.
498
+ *
499
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
500
+ * Zero dependencies — all built-in Node.js 18.15+
501
+ */
502
+ private collectAndSubmitMetrics;
503
+ /**
504
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
505
+ * Returns a percentage (0–100).
506
+ */
507
+ private measureCpuPercent;
508
+ /**
509
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
510
+ */
511
+ private collectDiskMetrics;
512
+ /**
513
+ * Submit a system metric snapshot to the monitoring server.
514
+ */
515
+ submitSystemMetric(metric: {
516
+ hostname: string;
517
+ cpuPercent: number;
518
+ memoryTotal: number;
519
+ memoryUsed: number;
520
+ memoryPercent: number;
521
+ disks: Array<{
522
+ path: string;
523
+ total: number;
524
+ used: number;
525
+ percent: number;
526
+ }>;
527
+ }): Promise<void>;
468
528
  }
469
529
 
470
530
  export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
package/dist/index.js CHANGED
@@ -94,6 +94,13 @@ var MonitorClient = class {
94
94
  this.healthCheckTimers = /* @__PURE__ */ new Map();
95
95
  this.healthCheckResultsQueue = [];
96
96
  this.healthCheckFlushTimer = null;
97
+ // SDK error reporting queue (errors visible on admin system errors page)
98
+ this.sdkErrorQueue = [];
99
+ this.sdkErrorFlushTimer = null;
100
+ this.sdkErrorsInCurrentWindow = 0;
101
+ this.sdkErrorWindowResetTimer = null;
102
+ // System metrics collection (on-premise only, opt-in)
103
+ this.metricsCollectionTimer = null;
97
104
  if (!config.apiKey || config.apiKey.trim().length === 0) {
98
105
  throw new Error("[MonitorClient] API key is required");
99
106
  }
@@ -153,24 +160,28 @@ var MonitorClient = class {
153
160
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
154
161
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
155
162
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
156
- this.healthCheckEnabled = config.healthCheckEnabled || false;
163
+ this.healthCheckEnabled = config.healthCheckEnabled ?? true;
157
164
  this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
158
165
  this.startFlushTimer();
159
166
  if (this.trackDependencies) {
160
167
  this.syncDependencies().catch((err) => {
161
- console.error("[MonitorClient] Failed to sync dependencies:", err instanceof Error ? err.message : String(err));
168
+ this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
162
169
  });
163
170
  }
164
171
  if (this.autoAudit) {
165
172
  this.setupAutoAudit().catch((err) => {
166
- console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
173
+ this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
167
174
  });
168
175
  }
169
176
  if (this.healthCheckEnabled) {
170
177
  this.setupHealthCheckPolling().catch((err) => {
171
- console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
178
+ this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
172
179
  });
173
180
  }
181
+ this.startSdkErrorFlushTimer();
182
+ this.setupSystemMetricsCollection().catch((err) => {
183
+ this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
184
+ });
174
185
  }
175
186
  /**
176
187
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -320,8 +331,66 @@ var MonitorClient = class {
320
331
  this.stopFlushTimer();
321
332
  this.stopAuditIntervalTimer();
322
333
  this.stopHealthCheckTimers();
334
+ this.stopSdkErrorFlushTimer();
335
+ this.stopMetricsCollection();
323
336
  await this.flush();
324
337
  await this.flushHealthResults();
338
+ await this.flushSdkErrors();
339
+ }
340
+ /**
341
+ * Queue an SDK error to be reported to the server's system errors page.
342
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
343
+ * Throttled to max 20 errors per minute to prevent flooding.
344
+ */
345
+ reportError(category, message, err) {
346
+ if (this.sdkErrorsInCurrentWindow >= 20) return;
347
+ this.sdkErrorsInCurrentWindow++;
348
+ const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
349
+ const stack = err instanceof Error ? err.stack : void 0;
350
+ this.sdkErrorQueue.push({ category, message: errorMessage, stack });
351
+ if (this.sdkErrorQueue.length >= 20) {
352
+ this.flushSdkErrors().catch(() => {
353
+ });
354
+ }
355
+ }
356
+ startSdkErrorFlushTimer() {
357
+ this.sdkErrorFlushTimer = setInterval(() => {
358
+ this.flushSdkErrors().catch(() => {
359
+ });
360
+ }, 3e4);
361
+ this.sdkErrorWindowResetTimer = setInterval(() => {
362
+ this.sdkErrorsInCurrentWindow = 0;
363
+ }, 6e4);
364
+ }
365
+ stopSdkErrorFlushTimer() {
366
+ if (this.sdkErrorFlushTimer) {
367
+ clearInterval(this.sdkErrorFlushTimer);
368
+ this.sdkErrorFlushTimer = null;
369
+ }
370
+ if (this.sdkErrorWindowResetTimer) {
371
+ clearInterval(this.sdkErrorWindowResetTimer);
372
+ this.sdkErrorWindowResetTimer = null;
373
+ }
374
+ }
375
+ async flushSdkErrors() {
376
+ if (this.sdkErrorQueue.length === 0) return;
377
+ const errors = [...this.sdkErrorQueue];
378
+ this.sdkErrorQueue = [];
379
+ try {
380
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
381
+ method: "POST",
382
+ headers: {
383
+ "Content-Type": "application/json",
384
+ "Authorization": `Bearer ${this.apiKey}`
385
+ },
386
+ body: JSON.stringify({ errors })
387
+ });
388
+ if (!response.ok) {
389
+ console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
390
+ }
391
+ } catch (err) {
392
+ console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
393
+ }
325
394
  }
326
395
  stopAuditIntervalTimer() {
327
396
  if (this.auditIntervalTimer) {
@@ -383,14 +452,14 @@ var MonitorClient = class {
383
452
  const intervalMs = intervalHours * 60 * 60 * 1e3;
384
453
  this.auditIntervalTimer = setInterval(() => {
385
454
  this.runScanAndTrackTime().catch((err) => {
386
- console.error("[MonitorClient] Auto audit scan failed:", err);
455
+ this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
387
456
  });
388
457
  }, intervalMs);
389
458
  }
390
459
  console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
391
460
  this.settingsPollingTimer = setInterval(() => {
392
461
  this.checkForScanRequest().catch((err) => {
393
- console.error("[MonitorClient] Scan request check failed:", err);
462
+ this.reportError("SETTINGS_POLL", "Scan request check failed", err);
394
463
  });
395
464
  }, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
396
465
  }
@@ -411,7 +480,7 @@ var MonitorClient = class {
411
480
  const duration = Date.now() - startTime;
412
481
  console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
413
482
  } catch (err) {
414
- console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
483
+ this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
415
484
  }
416
485
  }
417
486
  /**
@@ -438,7 +507,7 @@ var MonitorClient = class {
438
507
  }
439
508
  }
440
509
  } catch (err) {
441
- console.error("[MonitorClient] Failed to check for scan request:", err instanceof Error ? err.message : String(err));
510
+ this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
442
511
  }
443
512
  }
444
513
  enqueue(payload) {
@@ -525,7 +594,7 @@ var MonitorClient = class {
525
594
  );
526
595
  console.log("[MonitorClient] Technology sync completed successfully");
527
596
  } catch (err) {
528
- console.error("[MonitorClient] Technology sync failed:", err instanceof Error ? err.message : String(err));
597
+ this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
529
598
  }
530
599
  }
531
600
  async performDependencySync(signal) {
@@ -1029,7 +1098,7 @@ var MonitorClient = class {
1029
1098
  const result = await response.json();
1030
1099
  return result.data;
1031
1100
  } catch (err) {
1032
- console.error("[MonitorClient] Failed to audit dependencies:", err instanceof Error ? err.message : String(err));
1101
+ this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
1033
1102
  return null;
1034
1103
  }
1035
1104
  }
@@ -1059,7 +1128,7 @@ var MonitorClient = class {
1059
1128
  }
1060
1129
  return result;
1061
1130
  } catch (err) {
1062
- console.error("[MonitorClient] Multi-path audit failed:", err instanceof Error ? err.message : String(err));
1131
+ this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
1063
1132
  return null;
1064
1133
  }
1065
1134
  }
@@ -1321,12 +1390,12 @@ var MonitorClient = class {
1321
1390
  await this.fetchAndScheduleHealthChecks();
1322
1391
  this.healthCheckFetchTimer = setInterval(() => {
1323
1392
  this.fetchAndScheduleHealthChecks().catch((err) => {
1324
- console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1393
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
1325
1394
  });
1326
1395
  }, this.healthCheckFetchIntervalMs);
1327
1396
  this.healthCheckFlushTimer = setInterval(() => {
1328
1397
  this.flushHealthResults().catch((err) => {
1329
- console.error("[MonitorClient] Failed to flush health results:", err);
1398
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
1330
1399
  });
1331
1400
  }, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
1332
1401
  }
@@ -1347,7 +1416,7 @@ var MonitorClient = class {
1347
1416
  this.scheduleHealthCheck(endpoint);
1348
1417
  }
1349
1418
  } catch (err) {
1350
- console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1419
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
1351
1420
  }
1352
1421
  }
1353
1422
  /**
@@ -1356,11 +1425,11 @@ var MonitorClient = class {
1356
1425
  scheduleHealthCheck(endpoint) {
1357
1426
  const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1358
1427
  this.runHealthCheck(endpoint).catch((err) => {
1359
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1428
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1360
1429
  });
1361
1430
  const timer = setInterval(() => {
1362
1431
  this.runHealthCheck(endpoint).catch((err) => {
1363
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1432
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1364
1433
  });
1365
1434
  }, intervalMs);
1366
1435
  this.healthCheckTimers.set(endpoint.id, timer);
@@ -1386,7 +1455,7 @@ var MonitorClient = class {
1386
1455
  const response = await this.submitHealthResults(results);
1387
1456
  console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1388
1457
  } catch (err) {
1389
- console.error("[MonitorClient] Failed to flush health results:", err);
1458
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
1390
1459
  if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1391
1460
  this.healthCheckResultsQueue.unshift(...results);
1392
1461
  }
@@ -1409,6 +1478,150 @@ var MonitorClient = class {
1409
1478
  this.healthCheckTimers.delete(endpointId);
1410
1479
  }
1411
1480
  }
1481
+ // ---- System Metrics (on-premise only) ----
1482
+ /**
1483
+ * Start system metrics collection based on server-configured settings.
1484
+ * Only runs if metricsEnabled is true in project settings.
1485
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
1486
+ */
1487
+ async setupSystemMetricsCollection() {
1488
+ const settings = await this.fetchProjectSettings();
1489
+ if (!settings?.metricsEnabled) {
1490
+ return;
1491
+ }
1492
+ const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 60) * 1e3);
1493
+ const diskPaths = settings.metricsDiskPaths ?? ["/"];
1494
+ console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s, paths: ${diskPaths.join(", ")})`);
1495
+ this.collectAndSubmitMetrics(diskPaths).catch((err) => {
1496
+ this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
1497
+ });
1498
+ this.metricsCollectionTimer = setInterval(() => {
1499
+ this.collectAndSubmitMetrics(diskPaths).catch((err) => {
1500
+ this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
1501
+ });
1502
+ }, intervalMs);
1503
+ }
1504
+ stopMetricsCollection() {
1505
+ if (this.metricsCollectionTimer) {
1506
+ clearInterval(this.metricsCollectionTimer);
1507
+ this.metricsCollectionTimer = null;
1508
+ }
1509
+ }
1510
+ /**
1511
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
1512
+ * and submit to the monitoring server.
1513
+ *
1514
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
1515
+ * Zero dependencies — all built-in Node.js 18.15+
1516
+ */
1517
+ async collectAndSubmitMetrics(diskPaths) {
1518
+ if (typeof window !== "undefined" || typeof document !== "undefined") {
1519
+ return;
1520
+ }
1521
+ let os;
1522
+ let fs;
1523
+ try {
1524
+ const osModule = await import(
1525
+ /* webpackIgnore: true */
1526
+ "os"
1527
+ );
1528
+ const fsModule = await import(
1529
+ /* webpackIgnore: true */
1530
+ "fs/promises"
1531
+ );
1532
+ os = osModule;
1533
+ fs = fsModule;
1534
+ } catch {
1535
+ this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
1536
+ return;
1537
+ }
1538
+ try {
1539
+ const hostname = os.hostname();
1540
+ const cpuPercent = await this.measureCpuPercent(os);
1541
+ const memoryTotal = os.totalmem();
1542
+ const memoryFree = os.freemem();
1543
+ const memoryUsed = memoryTotal - memoryFree;
1544
+ const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
1545
+ const disks = await this.collectDiskMetrics(diskPaths, fs);
1546
+ await this.submitSystemMetric({
1547
+ hostname,
1548
+ cpuPercent,
1549
+ memoryTotal,
1550
+ memoryUsed,
1551
+ memoryPercent,
1552
+ disks
1553
+ });
1554
+ } catch (err) {
1555
+ this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
1556
+ }
1557
+ }
1558
+ /**
1559
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
1560
+ * Returns a percentage (0–100).
1561
+ */
1562
+ async measureCpuPercent(os) {
1563
+ const sample1 = os.cpus();
1564
+ await new Promise((resolve) => setTimeout(resolve, 500));
1565
+ const sample2 = os.cpus();
1566
+ let totalIdle = 0;
1567
+ let totalTick = 0;
1568
+ for (let i = 0; i < sample2.length; i++) {
1569
+ const prev = sample1[i];
1570
+ const curr = sample2[i];
1571
+ const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
1572
+ const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
1573
+ totalTick += currTotal - prevTotal;
1574
+ totalIdle += curr.times.idle - prev.times.idle;
1575
+ }
1576
+ const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
1577
+ return Math.max(0, Math.min(100, 100 - idlePercent));
1578
+ }
1579
+ /**
1580
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
1581
+ */
1582
+ async collectDiskMetrics(paths, fs) {
1583
+ const results = [];
1584
+ for (const diskPath of paths) {
1585
+ try {
1586
+ const stat = await fs.statfs(diskPath);
1587
+ const total = stat.bsize * stat.blocks;
1588
+ const free = stat.bsize * stat.bavail;
1589
+ const used = total - free;
1590
+ const percent = total > 0 ? used / total * 100 : 0;
1591
+ results.push({
1592
+ path: diskPath,
1593
+ total: Math.round(total),
1594
+ used: Math.round(used),
1595
+ percent: Math.round(percent * 10) / 10
1596
+ // 1 decimal place
1597
+ });
1598
+ } catch {
1599
+ this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
1600
+ }
1601
+ }
1602
+ return results;
1603
+ }
1604
+ /**
1605
+ * Submit a system metric snapshot to the monitoring server.
1606
+ */
1607
+ async submitSystemMetric(metric) {
1608
+ try {
1609
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
1610
+ method: "POST",
1611
+ headers: {
1612
+ "Content-Type": "application/json",
1613
+ "Authorization": `Bearer ${this.apiKey}`
1614
+ },
1615
+ body: JSON.stringify(metric)
1616
+ });
1617
+ if (!response.ok) {
1618
+ const errorText = await response.text().catch(() => "");
1619
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1620
+ }
1621
+ } catch (err) {
1622
+ this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
1623
+ }
1624
+ }
1412
1625
  };
1413
1626
  // Annotate the CommonJS export names for ESM import in node:
1414
1627
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -58,6 +58,13 @@ var MonitorClient = class {
58
58
  this.healthCheckTimers = /* @__PURE__ */ new Map();
59
59
  this.healthCheckResultsQueue = [];
60
60
  this.healthCheckFlushTimer = null;
61
+ // SDK error reporting queue (errors visible on admin system errors page)
62
+ this.sdkErrorQueue = [];
63
+ this.sdkErrorFlushTimer = null;
64
+ this.sdkErrorsInCurrentWindow = 0;
65
+ this.sdkErrorWindowResetTimer = null;
66
+ // System metrics collection (on-premise only, opt-in)
67
+ this.metricsCollectionTimer = null;
61
68
  if (!config.apiKey || config.apiKey.trim().length === 0) {
62
69
  throw new Error("[MonitorClient] API key is required");
63
70
  }
@@ -117,24 +124,28 @@ var MonitorClient = class {
117
124
  this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
118
125
  this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
119
126
  this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
120
- this.healthCheckEnabled = config.healthCheckEnabled || false;
127
+ this.healthCheckEnabled = config.healthCheckEnabled ?? true;
121
128
  this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
122
129
  this.startFlushTimer();
123
130
  if (this.trackDependencies) {
124
131
  this.syncDependencies().catch((err) => {
125
- console.error("[MonitorClient] Failed to sync dependencies:", err instanceof Error ? err.message : String(err));
132
+ this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
126
133
  });
127
134
  }
128
135
  if (this.autoAudit) {
129
136
  this.setupAutoAudit().catch((err) => {
130
- console.error("[MonitorClient] Failed to setup auto audit:", err instanceof Error ? err.message : String(err));
137
+ this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
131
138
  });
132
139
  }
133
140
  if (this.healthCheckEnabled) {
134
141
  this.setupHealthCheckPolling().catch((err) => {
135
- console.error("[MonitorClient] Failed to setup health check polling:", err instanceof Error ? err.message : String(err));
142
+ this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
136
143
  });
137
144
  }
145
+ this.startSdkErrorFlushTimer();
146
+ this.setupSystemMetricsCollection().catch((err) => {
147
+ this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
148
+ });
138
149
  }
139
150
  /**
140
151
  * Security: Validate and sanitize metadata to prevent oversized payloads
@@ -284,8 +295,66 @@ var MonitorClient = class {
284
295
  this.stopFlushTimer();
285
296
  this.stopAuditIntervalTimer();
286
297
  this.stopHealthCheckTimers();
298
+ this.stopSdkErrorFlushTimer();
299
+ this.stopMetricsCollection();
287
300
  await this.flush();
288
301
  await this.flushHealthResults();
302
+ await this.flushSdkErrors();
303
+ }
304
+ /**
305
+ * Queue an SDK error to be reported to the server's system errors page.
306
+ * Fire-and-forget — never throws. If reporting itself fails, logs to console only.
307
+ * Throttled to max 20 errors per minute to prevent flooding.
308
+ */
309
+ reportError(category, message, err) {
310
+ if (this.sdkErrorsInCurrentWindow >= 20) return;
311
+ this.sdkErrorsInCurrentWindow++;
312
+ const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
313
+ const stack = err instanceof Error ? err.stack : void 0;
314
+ this.sdkErrorQueue.push({ category, message: errorMessage, stack });
315
+ if (this.sdkErrorQueue.length >= 20) {
316
+ this.flushSdkErrors().catch(() => {
317
+ });
318
+ }
319
+ }
320
+ startSdkErrorFlushTimer() {
321
+ this.sdkErrorFlushTimer = setInterval(() => {
322
+ this.flushSdkErrors().catch(() => {
323
+ });
324
+ }, 3e4);
325
+ this.sdkErrorWindowResetTimer = setInterval(() => {
326
+ this.sdkErrorsInCurrentWindow = 0;
327
+ }, 6e4);
328
+ }
329
+ stopSdkErrorFlushTimer() {
330
+ if (this.sdkErrorFlushTimer) {
331
+ clearInterval(this.sdkErrorFlushTimer);
332
+ this.sdkErrorFlushTimer = null;
333
+ }
334
+ if (this.sdkErrorWindowResetTimer) {
335
+ clearInterval(this.sdkErrorWindowResetTimer);
336
+ this.sdkErrorWindowResetTimer = null;
337
+ }
338
+ }
339
+ async flushSdkErrors() {
340
+ if (this.sdkErrorQueue.length === 0) return;
341
+ const errors = [...this.sdkErrorQueue];
342
+ this.sdkErrorQueue = [];
343
+ try {
344
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
345
+ method: "POST",
346
+ headers: {
347
+ "Content-Type": "application/json",
348
+ "Authorization": `Bearer ${this.apiKey}`
349
+ },
350
+ body: JSON.stringify({ errors })
351
+ });
352
+ if (!response.ok) {
353
+ console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
354
+ }
355
+ } catch (err) {
356
+ console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
357
+ }
289
358
  }
290
359
  stopAuditIntervalTimer() {
291
360
  if (this.auditIntervalTimer) {
@@ -347,14 +416,14 @@ var MonitorClient = class {
347
416
  const intervalMs = intervalHours * 60 * 60 * 1e3;
348
417
  this.auditIntervalTimer = setInterval(() => {
349
418
  this.runScanAndTrackTime().catch((err) => {
350
- console.error("[MonitorClient] Auto audit scan failed:", err);
419
+ this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
351
420
  });
352
421
  }, intervalMs);
353
422
  }
354
423
  console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
355
424
  this.settingsPollingTimer = setInterval(() => {
356
425
  this.checkForScanRequest().catch((err) => {
357
- console.error("[MonitorClient] Scan request check failed:", err);
426
+ this.reportError("SETTINGS_POLL", "Scan request check failed", err);
358
427
  });
359
428
  }, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
360
429
  }
@@ -375,7 +444,7 @@ var MonitorClient = class {
375
444
  const duration = Date.now() - startTime;
376
445
  console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
377
446
  } catch (err) {
378
- console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
447
+ this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
379
448
  }
380
449
  }
381
450
  /**
@@ -402,7 +471,7 @@ var MonitorClient = class {
402
471
  }
403
472
  }
404
473
  } catch (err) {
405
- console.error("[MonitorClient] Failed to check for scan request:", err instanceof Error ? err.message : String(err));
474
+ this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
406
475
  }
407
476
  }
408
477
  enqueue(payload) {
@@ -489,7 +558,7 @@ var MonitorClient = class {
489
558
  );
490
559
  console.log("[MonitorClient] Technology sync completed successfully");
491
560
  } catch (err) {
492
- console.error("[MonitorClient] Technology sync failed:", err instanceof Error ? err.message : String(err));
561
+ this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
493
562
  }
494
563
  }
495
564
  async performDependencySync(signal) {
@@ -993,7 +1062,7 @@ var MonitorClient = class {
993
1062
  const result = await response.json();
994
1063
  return result.data;
995
1064
  } catch (err) {
996
- console.error("[MonitorClient] Failed to audit dependencies:", err instanceof Error ? err.message : String(err));
1065
+ this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
997
1066
  return null;
998
1067
  }
999
1068
  }
@@ -1023,7 +1092,7 @@ var MonitorClient = class {
1023
1092
  }
1024
1093
  return result;
1025
1094
  } catch (err) {
1026
- console.error("[MonitorClient] Multi-path audit failed:", err instanceof Error ? err.message : String(err));
1095
+ this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
1027
1096
  return null;
1028
1097
  }
1029
1098
  }
@@ -1285,12 +1354,12 @@ var MonitorClient = class {
1285
1354
  await this.fetchAndScheduleHealthChecks();
1286
1355
  this.healthCheckFetchTimer = setInterval(() => {
1287
1356
  this.fetchAndScheduleHealthChecks().catch((err) => {
1288
- console.error("[MonitorClient] Failed to fetch and schedule health checks:", err);
1357
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
1289
1358
  });
1290
1359
  }, this.healthCheckFetchIntervalMs);
1291
1360
  this.healthCheckFlushTimer = setInterval(() => {
1292
1361
  this.flushHealthResults().catch((err) => {
1293
- console.error("[MonitorClient] Failed to flush health results:", err);
1362
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
1294
1363
  });
1295
1364
  }, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
1296
1365
  }
@@ -1311,7 +1380,7 @@ var MonitorClient = class {
1311
1380
  this.scheduleHealthCheck(endpoint);
1312
1381
  }
1313
1382
  } catch (err) {
1314
- console.error("[MonitorClient] Failed to fetch health endpoints:", err);
1383
+ this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
1315
1384
  }
1316
1385
  }
1317
1386
  /**
@@ -1320,11 +1389,11 @@ var MonitorClient = class {
1320
1389
  scheduleHealthCheck(endpoint) {
1321
1390
  const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
1322
1391
  this.runHealthCheck(endpoint).catch((err) => {
1323
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1392
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1324
1393
  });
1325
1394
  const timer = setInterval(() => {
1326
1395
  this.runHealthCheck(endpoint).catch((err) => {
1327
- console.error(`[MonitorClient] Health check failed for ${endpoint.name}:`, err);
1396
+ this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
1328
1397
  });
1329
1398
  }, intervalMs);
1330
1399
  this.healthCheckTimers.set(endpoint.id, timer);
@@ -1350,7 +1419,7 @@ var MonitorClient = class {
1350
1419
  const response = await this.submitHealthResults(results);
1351
1420
  console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
1352
1421
  } catch (err) {
1353
- console.error("[MonitorClient] Failed to flush health results:", err);
1422
+ this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
1354
1423
  if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
1355
1424
  this.healthCheckResultsQueue.unshift(...results);
1356
1425
  }
@@ -1373,6 +1442,150 @@ var MonitorClient = class {
1373
1442
  this.healthCheckTimers.delete(endpointId);
1374
1443
  }
1375
1444
  }
1445
+ // ---- System Metrics (on-premise only) ----
1446
+ /**
1447
+ * Start system metrics collection based on server-configured settings.
1448
+ * Only runs if metricsEnabled is true in project settings.
1449
+ * Collects CPU, RAM, and disk usage using built-in Node.js modules.
1450
+ */
1451
+ async setupSystemMetricsCollection() {
1452
+ const settings = await this.fetchProjectSettings();
1453
+ if (!settings?.metricsEnabled) {
1454
+ return;
1455
+ }
1456
+ const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 60) * 1e3);
1457
+ const diskPaths = settings.metricsDiskPaths ?? ["/"];
1458
+ console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s, paths: ${diskPaths.join(", ")})`);
1459
+ this.collectAndSubmitMetrics(diskPaths).catch((err) => {
1460
+ this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
1461
+ });
1462
+ this.metricsCollectionTimer = setInterval(() => {
1463
+ this.collectAndSubmitMetrics(diskPaths).catch((err) => {
1464
+ this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
1465
+ });
1466
+ }, intervalMs);
1467
+ }
1468
+ stopMetricsCollection() {
1469
+ if (this.metricsCollectionTimer) {
1470
+ clearInterval(this.metricsCollectionTimer);
1471
+ this.metricsCollectionTimer = null;
1472
+ }
1473
+ }
1474
+ /**
1475
+ * Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
1476
+ * and submit to the monitoring server.
1477
+ *
1478
+ * Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
1479
+ * Zero dependencies — all built-in Node.js 18.15+
1480
+ */
1481
+ async collectAndSubmitMetrics(diskPaths) {
1482
+ if (typeof window !== "undefined" || typeof document !== "undefined") {
1483
+ return;
1484
+ }
1485
+ let os;
1486
+ let fs;
1487
+ try {
1488
+ const osModule = await import(
1489
+ /* webpackIgnore: true */
1490
+ "os"
1491
+ );
1492
+ const fsModule = await import(
1493
+ /* webpackIgnore: true */
1494
+ "fs/promises"
1495
+ );
1496
+ os = osModule;
1497
+ fs = fsModule;
1498
+ } catch {
1499
+ this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
1500
+ return;
1501
+ }
1502
+ try {
1503
+ const hostname = os.hostname();
1504
+ const cpuPercent = await this.measureCpuPercent(os);
1505
+ const memoryTotal = os.totalmem();
1506
+ const memoryFree = os.freemem();
1507
+ const memoryUsed = memoryTotal - memoryFree;
1508
+ const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
1509
+ const disks = await this.collectDiskMetrics(diskPaths, fs);
1510
+ await this.submitSystemMetric({
1511
+ hostname,
1512
+ cpuPercent,
1513
+ memoryTotal,
1514
+ memoryUsed,
1515
+ memoryPercent,
1516
+ disks
1517
+ });
1518
+ } catch (err) {
1519
+ this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
1520
+ }
1521
+ }
1522
+ /**
1523
+ * Measure CPU utilization by sampling cpus() twice with a 500ms gap.
1524
+ * Returns a percentage (0–100).
1525
+ */
1526
+ async measureCpuPercent(os) {
1527
+ const sample1 = os.cpus();
1528
+ await new Promise((resolve) => setTimeout(resolve, 500));
1529
+ const sample2 = os.cpus();
1530
+ let totalIdle = 0;
1531
+ let totalTick = 0;
1532
+ for (let i = 0; i < sample2.length; i++) {
1533
+ const prev = sample1[i];
1534
+ const curr = sample2[i];
1535
+ const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
1536
+ const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
1537
+ totalTick += currTotal - prevTotal;
1538
+ totalIdle += curr.times.idle - prev.times.idle;
1539
+ }
1540
+ const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
1541
+ return Math.max(0, Math.min(100, 100 - idlePercent));
1542
+ }
1543
+ /**
1544
+ * Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
1545
+ */
1546
+ async collectDiskMetrics(paths, fs) {
1547
+ const results = [];
1548
+ for (const diskPath of paths) {
1549
+ try {
1550
+ const stat = await fs.statfs(diskPath);
1551
+ const total = stat.bsize * stat.blocks;
1552
+ const free = stat.bsize * stat.bavail;
1553
+ const used = total - free;
1554
+ const percent = total > 0 ? used / total * 100 : 0;
1555
+ results.push({
1556
+ path: diskPath,
1557
+ total: Math.round(total),
1558
+ used: Math.round(used),
1559
+ percent: Math.round(percent * 10) / 10
1560
+ // 1 decimal place
1561
+ });
1562
+ } catch {
1563
+ this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
1564
+ }
1565
+ }
1566
+ return results;
1567
+ }
1568
+ /**
1569
+ * Submit a system metric snapshot to the monitoring server.
1570
+ */
1571
+ async submitSystemMetric(metric) {
1572
+ try {
1573
+ const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
1574
+ method: "POST",
1575
+ headers: {
1576
+ "Content-Type": "application/json",
1577
+ "Authorization": `Bearer ${this.apiKey}`
1578
+ },
1579
+ body: JSON.stringify(metric)
1580
+ });
1581
+ if (!response.ok) {
1582
+ const errorText = await response.text().catch(() => "");
1583
+ throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
1584
+ }
1585
+ } catch (err) {
1586
+ this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
1587
+ }
1588
+ }
1376
1589
  };
1377
1590
  export {
1378
1591
  MonitorClient
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceon-oy/monitor-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
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",