verdaccio-security-filter 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/build/index.js +27 -3
- package/build/lib/cve-checker.d.ts +4 -0
- package/build/lib/cve-checker.js +65 -35
- package/build/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Advanced security filter plugin for Verdaccio with **dual-layer protection architecture** combining middleware interception and metadata filtering for comprehensive package security.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/verdaccio-security-filter)
|
|
6
|
+
[](https://www.npmjs.com/package/verdaccio-security-filter)
|
|
7
|
+
[](https://github.com/ponomarenko/verdaccio-security-filter/actions/workflows/ci.yml)
|
|
5
8
|
[](./tests)
|
|
6
9
|
[]()
|
|
7
|
-
[]()
|
|
8
11
|
[]()
|
|
9
12
|
|
|
10
13
|
## 🏗️ Dual-Layer Architecture
|
|
@@ -76,6 +79,20 @@ This plugin implements **two independent security layers** for complete protecti
|
|
|
76
79
|
- **Metadata Validation** - Verify package integrity and detect dangerous characters
|
|
77
80
|
- **Checksum Enforcement** - Ensure package integrity
|
|
78
81
|
|
|
82
|
+
#### ⚙️ Reliability & Error Handling
|
|
83
|
+
- **Fail-Safe/Fail-Closed Configuration** - Choose error handling strategy
|
|
84
|
+
- **Fail-Open (Default)** - Allow packages on errors (availability priority)
|
|
85
|
+
- **Fail-Closed** - Block packages on errors (security priority)
|
|
86
|
+
- **Granular Control** - Different strategies for filter, CVE, and license checks
|
|
87
|
+
- **Retry Logic** - Automatic retry with exponential backoff for API calls
|
|
88
|
+
- 3 retries with increasing delays (1s, 2s, 4s)
|
|
89
|
+
- Rate limiting detection and handling
|
|
90
|
+
- Request timeout protection (10s)
|
|
91
|
+
- **Resilient CVE Scanning** - Production-ready vulnerability checking
|
|
92
|
+
- Network failure recovery
|
|
93
|
+
- API timeout protection
|
|
94
|
+
- Graceful degradation on errors
|
|
95
|
+
|
|
79
96
|
### 🔮 Planned Features (Roadmap)
|
|
80
97
|
|
|
81
98
|
#### Phase 3: Advanced Security
|
|
@@ -450,6 +467,39 @@ middlewares:
|
|
|
450
467
|
| `packageAge.minVersionAgeDays` | `number` | `undefined` | Minimum age for versions (days) |
|
|
451
468
|
| `packageAge.warnOnly` | `boolean` | `false` | Only warn, don't block |
|
|
452
469
|
|
|
470
|
+
### Error Handling Options
|
|
471
|
+
|
|
472
|
+
| Option | Type | Default | Description |
|
|
473
|
+
|--------|------|---------|-------------|
|
|
474
|
+
| `errorHandling.onFilterError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when filter errors occur |
|
|
475
|
+
| `errorHandling.onCveCheckError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when CVE check fails |
|
|
476
|
+
| `errorHandling.onLicenseCheckError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when license check fails |
|
|
477
|
+
|
|
478
|
+
**Error Handling Strategies:**
|
|
479
|
+
- **fail-open** (Default): Allow packages through on errors (prioritizes availability)
|
|
480
|
+
- **fail-closed**: Block packages on errors (prioritizes security)
|
|
481
|
+
|
|
482
|
+
**Example Configuration:**
|
|
483
|
+
```yaml
|
|
484
|
+
# Production: Security-first approach
|
|
485
|
+
errorHandling:
|
|
486
|
+
onFilterError: fail-closed
|
|
487
|
+
onCveCheckError: fail-closed
|
|
488
|
+
onLicenseCheckError: fail-closed
|
|
489
|
+
|
|
490
|
+
# Development: Availability-first approach
|
|
491
|
+
errorHandling:
|
|
492
|
+
onFilterError: fail-open
|
|
493
|
+
onCveCheckError: fail-open
|
|
494
|
+
onLicenseCheckError: fail-open
|
|
495
|
+
|
|
496
|
+
# Balanced: Block only on license failures
|
|
497
|
+
errorHandling:
|
|
498
|
+
onFilterError: fail-open
|
|
499
|
+
onCveCheckError: fail-open
|
|
500
|
+
onLicenseCheckError: fail-closed
|
|
501
|
+
```
|
|
502
|
+
|
|
453
503
|
### Version Range Rules
|
|
454
504
|
|
|
455
505
|
```typescript
|
package/build/index.js
CHANGED
|
@@ -70,6 +70,14 @@ class SecurityFilterPlugin {
|
|
|
70
70
|
*/
|
|
71
71
|
constructor(config, options) {
|
|
72
72
|
this.config = config || {};
|
|
73
|
+
// Set default error handling strategy if not provided
|
|
74
|
+
if (!this.config.errorHandling) {
|
|
75
|
+
this.config.errorHandling = {
|
|
76
|
+
onFilterError: 'fail-open',
|
|
77
|
+
onCveCheckError: 'fail-open',
|
|
78
|
+
onLicenseCheckError: 'fail-open',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
73
81
|
// Initialize enhanced logger
|
|
74
82
|
this.logger = new logger_1.SecurityLogger(options.logger, this.config.logger);
|
|
75
83
|
// Initialize metrics collector
|
|
@@ -223,6 +231,7 @@ class SecurityFilterPlugin {
|
|
|
223
231
|
* Register Express middleware to intercept package requests
|
|
224
232
|
* Intercepts both metadata and tarball requests
|
|
225
233
|
*/
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
226
235
|
register_middlewares(app, _auth, _storage) {
|
|
227
236
|
this.logger.info('[Middleware] Registering security filter middleware');
|
|
228
237
|
this.logger.info(`[Middleware] Config mode: ${this.config.mode || 'NOT SET'}`);
|
|
@@ -275,6 +284,7 @@ class SecurityFilterPlugin {
|
|
|
275
284
|
// Intercept the response
|
|
276
285
|
const originalSend = res.send;
|
|
277
286
|
const originalJson = res.json;
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
278
288
|
const self = this;
|
|
279
289
|
// Override res.json to intercept JSON responses
|
|
280
290
|
res.json = function (body) {
|
|
@@ -338,7 +348,7 @@ class SecurityFilterPlugin {
|
|
|
338
348
|
return originalSend.call(this, JSON.stringify(blockedResponse));
|
|
339
349
|
}
|
|
340
350
|
}
|
|
341
|
-
catch (
|
|
351
|
+
catch (_a) {
|
|
342
352
|
// Not JSON, pass through
|
|
343
353
|
}
|
|
344
354
|
}
|
|
@@ -365,7 +375,7 @@ class SecurityFilterPlugin {
|
|
|
365
375
|
*/
|
|
366
376
|
filter_metadata(packageInfo) {
|
|
367
377
|
return __awaiter(this, void 0, void 0, function* () {
|
|
368
|
-
var _a, _b, _c, _d;
|
|
378
|
+
var _a, _b, _c, _d, _e;
|
|
369
379
|
const packageName = packageInfo.name;
|
|
370
380
|
this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
|
|
371
381
|
try {
|
|
@@ -463,7 +473,21 @@ class SecurityFilterPlugin {
|
|
|
463
473
|
}
|
|
464
474
|
catch (error) {
|
|
465
475
|
this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
|
|
466
|
-
|
|
476
|
+
const errorStrategy = ((_e = this.config.errorHandling) === null || _e === void 0 ? void 0 : _e.onFilterError) || 'fail-open';
|
|
477
|
+
if (errorStrategy === 'fail-closed') {
|
|
478
|
+
this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
|
|
479
|
+
this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
|
|
480
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
481
|
+
blocked: true,
|
|
482
|
+
reason: `Error during security processing (fail-closed mode): ${error.message}`,
|
|
483
|
+
plugin: {
|
|
484
|
+
name: 'verdaccio-security-filter',
|
|
485
|
+
version: '2.0.0',
|
|
486
|
+
},
|
|
487
|
+
blockedAt: new Date().toISOString(),
|
|
488
|
+
} });
|
|
489
|
+
}
|
|
490
|
+
this.logger.warn(`[filter_metadata] Error handling: fail-open - ALLOWING ${packageName}`);
|
|
467
491
|
return packageInfo;
|
|
468
492
|
}
|
|
469
493
|
});
|
package/build/lib/cve-checker.js
CHANGED
|
@@ -134,47 +134,77 @@ class CVEChecker {
|
|
|
134
134
|
*/
|
|
135
135
|
fetchFromOSV(packageName, version) {
|
|
136
136
|
return __awaiter(this, void 0, void 0, function* () {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
const maxRetries = 3;
|
|
138
|
+
const retryDelay = 1000;
|
|
139
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
140
|
+
try {
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
143
|
+
const response = yield fetch('https://api.osv.dev/v1/query', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
147
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
package: {
|
|
150
|
+
name: packageName,
|
|
151
|
+
ecosystem: 'npm',
|
|
152
|
+
},
|
|
153
|
+
version: version,
|
|
154
|
+
}),
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
});
|
|
157
|
+
clearTimeout(timeoutId);
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
if (response.status === 429 && attempt < maxRetries) {
|
|
160
|
+
const waitTime = retryDelay * Math.pow(2, attempt - 1);
|
|
161
|
+
console.warn(`[CVE Checker] Rate limited, retrying in ${waitTime}ms (attempt ${attempt}/${maxRetries})`);
|
|
162
|
+
yield this.sleep(waitTime);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`OSV API returned ${response.status}`);
|
|
166
|
+
}
|
|
167
|
+
const data = yield response.json();
|
|
168
|
+
if (!data.vulns || data.vulns.length === 0) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
return data.vulns.map((vuln) => {
|
|
172
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
173
|
+
return ({
|
|
174
|
+
id: vuln.id,
|
|
175
|
+
severity: this.mapOSVSeverity(vuln.severity),
|
|
176
|
+
summary: vuln.summary || vuln.details || 'No description available',
|
|
177
|
+
affectedVersions: ((_a = vuln.affected) === null || _a === void 0 ? void 0 : _a.map((a) => { var _a; return (_a = a.ranges) === null || _a === void 0 ? void 0 : _a.map((r) => { var _a; return (_a = r.events) === null || _a === void 0 ? void 0 : _a.map((e) => e.introduced || e.fixed).filter(Boolean); }).flat(); }).flat()) || [],
|
|
178
|
+
fixedVersion: (_g = (_f = (_e = (_d = (_c = (_b = vuln.affected) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.ranges) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.events) === null || _f === void 0 ? void 0 : _f.find((e) => e.fixed)) === null || _g === void 0 ? void 0 : _g.fixed,
|
|
179
|
+
publishedDate: vuln.published || new Date().toISOString(),
|
|
180
|
+
source: 'osv',
|
|
181
|
+
});
|
|
182
|
+
});
|
|
153
183
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
if (attempt === maxRetries) {
|
|
187
|
+
console.error(`[CVE Checker] OSV API error after ${maxRetries} attempts:`, message);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
191
|
+
console.warn(`[CVE Checker] Request timeout, retrying (attempt ${attempt}/${maxRetries})`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.warn(`[CVE Checker] Request failed, retrying (attempt ${attempt}/${maxRetries}):`, message);
|
|
195
|
+
}
|
|
196
|
+
yield this.sleep(retryDelay * attempt);
|
|
157
197
|
}
|
|
158
|
-
return data.vulns.map((vuln) => {
|
|
159
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
160
|
-
return ({
|
|
161
|
-
id: vuln.id,
|
|
162
|
-
severity: this.mapOSVSeverity(vuln.severity),
|
|
163
|
-
summary: vuln.summary || vuln.details || 'No description available',
|
|
164
|
-
affectedVersions: ((_a = vuln.affected) === null || _a === void 0 ? void 0 : _a.map((a) => { var _a; return (_a = a.ranges) === null || _a === void 0 ? void 0 : _a.map((r) => { var _a; return (_a = r.events) === null || _a === void 0 ? void 0 : _a.map((e) => e.introduced || e.fixed).filter(Boolean); }).flat(); }).flat()) || [],
|
|
165
|
-
fixedVersion: (_g = (_f = (_e = (_d = (_c = (_b = vuln.affected) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.ranges) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.events) === null || _f === void 0 ? void 0 : _f.find((e) => e.fixed)) === null || _g === void 0 ? void 0 : _g.fixed,
|
|
166
|
-
publishedDate: vuln.published || new Date().toISOString(),
|
|
167
|
-
source: 'osv',
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
-
console.error('[CVE Checker] OSV API error:', message);
|
|
174
|
-
return [];
|
|
175
198
|
}
|
|
199
|
+
return [];
|
|
176
200
|
});
|
|
177
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Sleep utility for retry delays
|
|
204
|
+
*/
|
|
205
|
+
sleep(ms) {
|
|
206
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
207
|
+
}
|
|
178
208
|
/**
|
|
179
209
|
* Map OSV severity to our severity levels
|
|
180
210
|
*/
|
package/build/types.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface SecurityConfig extends Config {
|
|
|
61
61
|
logger?: LoggerConfig;
|
|
62
62
|
metrics?: MetricsConfig;
|
|
63
63
|
packageAge?: PackageAgeConfig;
|
|
64
|
+
errorHandling?: {
|
|
65
|
+
onFilterError?: 'fail-open' | 'fail-closed';
|
|
66
|
+
onCveCheckError?: 'fail-open' | 'fail-closed';
|
|
67
|
+
onLicenseCheckError?: 'fail-open' | 'fail-closed';
|
|
68
|
+
};
|
|
64
69
|
}
|
|
65
70
|
export interface SecurityRules {
|
|
66
71
|
blockedVersions: string[];
|
package/package.json
CHANGED