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