verdaccio-security-filter 1.3.0 → 1.3.1

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,6 +2,7 @@
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
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
5
6
  [![npm version](https://img.shields.io/npm/v/verdaccio-security-filter.svg)](https://www.npmjs.com/package/verdaccio-security-filter)
6
7
  [![npm downloads](https://img.shields.io/npm/dm/verdaccio-security-filter.svg)](https://www.npmjs.com/package/verdaccio-security-filter)
7
8
  [![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)
package/build/index.d.ts CHANGED
@@ -77,6 +77,11 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
77
77
  * @private
78
78
  */
79
79
  private _getAppliedRules;
80
+ /**
81
+ * Cleanup plugin resources
82
+ * Should be called when Verdaccio shuts down
83
+ */
84
+ destroy(): void;
80
85
  }
81
86
  export { SecurityLogger } from './lib/logger';
82
87
  export { MetricsCollector } from './lib/metrics';
package/build/index.js CHANGED
@@ -290,79 +290,33 @@ class SecurityFilterPlugin {
290
290
  // Check if package should be blocked
291
291
  const blockResult = yield this._checkPackageBlock(packageName);
292
292
  if (blockResult.blocked) {
293
- // Intercept the response
294
- const originalSend = res.send;
295
- const originalJson = res.json;
296
- // eslint-disable-next-line @typescript-eslint/no-this-alias
297
- const self = this;
298
- // Override res.json to intercept JSON responses
299
- res.json = function (body) {
300
- if (body && typeof body === 'object' && body.name === packageName) {
301
- // This is package metadata - modify it to show blocked info
302
- self.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
303
- self.logger.info(`[Middleware] Modifying response to show blocking info`);
304
- const blockedResponse = {
305
- name: packageName,
306
- versions: {},
307
- 'dist-tags': {},
308
- security: {
309
- blocked: true,
310
- reason: blockResult.reason,
311
- plugin: {
312
- name: 'verdaccio-security-filter',
313
- version: '1.0.0',
314
- mode: self.config.mode || 'blacklist',
315
- },
316
- blockedAt: new Date().toISOString(),
317
- rules: self._getAppliedRules(packageName),
318
- message: `This package has been blocked by security filter: ${blockResult.reason}`,
319
- contact: 'Please contact your registry administrator for more information',
320
- },
321
- _security: {
322
- blocked: true,
323
- reason: blockResult.reason,
324
- blockedBy: 'verdaccio-security-filter',
325
- blockedAt: new Date().toISOString(),
326
- }
327
- };
328
- return originalJson.call(this, blockedResponse);
329
- }
330
- return originalJson.call(this, body);
331
- };
332
- // Override res.send for non-JSON responses
333
- res.send = function (body) {
334
- if (typeof body === 'string') {
335
- try {
336
- const parsed = JSON.parse(body);
337
- if (parsed && parsed.name === packageName) {
338
- self.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
339
- const blockedResponse = {
340
- name: packageName,
341
- versions: {},
342
- 'dist-tags': {},
343
- security: {
344
- blocked: true,
345
- reason: blockResult.reason,
346
- plugin: {
347
- name: 'verdaccio-security-filter',
348
- version: '1.0.0',
349
- mode: self.config.mode || 'blacklist',
350
- },
351
- blockedAt: new Date().toISOString(),
352
- rules: self._getAppliedRules(packageName),
353
- message: `This package has been blocked by security filter: ${blockResult.reason}`,
354
- contact: 'Please contact your registry administrator for more information',
355
- }
356
- };
357
- return originalSend.call(this, JSON.stringify(blockedResponse));
358
- }
359
- }
360
- catch (_a) {
361
- // Not JSON, pass through
362
- }
293
+ this.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
294
+ const blockedResponse = {
295
+ name: packageName,
296
+ versions: {},
297
+ 'dist-tags': {},
298
+ security: {
299
+ blocked: true,
300
+ reason: blockResult.reason,
301
+ plugin: {
302
+ name: 'verdaccio-security-filter',
303
+ version: '1.0.0',
304
+ mode: this.config.mode || 'blacklist',
305
+ },
306
+ blockedAt: new Date().toISOString(),
307
+ rules: this._getAppliedRules(packageName),
308
+ message: `This package has been blocked by security filter: ${blockResult.reason}`,
309
+ contact: 'Please contact your registry administrator for more information',
310
+ },
311
+ _security: {
312
+ blocked: true,
313
+ reason: blockResult.reason,
314
+ blockedBy: 'verdaccio-security-filter',
315
+ blockedAt: new Date().toISOString(),
363
316
  }
364
- return originalSend.call(this, body);
365
317
  };
318
+ res.status(200).json(blockedResponse);
319
+ return;
366
320
  }
367
321
  else {
368
322
  this.logger.info(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
@@ -406,16 +360,24 @@ class SecurityFilterPlugin {
406
360
  rules: this._getAppliedRules(packageName),
407
361
  } });
408
362
  }
409
- // 2. CVE Check - check all versions
363
+ // 2. CVE Check - check versions in controlled batches
410
364
  if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
411
365
  const versions = Object.keys(packageInfo.versions || {});
412
366
  const vulnerableVersions = [];
413
- for (const version of versions) {
414
- const cveResult = yield this.cveChecker.checkPackage(packageName, version);
415
- if (cveResult.isVulnerable) {
416
- vulnerableVersions.push(version);
417
- this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${cveResult.vulnerabilities.length} vulnerabilities`);
418
- }
367
+ const BATCH_SIZE = 10;
368
+ for (let i = 0; i < versions.length; i += BATCH_SIZE) {
369
+ const batch = versions.slice(i, i + BATCH_SIZE);
370
+ const results = yield Promise.allSettled(batch.map(version => this.cveChecker.checkPackage(packageName, version)));
371
+ results.forEach((result, index) => {
372
+ if (result.status === 'fulfilled' && result.value.isVulnerable) {
373
+ const version = batch[index];
374
+ vulnerableVersions.push(version);
375
+ this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${result.value.vulnerabilities.length} vulnerabilities`);
376
+ }
377
+ else if (result.status === 'rejected') {
378
+ this.logger.error(`[filter_metadata] CVE check failed for ${packageName}@${batch[index]}: ${result.reason}`);
379
+ }
380
+ });
419
381
  }
420
382
  // If configured to auto-block and vulnerabilities found
421
383
  if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
@@ -610,6 +572,21 @@ class SecurityFilterPlugin {
610
572
  }
611
573
  return rules;
612
574
  }
575
+ /**
576
+ * Cleanup plugin resources
577
+ * Should be called when Verdaccio shuts down
578
+ */
579
+ destroy() {
580
+ this.logger.info('[Cleanup] Shutting down security filter plugin');
581
+ try {
582
+ this.cveChecker.destroy();
583
+ this.metrics.cleanup();
584
+ this.logger.info('[Cleanup] Plugin resources cleaned up successfully');
585
+ }
586
+ catch (error) {
587
+ this.logger.error(`[Cleanup] Error during cleanup: ${error.message}`);
588
+ }
589
+ }
613
590
  }
614
591
  exports.default = SecurityFilterPlugin;
615
592
  // Export utility classes for testing
@@ -6,6 +6,9 @@ export declare class CVEChecker {
6
6
  private config;
7
7
  private cache;
8
8
  private lastUpdate;
9
+ private readonly MAX_CACHE_SIZE;
10
+ private saveTimer;
11
+ private readonly SAVE_DEBOUNCE_MS;
9
12
  constructor(config?: CVECheckConfig);
10
13
  /**
11
14
  * Check if a package version has known vulnerabilities
@@ -37,11 +40,19 @@ export declare class CVEChecker {
37
40
  */
38
41
  private loadCache;
39
42
  /**
40
- * Save cache to disk
43
+ * Debounced save cache to reduce I/O operations
44
+ */
45
+ private debouncedSaveCache;
46
+ /**
47
+ * Save cache to disk immediately
41
48
  */
42
49
  private saveCache;
43
50
  /**
44
51
  * Clear cache
45
52
  */
46
53
  clearCache(): void;
54
+ /**
55
+ * Cleanup resources
56
+ */
57
+ destroy(): void;
47
58
  }
@@ -45,13 +45,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.CVEChecker = void 0;
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
+ const lru_cache_1 = require("./lru-cache");
48
49
  /**
49
50
  * CVE Checker that integrates with vulnerability databases
50
51
  */
51
52
  class CVEChecker {
52
53
  constructor(config) {
53
- this.cache = new Map();
54
54
  this.lastUpdate = 0;
55
+ this.MAX_CACHE_SIZE = 1000;
56
+ this.saveTimer = null;
57
+ this.SAVE_DEBOUNCE_MS = 5000;
55
58
  this.config = {
56
59
  enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
57
60
  databases: (config === null || config === void 0 ? void 0 : config.databases) || ['osv'],
@@ -60,6 +63,7 @@ class CVEChecker {
60
63
  updateInterval: (config === null || config === void 0 ? void 0 : config.updateInterval) || 24,
61
64
  cacheDir: (config === null || config === void 0 ? void 0 : config.cacheDir) || './.security-cache',
62
65
  };
66
+ this.cache = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
63
67
  if (this.config.enabled) {
64
68
  this.loadCache();
65
69
  }
@@ -96,7 +100,7 @@ class CVEChecker {
96
100
  };
97
101
  // Cache result
98
102
  this.cache.set(cacheKey, result);
99
- this.saveCache();
103
+ this.debouncedSaveCache();
100
104
  return result;
101
105
  });
102
106
  }
@@ -243,7 +247,11 @@ class CVEChecker {
243
247
  if (fs.existsSync(cacheFile)) {
244
248
  const data = fs.readFileSync(cacheFile, 'utf8');
245
249
  const parsed = JSON.parse(data);
246
- this.cache = new Map(Object.entries(parsed.cache));
250
+ const entries = Object.entries(parsed.cache);
251
+ const sortedEntries = entries.slice(-this.MAX_CACHE_SIZE);
252
+ sortedEntries.forEach(([key, value]) => {
253
+ this.cache.set(key, value);
254
+ });
247
255
  this.lastUpdate = parsed.lastUpdate || 0;
248
256
  }
249
257
  }
@@ -253,7 +261,19 @@ class CVEChecker {
253
261
  }
254
262
  }
