verdaccio-security-filter 1.0.6 → 1.2.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 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
+ [![npm version](https://img.shields.io/npm/v/verdaccio-security-filter.svg)](https://www.npmjs.com/package/verdaccio-security-filter)
6
+ [![npm downloads](https://img.shields.io/npm/dm/verdaccio-security-filter.svg)](https://www.npmjs.com/package/verdaccio-security-filter)
7
+ [![CI](https://github.com/ponomarenko/verdaccio-security-filter/actions/workflows/ci.yml/badge.svg)](https://github.com/ponomarenko/verdaccio-security-filter/actions/workflows/ci.yml)
5
8
  [![Tests](https://img.shields.io/badge/tests-71%20passing-brightgreen)](./tests)
6
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)]()
7
- [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)]()
10
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)]()
8
11
  [![Verdaccio](https://img.shields.io/badge/Verdaccio-6.x%20%7C%207.x-orange)]()
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 (e) {
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
- // On error, allow package through but log the issue
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
  });
@@ -20,6 +20,10 @@ export declare class CVEChecker {
20
20
  * https://osv.dev/
21
21
  */
22
22
  private fetchFromOSV;
23
+ /**
24
+ * Sleep utility for retry delays
25
+ */
26
+ private sleep;
23
27
  /**
24
28
  * Map OSV severity to our severity levels
25
29
  */
@@ -134,47 +134,77 @@ class CVEChecker {
134
134
  */
135
135
  fetchFromOSV(packageName, version) {
136
136
  return __awaiter(this, void 0, void 0, function* () {
137
- try {
138
- const response = yield fetch('https://api.osv.dev/v1/query', {
139
- method: 'POST',
140
- headers: {
141
- 'Content-Type': 'application/json',
142
- },
143
- body: JSON.stringify({
144
- package: {
145
- name: packageName,
146
- ecosystem: 'npm',
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
- version: version,
149
- }),
150
- });
151
- if (!response.ok) {
152
- throw new Error(`OSV API returned ${response.status}`);
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
- const data = yield response.json();
155
- if (!data.vulns || data.vulns.length === 0) {
156
- return [];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "Advanced security filter plugin for Verdaccio with version range blocking and fallback strategies",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",