@ceon-oy/monitor-sdk 1.0.9 → 1.0.11
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 +53 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.js +177 -38
- package/dist/index.mjs +177 -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;
|
|
@@ -144,6 +176,15 @@ declare class MonitorClient {
|
|
|
144
176
|
private lastKnownScanRequestedAt;
|
|
145
177
|
private lastKnownTechScanRequestedAt;
|
|
146
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;
|
|
147
188
|
captureError(error: Error, context?: ErrorContext): Promise<void>;
|
|
148
189
|
captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>;
|
|
149
190
|
flush(): Promise<void>;
|
|
@@ -168,6 +209,7 @@ declare class MonitorClient {
|
|
|
168
209
|
private setupAutoAudit;
|
|
169
210
|
/**
|
|
170
211
|
* Run a vulnerability scan and track the time it was run.
|
|
212
|
+
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
171
213
|
*/
|
|
172
214
|
private runScanAndTrackTime;
|
|
173
215
|
/**
|
|
@@ -263,6 +305,16 @@ declare class MonitorClient {
|
|
|
263
305
|
projectPath?: string;
|
|
264
306
|
environment?: string;
|
|
265
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>;
|
|
266
318
|
/**
|
|
267
319
|
* Parse npm audit JSON output into vulnerability items
|
|
268
320
|
*/
|
|
@@ -271,4 +323,4 @@ declare class MonitorClient {
|
|
|
271
323
|
private getRecommendation;
|
|
272
324
|
}
|
|
273
325
|
|
|
274
|
-
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;
|
|
@@ -144,6 +176,15 @@ declare class MonitorClient {
|
|
|
144
176
|
private lastKnownScanRequestedAt;
|
|
145
177
|
private lastKnownTechScanRequestedAt;
|
|
146
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;
|
|
147
188
|
captureError(error: Error, context?: ErrorContext): Promise<void>;
|
|
148
189
|
captureMessage(message: string, severity?: Severity, context?: ErrorContext): Promise<void>;
|
|
149
190
|
flush(): Promise<void>;
|
|
@@ -168,6 +209,7 @@ declare class MonitorClient {
|
|
|
168
209
|
private setupAutoAudit;
|
|
169
210
|
/**
|
|
170
211
|
* Run a vulnerability scan and track the time it was run.
|
|
212
|
+
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
171
213
|
*/
|
|
172
214
|
private runScanAndTrackTime;
|
|
173
215
|
/**
|
|
@@ -263,6 +305,16 @@ declare class MonitorClient {
|
|
|
263
305
|
projectPath?: string;
|
|
264
306
|
environment?: string;
|
|
265
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>;
|
|
266
318
|
/**
|
|
267
319
|
* Parse npm audit JSON output into vulnerability items
|
|
268
320
|
*/
|
|
@@ -271,4 +323,4 @@ declare class MonitorClient {
|
|
|
271
323
|
private getRecommendation;
|
|
272
324
|
}
|
|
273
325
|
|
|
274
|
-
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 = [];
|
|
@@ -73,14 +92,14 @@ var MonitorClient = class {
|
|
|
73
92
|
this.apiKey = config.apiKey;
|
|
74
93
|
this.endpoint = config.endpoint.replace(/\/$/, "");
|
|
75
94
|
this.environment = config.environment || "production";
|
|
76
|
-
this.batchSize = Math.max(1, config.batchSize || 10);
|
|
77
|
-
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));
|
|
78
97
|
this.trackDependencies = config.trackDependencies || false;
|
|
79
98
|
this.packageJsonPath = config.packageJsonPath;
|
|
80
99
|
this.dependencySources = config.dependencySources;
|
|
81
|
-
this.maxQueueSize = config.maxQueueSize || 1e3;
|
|
82
|
-
this.maxRetries = config.maxRetries || 3;
|
|
83
|
-
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);
|
|
84
103
|
const defaultExcludePatterns = [
|
|
85
104
|
"@types/*",
|
|
86
105
|
"eslint*",
|
|
@@ -91,7 +110,12 @@ var MonitorClient = class {
|
|
|
91
110
|
"@typescript-eslint/*"
|
|
92
111
|
];
|
|
93
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
|
+
});
|
|
94
117
|
this.autoAudit = config.autoAudit || false;
|
|
118
|
+
this.auditPaths = config.auditPaths;
|
|
95
119
|
this.startFlushTimer();
|
|
96
120
|
if (this.trackDependencies) {
|
|
97
121
|
this.syncDependencies().catch((err) => {
|
|
@@ -104,6 +128,36 @@ var MonitorClient = class {
|
|
|
104
128
|
});
|
|
105
129
|
}
|
|
106
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
|
+
}
|
|
107
161
|
async captureError(error, context) {
|
|
108
162
|
if (this.isClosed) return;
|
|
109
163
|
const payload = {
|
|
@@ -117,7 +171,7 @@ var MonitorClient = class {
|
|
|
117
171
|
userAgent: context?.userAgent,
|
|
118
172
|
ip: context?.ip,
|
|
119
173
|
requestId: context?.requestId,
|
|
120
|
-
metadata: context?.metadata
|
|
174
|
+
metadata: this.sanitizeMetadata(context?.metadata)
|
|
121
175
|
};
|
|
122
176
|
this.enqueue(payload);
|
|
123
177
|
}
|
|
@@ -133,7 +187,7 @@ var MonitorClient = class {
|
|
|
133
187
|
userAgent: context?.userAgent,
|
|
134
188
|
ip: context?.ip,
|
|
135
189
|
requestId: context?.requestId,
|
|
136
|
-
metadata: context?.metadata
|
|
190
|
+
metadata: this.sanitizeMetadata(context?.metadata)
|
|
137
191
|
};
|
|
138
192
|
this.enqueue(payload);
|
|
139
193
|
}
|
|
@@ -157,6 +211,12 @@ var MonitorClient = class {
|
|
|
157
211
|
const key = this.getErrorKey(error);
|
|
158
212
|
const retries = this.retryCount.get(key) || 0;
|
|
159
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
|
+
}
|
|
160
220
|
this.retryCount.set(key, retries + 1);
|
|
161
221
|
if (this.queue.length < this.maxQueueSize) {
|
|
162
222
|
this.queue.push(error);
|
|
@@ -241,20 +301,29 @@ var MonitorClient = class {
|
|
|
241
301
|
await this.runScanAndTrackTime();
|
|
242
302
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
243
303
|
this.auditIntervalTimer = setInterval(() => {
|
|
244
|
-
this.runScanAndTrackTime()
|
|
304
|
+
this.runScanAndTrackTime().catch((err) => {
|
|
305
|
+
console.error("[MonitorClient] Auto audit scan failed:", err);
|
|
306
|
+
});
|
|
245
307
|
}, intervalMs);
|
|
246
308
|
}
|
|
247
309
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
248
310
|
this.settingsPollingTimer = setInterval(() => {
|
|
249
|
-
this.checkForScanRequest()
|
|
250
|
-
|
|
311
|
+
this.checkForScanRequest().catch((err) => {
|
|
312
|
+
console.error("[MonitorClient] Scan request check failed:", err);
|
|
313
|
+
});
|
|
314
|
+
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
251
315
|
}
|
|
252
316
|
/**
|
|
253
317
|
* Run a vulnerability scan and track the time it was run.
|
|
318
|
+
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
254
319
|
*/
|
|
255
320
|
async runScanAndTrackTime() {
|
|
256
321
|
try {
|
|
257
|
-
|
|
322
|
+
if (this.auditPaths && this.auditPaths.length > 0) {
|
|
323
|
+
await this.auditMultiplePaths();
|
|
324
|
+
} else {
|
|
325
|
+
await this.auditDependencies();
|
|
326
|
+
}
|
|
258
327
|
this.lastScanTime = /* @__PURE__ */ new Date();
|
|
259
328
|
} catch (err) {
|
|
260
329
|
console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
|
|
@@ -294,7 +363,9 @@ var MonitorClient = class {
|
|
|
294
363
|
}
|
|
295
364
|
this.queue.push(payload);
|
|
296
365
|
if (this.queue.length >= this.batchSize) {
|
|
297
|
-
this.flush()
|
|
366
|
+
this.flush().catch((err) => {
|
|
367
|
+
console.error("[MonitorClient] Flush failed:", err);
|
|
368
|
+
});
|
|
298
369
|
}
|
|
299
370
|
}
|
|
300
371
|
/**
|
|
@@ -329,7 +400,7 @@ var MonitorClient = class {
|
|
|
329
400
|
});
|
|
330
401
|
if (!response.ok) {
|
|
331
402
|
const errorText = await response.text().catch(() => "");
|
|
332
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
403
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
333
404
|
}
|
|
334
405
|
}
|
|
335
406
|
async sendBatch(errors) {
|
|
@@ -343,12 +414,14 @@ var MonitorClient = class {
|
|
|
343
414
|
});
|
|
344
415
|
if (!response.ok) {
|
|
345
416
|
const errorText = await response.text().catch(() => "");
|
|
346
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
417
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
347
418
|
}
|
|
348
419
|
}
|
|
349
420
|
startFlushTimer() {
|
|
350
421
|
this.flushTimer = setInterval(() => {
|
|
351
|
-
this.flush()
|
|
422
|
+
this.flush().catch((err) => {
|
|
423
|
+
console.error("[MonitorClient] Scheduled flush failed:", err);
|
|
424
|
+
});
|
|
352
425
|
}, this.flushIntervalMs);
|
|
353
426
|
}
|
|
354
427
|
stopFlushTimer() {
|
|
@@ -393,14 +466,7 @@ var MonitorClient = class {
|
|
|
393
466
|
const baseDir = process.cwd();
|
|
394
467
|
const resolvedPath = path.isAbsolute(packagePath) ? packagePath : path.join(baseDir, packagePath);
|
|
395
468
|
const normalizedPath = path.normalize(resolvedPath);
|
|
396
|
-
|
|
397
|
-
if (!path.isAbsolute(packagePath)) {
|
|
398
|
-
if (!normalizedPath.startsWith(normalizedBase)) {
|
|
399
|
-
console.warn("[MonitorClient] Path traversal attempt blocked:", packagePath);
|
|
400
|
-
return [];
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (packagePath.includes("\0") || /\.\.[\\/]/.test(packagePath)) {
|
|
469
|
+
if (packagePath.includes("\0")) {
|
|
404
470
|
console.warn("[MonitorClient] Suspicious path blocked:", packagePath);
|
|
405
471
|
return [];
|
|
406
472
|
}
|
|
@@ -411,6 +477,15 @@ var MonitorClient = class {
|
|
|
411
477
|
if (!fs.existsSync(normalizedPath)) {
|
|
412
478
|
return [];
|
|
413
479
|
}
|
|
480
|
+
try {
|
|
481
|
+
const realPath = fs.realpathSync(normalizedPath);
|
|
482
|
+
if (!realPath.endsWith("package.json")) {
|
|
483
|
+
console.warn("[MonitorClient] Symlink points to non-package.json file:", packagePath);
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
414
489
|
const packageJson = JSON.parse(fs.readFileSync(normalizedPath, "utf-8"));
|
|
415
490
|
const technologies = [];
|
|
416
491
|
const deps = {
|
|
@@ -434,9 +509,8 @@ var MonitorClient = class {
|
|
|
434
509
|
}
|
|
435
510
|
}
|
|
436
511
|
shouldExclude(packageName) {
|
|
437
|
-
for (const
|
|
438
|
-
|
|
439
|
-
if (new RegExp(`^${regexPattern}$`).test(packageName)) {
|
|
512
|
+
for (const regex of this.compiledExcludePatterns) {
|
|
513
|
+
if (regex.test(packageName)) {
|
|
440
514
|
return true;
|
|
441
515
|
}
|
|
442
516
|
}
|
|
@@ -459,7 +533,7 @@ var MonitorClient = class {
|
|
|
459
533
|
});
|
|
460
534
|
if (!response.ok) {
|
|
461
535
|
const errorText = await response.text().catch(() => "");
|
|
462
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
536
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
463
537
|
}
|
|
464
538
|
}
|
|
465
539
|
/**
|
|
@@ -470,6 +544,7 @@ var MonitorClient = class {
|
|
|
470
544
|
if (this.isClosed) return {};
|
|
471
545
|
const payload = {
|
|
472
546
|
...input,
|
|
547
|
+
metadata: this.sanitizeMetadata(input.metadata),
|
|
473
548
|
environment: this.environment
|
|
474
549
|
};
|
|
475
550
|
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security`, {
|
|
@@ -482,7 +557,7 @@ var MonitorClient = class {
|
|
|
482
557
|
});
|
|
483
558
|
if (!response.ok) {
|
|
484
559
|
const errorText = await response.text().catch(() => "");
|
|
485
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
560
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
486
561
|
}
|
|
487
562
|
const result = await response.json();
|
|
488
563
|
return { warning: result.warning };
|
|
@@ -561,7 +636,7 @@ var MonitorClient = class {
|
|
|
561
636
|
});
|
|
562
637
|
if (!response.ok) {
|
|
563
638
|
const errorText = await response.text().catch(() => "");
|
|
564
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
639
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
565
640
|
}
|
|
566
641
|
const result = await response.json();
|
|
567
642
|
return result.data;
|
|
@@ -602,6 +677,12 @@ var MonitorClient = class {
|
|
|
602
677
|
return null;
|
|
603
678
|
}
|
|
604
679
|
projectPath = path.resolve(projectPath);
|
|
680
|
+
try {
|
|
681
|
+
projectPath = fs.realpathSync(projectPath);
|
|
682
|
+
} catch {
|
|
683
|
+
console.error("[MonitorClient] projectPath does not exist or is inaccessible");
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
605
686
|
try {
|
|
606
687
|
const stats = fs.statSync(projectPath);
|
|
607
688
|
if (!stats.isDirectory()) {
|
|
@@ -612,21 +693,19 @@ var MonitorClient = class {
|
|
|
612
693
|
console.error("[MonitorClient] projectPath does not exist");
|
|
613
694
|
return null;
|
|
614
695
|
}
|
|
615
|
-
const packageJsonPath = path.join(projectPath, "package.json");
|
|
616
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
617
|
-
console.error("[MonitorClient] No package.json found in projectPath");
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
696
|
let auditOutput;
|
|
621
697
|
try {
|
|
698
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
699
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
700
|
+
console.error("[MonitorClient] No package.json found in projectPath");
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
622
703
|
auditOutput = execSync("npm audit --json", {
|
|
623
704
|
cwd: projectPath,
|
|
624
705
|
encoding: "utf-8",
|
|
625
706
|
stdio: ["pipe", "pipe", "pipe"],
|
|
626
|
-
maxBuffer:
|
|
627
|
-
|
|
628
|
-
timeout: 6e4
|
|
629
|
-
// 60 second timeout
|
|
707
|
+
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
|
|
708
|
+
timeout: CONFIG_LIMITS.AUDIT_TIMEOUT_MS
|
|
630
709
|
});
|
|
631
710
|
} catch (err) {
|
|
632
711
|
const execError = err;
|
|
@@ -668,6 +747,63 @@ var MonitorClient = class {
|
|
|
668
747
|
return null;
|
|
669
748
|
}
|
|
670
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Run npm audit on multiple directories and send results to the monitoring server.
|
|
752
|
+
* This is useful for monorepos or projects with separate client/server directories.
|
|
753
|
+
*
|
|
754
|
+
* Uses the auditPaths configuration to determine which directories to scan.
|
|
755
|
+
* Each path is scanned independently and results are tagged with their environment label.
|
|
756
|
+
*
|
|
757
|
+
* @returns Combined summary of all audit results
|
|
758
|
+
*/
|
|
759
|
+
async auditMultiplePaths() {
|
|
760
|
+
if (!this.auditPaths || this.auditPaths.length === 0) {
|
|
761
|
+
console.warn("[MonitorClient] No auditPaths configured");
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const results = [];
|
|
765
|
+
const totalSummary = {
|
|
766
|
+
critical: 0,
|
|
767
|
+
high: 0,
|
|
768
|
+
moderate: 0,
|
|
769
|
+
low: 0,
|
|
770
|
+
info: 0
|
|
771
|
+
};
|
|
772
|
+
for (const auditPath of this.auditPaths) {
|
|
773
|
+
console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${auditPath.path}`);
|
|
774
|
+
try {
|
|
775
|
+
const summary = await this.auditDependencies({
|
|
776
|
+
projectPath: auditPath.path,
|
|
777
|
+
environment: auditPath.environment
|
|
778
|
+
});
|
|
779
|
+
if (summary) {
|
|
780
|
+
results.push({
|
|
781
|
+
environment: auditPath.environment,
|
|
782
|
+
scanId: summary.scanId,
|
|
783
|
+
processed: summary.processed,
|
|
784
|
+
resolved: summary.resolved,
|
|
785
|
+
summary: summary.summary
|
|
786
|
+
});
|
|
787
|
+
totalSummary.critical += summary.summary.critical;
|
|
788
|
+
totalSummary.high += summary.summary.high;
|
|
789
|
+
totalSummary.moderate += summary.summary.moderate;
|
|
790
|
+
totalSummary.low += summary.summary.low;
|
|
791
|
+
totalSummary.info += summary.summary.info;
|
|
792
|
+
}
|
|
793
|
+
} catch (err) {
|
|
794
|
+
console.error(`[MonitorClient] Failed to audit ${auditPath.environment}:`, err instanceof Error ? err.message : String(err));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (results.length === 0) {
|
|
798
|
+
console.warn("[MonitorClient] No audit results collected from any path");
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
console.log(`[MonitorClient] Multi-path audit complete: ${results.length} paths scanned`);
|
|
802
|
+
return {
|
|
803
|
+
results,
|
|
804
|
+
totalSummary
|
|
805
|
+
};
|
|
806
|
+
}
|
|
671
807
|
/**
|
|
672
808
|
* Parse npm audit JSON output into vulnerability items
|
|
673
809
|
*/
|
|
@@ -677,6 +813,9 @@ var MonitorClient = class {
|
|
|
677
813
|
return vulnerabilities;
|
|
678
814
|
}
|
|
679
815
|
for (const [packageName, vuln] of Object.entries(auditData.vulnerabilities)) {
|
|
816
|
+
if (!vuln.via || !Array.isArray(vuln.via)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
680
819
|
const viaDetails = vuln.via.find(
|
|
681
820
|
(v) => typeof v === "object" && "title" in v
|
|
682
821
|
);
|
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 = [];
|
|
@@ -37,14 +56,14 @@ var MonitorClient = class {
|
|
|
37
56
|
this.apiKey = config.apiKey;
|
|
38
57
|
this.endpoint = config.endpoint.replace(/\/$/, "");
|
|
39
58
|
this.environment = config.environment || "production";
|
|
40
|
-
this.batchSize = Math.max(1, config.batchSize || 10);
|
|
41
|
-
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));
|
|
42
61
|
this.trackDependencies = config.trackDependencies || false;
|
|
43
62
|
this.packageJsonPath = config.packageJsonPath;
|
|
44
63
|
this.dependencySources = config.dependencySources;
|
|
45
|
-
this.maxQueueSize = config.maxQueueSize || 1e3;
|
|
46
|
-
this.maxRetries = config.maxRetries || 3;
|
|
47
|
-
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);
|
|
48
67
|
const defaultExcludePatterns = [
|
|
49
68
|
"@types/*",
|
|
50
69
|
"eslint*",
|
|
@@ -55,7 +74,12 @@ var MonitorClient = class {
|
|
|
55
74
|
"@typescript-eslint/*"
|
|
56
75
|
];
|
|
57
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
|
+
});
|
|
58
81
|
this.autoAudit = config.autoAudit || false;
|
|
82
|
+
this.auditPaths = config.auditPaths;
|
|
59
83
|
this.startFlushTimer();
|
|
60
84
|
if (this.trackDependencies) {
|
|
61
85
|
this.syncDependencies().catch((err) => {
|
|
@@ -68,6 +92,36 @@ var MonitorClient = class {
|
|
|
68
92
|
});
|
|
69
93
|
}
|
|
70
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
|
+
}
|
|
71
125
|
async captureError(error, context) {
|
|
72
126
|
if (this.isClosed) return;
|
|
73
127
|
const payload = {
|
|
@@ -81,7 +135,7 @@ var MonitorClient = class {
|
|
|
81
135
|
userAgent: context?.userAgent,
|
|
82
136
|
ip: context?.ip,
|
|
83
137
|
requestId: context?.requestId,
|
|
84
|
-
metadata: context?.metadata
|
|
138
|
+
metadata: this.sanitizeMetadata(context?.metadata)
|
|
85
139
|
};
|
|
86
140
|
this.enqueue(payload);
|
|
87
141
|
}
|
|
@@ -97,7 +151,7 @@ var MonitorClient = class {
|
|
|
97
151
|
userAgent: context?.userAgent,
|
|
98
152
|
ip: context?.ip,
|
|
99
153
|
requestId: context?.requestId,
|
|
100
|
-
metadata: context?.metadata
|
|
154
|
+
metadata: this.sanitizeMetadata(context?.metadata)
|
|
101
155
|
};
|
|
102
156
|
this.enqueue(payload);
|
|
103
157
|
}
|
|
@@ -121,6 +175,12 @@ var MonitorClient = class {
|
|
|
121
175
|
const key = this.getErrorKey(error);
|
|
122
176
|
const retries = this.retryCount.get(key) || 0;
|
|
123
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
|
+
}
|
|
124
184
|
this.retryCount.set(key, retries + 1);
|
|
125
185
|
if (this.queue.length < this.maxQueueSize) {
|
|
126
186
|
this.queue.push(error);
|
|
@@ -205,20 +265,29 @@ var MonitorClient = class {
|
|
|
205
265
|
await this.runScanAndTrackTime();
|
|
206
266
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
207
267
|
this.auditIntervalTimer = setInterval(() => {
|
|
208
|
-
this.runScanAndTrackTime()
|
|
268
|
+
this.runScanAndTrackTime().catch((err) => {
|
|
269
|
+
console.error("[MonitorClient] Auto audit scan failed:", err);
|
|
270
|
+
});
|
|
209
271
|
}, intervalMs);
|
|
210
272
|
}
|
|
211
273
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
212
274
|
this.settingsPollingTimer = setInterval(() => {
|
|
213
|
-
this.checkForScanRequest()
|
|
214
|
-
|
|
275
|
+
this.checkForScanRequest().catch((err) => {
|
|
276
|
+
console.error("[MonitorClient] Scan request check failed:", err);
|
|
277
|
+
});
|
|
278
|
+
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
215
279
|
}
|
|
216
280
|
/**
|
|
217
281
|
* Run a vulnerability scan and track the time it was run.
|
|
282
|
+
* Uses auditMultiplePaths() if auditPaths is configured, otherwise runs single audit.
|
|
218
283
|
*/
|
|
219
284
|
async runScanAndTrackTime() {
|
|
220
285
|
try {
|
|
221
|
-
|
|
286
|
+
if (this.auditPaths && this.auditPaths.length > 0) {
|
|
287
|
+
await this.auditMultiplePaths();
|
|
288
|
+
} else {
|
|
289
|
+
await this.auditDependencies();
|
|
290
|
+
}
|
|
222
291
|
this.lastScanTime = /* @__PURE__ */ new Date();
|
|
223
292
|
} catch (err) {
|
|
224
293
|
console.error("[MonitorClient] Vulnerability scan failed:", err instanceof Error ? err.message : String(err));
|
|
@@ -258,7 +327,9 @@ var MonitorClient = class {
|
|
|
258
327
|
}
|
|
259
328
|
this.queue.push(payload);
|
|
260
329
|
if (this.queue.length >= this.batchSize) {
|
|
261
|
-
this.flush()
|
|
330
|
+
this.flush().catch((err) => {
|
|
331
|
+
console.error("[MonitorClient] Flush failed:", err);
|
|
332
|
+
});
|
|
262
333
|
}
|
|
263
334
|
}
|
|
264
335
|
/**
|
|
@@ -293,7 +364,7 @@ var MonitorClient = class {
|
|
|
293
364
|
});
|
|
294
365
|
if (!response.ok) {
|
|
295
366
|
const errorText = await response.text().catch(() => "");
|
|
296
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
367
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
297
368
|
}
|
|
298
369
|
}
|
|
299
370
|
async sendBatch(errors) {
|
|
@@ -307,12 +378,14 @@ var MonitorClient = class {
|
|
|
307
378
|
});
|
|
308
379
|
if (!response.ok) {
|
|
309
380
|
const errorText = await response.text().catch(() => "");
|
|
310
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
381
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
311
382
|
}
|
|
312
383
|
}
|
|
313
384
|
startFlushTimer() {
|
|
314
385
|
this.flushTimer = setInterval(() => {
|
|
315
|
-
this.flush()
|
|
386
|
+
this.flush().catch((err) => {
|
|
387
|
+
console.error("[MonitorClient] Scheduled flush failed:", err);
|
|
388
|
+
});
|
|
316
389
|
}, this.flushIntervalMs);
|
|
317
390
|
}
|
|
318
391
|
stopFlushTimer() {
|
|
@@ -357,14 +430,7 @@ var MonitorClient = class {
|
|
|
357
430
|
const baseDir = process.cwd();
|
|
358
431
|
const resolvedPath = path.isAbsolute(packagePath) ? packagePath : path.join(baseDir, packagePath);
|
|
359
432
|
const normalizedPath = path.normalize(resolvedPath);
|
|
360
|
-
|
|
361
|
-
if (!path.isAbsolute(packagePath)) {
|
|
362
|
-
if (!normalizedPath.startsWith(normalizedBase)) {
|
|
363
|
-
console.warn("[MonitorClient] Path traversal attempt blocked:", packagePath);
|
|
364
|
-
return [];
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (packagePath.includes("\0") || /\.\.[\\/]/.test(packagePath)) {
|
|
433
|
+
if (packagePath.includes("\0")) {
|
|
368
434
|
console.warn("[MonitorClient] Suspicious path blocked:", packagePath);
|
|
369
435
|
return [];
|
|
370
436
|
}
|
|
@@ -375,6 +441,15 @@ var MonitorClient = class {
|
|
|
375
441
|
if (!fs.existsSync(normalizedPath)) {
|
|
376
442
|
return [];
|
|
377
443
|
}
|
|
444
|
+
try {
|
|
445
|
+
const realPath = fs.realpathSync(normalizedPath);
|
|
446
|
+
if (!realPath.endsWith("package.json")) {
|
|
447
|
+
console.warn("[MonitorClient] Symlink points to non-package.json file:", packagePath);
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
378
453
|
const packageJson = JSON.parse(fs.readFileSync(normalizedPath, "utf-8"));
|
|
379
454
|
const technologies = [];
|
|
380
455
|
const deps = {
|
|
@@ -398,9 +473,8 @@ var MonitorClient = class {
|
|
|
398
473
|
}
|
|
399
474
|
}
|
|
400
475
|
shouldExclude(packageName) {
|
|
401
|
-
for (const
|
|
402
|
-
|
|
403
|
-
if (new RegExp(`^${regexPattern}$`).test(packageName)) {
|
|
476
|
+
for (const regex of this.compiledExcludePatterns) {
|
|
477
|
+
if (regex.test(packageName)) {
|
|
404
478
|
return true;
|
|
405
479
|
}
|
|
406
480
|
}
|
|
@@ -423,7 +497,7 @@ var MonitorClient = class {
|
|
|
423
497
|
});
|
|
424
498
|
if (!response.ok) {
|
|
425
499
|
const errorText = await response.text().catch(() => "");
|
|
426
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
500
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
427
501
|
}
|
|
428
502
|
}
|
|
429
503
|
/**
|
|
@@ -434,6 +508,7 @@ var MonitorClient = class {
|
|
|
434
508
|
if (this.isClosed) return {};
|
|
435
509
|
const payload = {
|
|
436
510
|
...input,
|
|
511
|
+
metadata: this.sanitizeMetadata(input.metadata),
|
|
437
512
|
environment: this.environment
|
|
438
513
|
};
|
|
439
514
|
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/security`, {
|
|
@@ -446,7 +521,7 @@ var MonitorClient = class {
|
|
|
446
521
|
});
|
|
447
522
|
if (!response.ok) {
|
|
448
523
|
const errorText = await response.text().catch(() => "");
|
|
449
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
524
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
450
525
|
}
|
|
451
526
|
const result = await response.json();
|
|
452
527
|
return { warning: result.warning };
|
|
@@ -525,7 +600,7 @@ var MonitorClient = class {
|
|
|
525
600
|
});
|
|
526
601
|
if (!response.ok) {
|
|
527
602
|
const errorText = await response.text().catch(() => "");
|
|
528
|
-
throw new Error(`HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`);
|
|
603
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
529
604
|
}
|
|
530
605
|
const result = await response.json();
|
|
531
606
|
return result.data;
|
|
@@ -566,6 +641,12 @@ var MonitorClient = class {
|
|
|
566
641
|
return null;
|
|
567
642
|
}
|
|
568
643
|
projectPath = path.resolve(projectPath);
|
|
644
|
+
try {
|
|
645
|
+
projectPath = fs.realpathSync(projectPath);
|
|
646
|
+
} catch {
|
|
647
|
+
console.error("[MonitorClient] projectPath does not exist or is inaccessible");
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
569
650
|
try {
|
|
570
651
|
const stats = fs.statSync(projectPath);
|
|
571
652
|
if (!stats.isDirectory()) {
|
|
@@ -576,21 +657,19 @@ var MonitorClient = class {
|
|
|
576
657
|
console.error("[MonitorClient] projectPath does not exist");
|
|
577
658
|
return null;
|
|
578
659
|
}
|
|
579
|
-
const packageJsonPath = path.join(projectPath, "package.json");
|
|
580
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
581
|
-
console.error("[MonitorClient] No package.json found in projectPath");
|
|
582
|
-
return null;
|
|
583
|
-
}
|
|
584
660
|
let auditOutput;
|
|
585
661
|
try {
|
|
662
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
663
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
664
|
+
console.error("[MonitorClient] No package.json found in projectPath");
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
586
667
|
auditOutput = execSync("npm audit --json", {
|
|
587
668
|
cwd: projectPath,
|
|
588
669
|
encoding: "utf-8",
|
|
589
670
|
stdio: ["pipe", "pipe", "pipe"],
|
|
590
|
-
maxBuffer:
|
|
591
|
-
|
|
592
|
-
timeout: 6e4
|
|
593
|
-
// 60 second timeout
|
|
671
|
+
maxBuffer: CONFIG_LIMITS.AUDIT_MAX_BUFFER,
|
|
672
|
+
timeout: CONFIG_LIMITS.AUDIT_TIMEOUT_MS
|
|
594
673
|
});
|
|
595
674
|
} catch (err) {
|
|
596
675
|
const execError = err;
|
|
@@ -632,6 +711,63 @@ var MonitorClient = class {
|
|
|
632
711
|
return null;
|
|
633
712
|
}
|
|
634
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Run npm audit on multiple directories and send results to the monitoring server.
|
|
716
|
+
* This is useful for monorepos or projects with separate client/server directories.
|
|
717
|
+
*
|
|
718
|
+
* Uses the auditPaths configuration to determine which directories to scan.
|
|
719
|
+
* Each path is scanned independently and results are tagged with their environment label.
|
|
720
|
+
*
|
|
721
|
+
* @returns Combined summary of all audit results
|
|
722
|
+
*/
|
|
723
|
+
async auditMultiplePaths() {
|
|
724
|
+
if (!this.auditPaths || this.auditPaths.length === 0) {
|
|
725
|
+
console.warn("[MonitorClient] No auditPaths configured");
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
const results = [];
|
|
729
|
+
const totalSummary = {
|
|
730
|
+
critical: 0,
|
|
731
|
+
high: 0,
|
|
732
|
+
moderate: 0,
|
|
733
|
+
low: 0,
|
|
734
|
+
info: 0
|
|
735
|
+
};
|
|
736
|
+
for (const auditPath of this.auditPaths) {
|
|
737
|
+
console.log(`[MonitorClient] Auditing ${auditPath.environment} at ${auditPath.path}`);
|
|
738
|
+
try {
|
|
739
|
+
const summary = await this.auditDependencies({
|
|
740
|
+
projectPath: auditPath.path,
|
|
741
|
+
environment: auditPath.environment
|
|
742
|
+
});
|
|
743
|
+
if (summary) {
|
|
744
|
+
results.push({
|
|
745
|
+
environment: auditPath.environment,
|
|
746
|
+
scanId: summary.scanId,
|
|
747
|
+
processed: summary.processed,
|
|
748
|
+
resolved: summary.resolved,
|
|
749
|
+
summary: summary.summary
|
|
750
|
+
});
|
|
751
|
+
totalSummary.critical += summary.summary.critical;
|
|
752
|
+
totalSummary.high += summary.summary.high;
|
|
753
|
+
totalSummary.moderate += summary.summary.moderate;
|
|
754
|
+
totalSummary.low += summary.summary.low;
|
|
755
|
+
totalSummary.info += summary.summary.info;
|
|
756
|
+
}
|
|
757
|
+
} catch (err) {
|
|
758
|
+
console.error(`[MonitorClient] Failed to audit ${auditPath.environment}:`, err instanceof Error ? err.message : String(err));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (results.length === 0) {
|
|
762
|
+
console.warn("[MonitorClient] No audit results collected from any path");
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
console.log(`[MonitorClient] Multi-path audit complete: ${results.length} paths scanned`);
|
|
766
|
+
return {
|
|
767
|
+
results,
|
|
768
|
+
totalSummary
|
|
769
|
+
};
|
|
770
|
+
}
|
|
635
771
|
/**
|
|
636
772
|
* Parse npm audit JSON output into vulnerability items
|
|
637
773
|
*/
|
|
@@ -641,6 +777,9 @@ var MonitorClient = class {
|
|
|
641
777
|
return vulnerabilities;
|
|
642
778
|
}
|
|
643
779
|
for (const [packageName, vuln] of Object.entries(auditData.vulnerabilities)) {
|
|
780
|
+
if (!vuln.via || !Array.isArray(vuln.via)) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
644
783
|
const viaDetails = vuln.via.find(
|
|
645
784
|
(v) => typeof v === "object" && "title" in v
|
|
646
785
|
);
|
package/package.json
CHANGED