255
263
  /**
256
- * Save cache to disk
264
+ * Debounced save cache to reduce I/O operations
265
+ */
266
+ debouncedSaveCache() {
267
+ if (this.saveTimer) {
268
+ clearTimeout(this.saveTimer);
269
+ }
270
+ this.saveTimer = setTimeout(() => {
271
+ this.saveCache();
272
+ this.saveTimer = null;
273
+ }, this.SAVE_DEBOUNCE_MS);
274
+ }
275
+ /**
276
+ * Save cache to disk immediately
257
277
  */
258
278
  saveCache() {
259
279
  try {
@@ -263,7 +283,7 @@ class CVEChecker {
263
283
  fs.mkdirSync(dir, { recursive: true });
264
284
  }
265
285
  const data = {
266
- cache: Object.fromEntries(this.cache),
286
+ cache: Object.fromEntries(this.cache.entries()),
267
287
  lastUpdate: Date.now(),
268
288
  };
269
289
  fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
@@ -282,5 +302,16 @@ class CVEChecker {
282
302
  this.lastUpdate = 0;
283
303
  this.saveCache();
284
304
  }
305
+ /**
306
+ * Cleanup resources
307
+ */
308
+ destroy() {
309
+ if (this.saveTimer) {
310
+ clearTimeout(this.saveTimer);
311
+ this.saveTimer = null;
312
+ }
313
+ this.saveCache();
314
+ this.cache.clear();
315
+ }
285
316
  }
286
317
  exports.CVEChecker = CVEChecker;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Simple LRU (Least Recently Used) Cache implementation with size limits
3
+ */
4
+ export declare class LRUCache<K, V> {
5
+ private cache;
6
+ private readonly maxSize;
7
+ constructor(maxSize?: number);
8
+ get(key: K): V | undefined;
9
+ set(key: K, value: V): void;
10
+ has(key: K): boolean;
11
+ delete(key: K): boolean;
12
+ clear(): void;
13
+ get size(): number;
14
+ entries(): IterableIterator<[K, V]>;
15
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LRUCache = void 0;
4
+ /**
5
+ * Simple LRU (Least Recently Used) Cache implementation with size limits
6
+ */
7
+ class LRUCache {
8
+ constructor(maxSize = 1000) {
9
+ this.cache = new Map();
10
+ this.maxSize = maxSize;
11
+ }
12
+ get(key) {
13
+ if (!this.cache.has(key)) {
14
+ return undefined;
15
+ }
16
+ const value = this.cache.get(key);
17
+ this.cache.delete(key);
18
+ this.cache.set(key, value);
19
+ return value;
20
+ }
21
+ set(key, value) {
22
+ if (this.cache.has(key)) {
23
+ this.cache.delete(key);
24
+ }
25
+ else if (this.cache.size >= this.maxSize) {
26
+ const firstKey = this.cache.keys().next().value;
27
+ if (firstKey !== undefined) {
28
+ this.cache.delete(firstKey);
29
+ }
30
+ }
31
+ this.cache.set(key, value);
32
+ }
33
+ has(key) {
34
+ return this.cache.has(key);
35
+ }
36
+ delete(key) {
37
+ return this.cache.delete(key);
38
+ }
39
+ clear() {
40
+ this.cache.clear();
41
+ }
42
+ get size() {
43
+ return this.cache.size;
44
+ }
45
+ entries() {
46
+ return this.cache.entries();
47
+ }
48
+ }
49
+ exports.LRUCache = LRUCache;
@@ -6,6 +6,7 @@ export declare class MetricsCollector {
6
6
  private config;
7
7
  private metricsBuffer;
8
8
  private readonly bufferSize;
9
+ private readonly maxBufferSize;
9
10
  constructor(config?: MetricsConfig);
10
11
  /**
11
12
  * Record a security event
@@ -43,6 +43,7 @@ class MetricsCollector {
43
43
  constructor(config) {
44
44
  this.metricsBuffer = [];
45
45
  this.bufferSize = 100;
46
+ this.maxBufferSize = 500;
46
47
  this.config = {
47
48
  enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
48
49
  output: (config === null || config === void 0 ? void 0 : config.output) || 'stdout',
@@ -56,6 +57,9 @@ class MetricsCollector {
56
57
  if (!this.config.enabled) {
57
58
  return;
58
59
  }
60
+ if (this.metricsBuffer.length >= this.maxBufferSize) {
61
+ this.flush();
62
+ }
59
63
  this.metricsBuffer.push(event);
60
64
  if (this.metricsBuffer.length >= this.bufferSize) {
61
65
  this.flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",