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 +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +55 -78
- package/build/lib/cve-checker.d.ts +12 -1
- package/build/lib/cve-checker.js +36 -5
- package/build/lib/lru-cache.d.ts +15 -0
- package/build/lib/lru-cache.js +49 -0
- package/build/lib/metrics.d.ts +1 -0
- package/build/lib/metrics.js +4 -0
- package/package.json +1 -1
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
|
+
[](https://stand-with-ukraine.pp.ua)
|
|
5
6
|
[](https://www.npmjs.com/package/verdaccio-security-filter)
|
|
6
7
|
[](https://www.npmjs.com/package/verdaccio-security-filter)
|
|
7
8
|
[](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
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
*
|
|
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
|
}
|
package/build/lib/cve-checker.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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;
|
package/build/lib/metrics.d.ts
CHANGED
package/build/lib/metrics.js
CHANGED
|
@@ -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