verdaccio-security-filter 1.3.0 → 1.3.2
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 +1 -54
- package/build/index.js +51 -170
- package/build/lib/author-checker.d.ts +0 -19
- package/build/lib/author-checker.js +5 -31
- package/build/lib/cve-checker.d.ts +5 -31
- package/build/lib/cve-checker.js +29 -42
- package/build/lib/license-checker.d.ts +0 -21
- package/build/lib/license-checker.js +0 -30
- package/build/lib/logger.d.ts +0 -21
- package/build/lib/logger.js +0 -21
- package/build/lib/lru-cache.d.ts +12 -0
- package/build/lib/lru-cache.js +46 -0
- package/build/lib/metrics.d.ts +1 -36
- package/build/lib/metrics.js +4 -38
- package/build/lib/package-age-checker.d.ts +0 -22
- package/build/lib/package-age-checker.js +0 -29
- package/build/lib/whitelist-checker.d.ts +0 -26
- package/build/lib/whitelist-checker.js +0 -37
- package/build/types.d.ts +0 -3
- package/package.json +5 -3
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
|
@@ -2,15 +2,6 @@ import { IPluginMiddleware, IPluginStorageFilter, PluginOptions, IBasicAuth, ISt
|
|
|
2
2
|
import { Application } from 'express';
|
|
3
3
|
import { SecurityConfig } from './types';
|
|
4
4
|
import { SecurityLogger } from './lib/logger';
|
|
5
|
-
/**
|
|
6
|
-
* Security Filter Plugin for Verdaccio
|
|
7
|
-
* Works with both middleware and filter interfaces for complete protection
|
|
8
|
-
* Compatible with Verdaccio 6.x and 7.x
|
|
9
|
-
*
|
|
10
|
-
* Two-layer protection:
|
|
11
|
-
* - Layer 1 (filter_metadata): CVE/License/Age checks when metadata is available
|
|
12
|
-
* - Layer 2 (middleware): Whitelist/Pattern/Scope checks + tarball blocking
|
|
13
|
-
*/
|
|
14
5
|
export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityConfig>, IPluginStorageFilter<SecurityConfig> {
|
|
15
6
|
logger: SecurityLogger;
|
|
16
7
|
config: SecurityConfig;
|
|
@@ -21,62 +12,18 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
|
|
|
21
12
|
private readonly licenseChecker;
|
|
22
13
|
private readonly packageAgeChecker;
|
|
23
14
|
private readonly authorChecker;
|
|
24
|
-
/**
|
|
25
|
-
* Creates a new SecurityFilterPlugin instance
|
|
26
|
-
* @param {SecurityConfig} config - Plugin configuration
|
|
27
|
-
* @param {PluginOptions} options - Verdaccio plugin options
|
|
28
|
-
*/
|
|
29
15
|
constructor(config: SecurityConfig, options: PluginOptions<SecurityConfig>);
|
|
30
|
-
/**
|
|
31
|
-
* Parse and validate version range rules
|
|
32
|
-
* @private
|
|
33
|
-
*/
|
|
34
16
|
private _parseVersionRangeRules;
|
|
35
|
-
/**
|
|
36
|
-
* Log initialization details
|
|
37
|
-
* @private
|
|
38
|
-
*/
|
|
39
17
|
private _logInitialization;
|
|
40
|
-
/**
|
|
41
|
-
* Get list of enabled features
|
|
42
|
-
* @private
|
|
43
|
-
*/
|
|
44
18
|
private _getEnabledFeatures;
|
|
45
|
-
/**
|
|
46
|
-
* Check if package name matches blocked patterns
|
|
47
|
-
* @private
|
|
48
|
-
*/
|
|
49
19
|
private _isBlockedByPattern;
|
|
50
|
-
/**
|
|
51
|
-
* Check if package scope is allowed
|
|
52
|
-
* @private
|
|
53
|
-
*/
|
|
54
20
|
private _isScopeAllowed;
|
|
55
|
-
/**
|
|
56
|
-
* Get version range rule for a specific package and version
|
|
57
|
-
* @private
|
|
58
|
-
*/
|
|
59
21
|
private _getVersionRangeRule;
|
|
60
|
-
/**
|
|
61
|
-
* Register Express middleware to intercept package requests
|
|
62
|
-
* Intercepts both metadata and tarball requests
|
|
63
|
-
*/
|
|
64
22
|
register_middlewares(app: Application, _auth: IBasicAuth<SecurityConfig>, _storage: IStorageManager<SecurityConfig>): void;
|
|
65
|
-
/**
|
|
66
|
-
* Filter package metadata before returning to client
|
|
67
|
-
* This provides CVE, License, and Age checking when metadata is available
|
|
68
|
-
*/
|
|
69
23
|
filter_metadata(packageInfo: Package): Promise<Package>;
|
|
70
|
-
/**
|
|
71
|
-
* Check if package should be blocked (used by middleware)
|
|
72
|
-
* @private
|
|
73
|
-
*/
|
|
74
24
|
private _checkPackageBlock;
|
|
75
|
-
/**
|
|
76
|
-
* Get applied security rules for a package
|
|
77
|
-
* @private
|
|
78
|
-
*/
|
|
79
25
|
private _getAppliedRules;
|
|
26
|
+
destroy(): void;
|
|
80
27
|
}
|
|
81
28
|
export { SecurityLogger } from './lib/logger';
|
|
82
29
|
export { MetricsCollector } from './lib/metrics';
|
package/build/index.js
CHANGED
|
@@ -54,24 +54,9 @@ const cve_checker_1 = require("./lib/cve-checker");
|
|
|
54
54
|
const license_checker_1 = require("./lib/license-checker");
|
|
55
55
|
const package_age_checker_1 = require("./lib/package-age-checker");
|
|
56
56
|
const author_checker_1 = require("./lib/author-checker");
|
|
57
|
-
/**
|
|
58
|
-
* Security Filter Plugin for Verdaccio
|
|
59
|
-
* Works with both middleware and filter interfaces for complete protection
|
|
60
|
-
* Compatible with Verdaccio 6.x and 7.x
|
|
61
|
-
*
|
|
62
|
-
* Two-layer protection:
|
|
63
|
-
* - Layer 1 (filter_metadata): CVE/License/Age checks when metadata is available
|
|
64
|
-
* - Layer 2 (middleware): Whitelist/Pattern/Scope checks + tarball blocking
|
|
65
|
-
*/
|
|
66
57
|
class SecurityFilterPlugin {
|
|
67
|
-
/**
|
|
68
|
-
* Creates a new SecurityFilterPlugin instance
|
|
69
|
-
* @param {SecurityConfig} config - Plugin configuration
|
|
70
|
-
* @param {PluginOptions} options - Verdaccio plugin options
|
|
71
|
-
*/
|
|
72
58
|
constructor(config, options) {
|
|
73
59
|
this.config = config || {};
|
|
74
|
-
// Set default error handling strategy if not provided
|
|
75
60
|
if (!this.config.errorHandling) {
|
|
76
61
|
this.config.errorHandling = {
|
|
77
62
|
onFilterError: 'fail-open',
|
|
@@ -79,23 +64,18 @@ class SecurityFilterPlugin {
|
|
|
79
64
|
onLicenseCheckError: 'fail-open',
|
|
80
65
|
};
|
|
81
66
|
}
|
|
82
|
-
// Initialize enhanced logger
|
|
83
67
|
this.logger = new logger_1.SecurityLogger(options.logger, this.config.logger);
|
|
84
|
-
// Initialize metrics collector
|
|
85
68
|
this.metrics = new metrics_1.MetricsCollector(this.config.metrics);
|
|
86
|
-
// Initialize whitelist checker
|
|
87
69
|
this.whitelistChecker = new whitelist_checker_1.WhitelistChecker(this.config.whitelist);
|
|
88
|
-
// Initialize security checkers
|
|
89
70
|
this.cveChecker = new cve_checker_1.CVEChecker(this.config.cveCheck);
|
|
90
71
|
this.licenseChecker = new license_checker_1.LicenseChecker(this.config.licenses);
|
|
91
72
|
this.packageAgeChecker = new package_age_checker_1.PackageAgeChecker(this.config.packageAge);
|
|
92
73
|
this.authorChecker = new author_checker_1.AuthorChecker(this.config.authorFilter);
|
|
93
|
-
// Security rules configuration
|
|
94
74
|
this.securityRules = {
|
|
95
75
|
blockedVersions: this.config.blockedVersions || [],
|
|
96
76
|
blockedPatterns: this.config.blockedPatterns || [],
|
|
97
77
|
minPackageSize: this.config.minPackageSize || 0,
|
|
98
|
-
maxPackageSize: this.config.maxPackageSize || 100 * 1024 * 1024,
|
|
78
|
+
maxPackageSize: this.config.maxPackageSize || 100 * 1024 * 1024,
|
|
99
79
|
allowedScopes: this.config.allowedScopes || [],
|
|
100
80
|
blockedScopes: this.config.blockedScopes || [],
|
|
101
81
|
enforceChecksum: this.config.enforceChecksum !== false,
|
|
@@ -103,10 +83,6 @@ class SecurityFilterPlugin {
|
|
|
103
83
|
};
|
|
104
84
|
this._logInitialization();
|
|
105
85
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Parse and validate version range rules
|
|
108
|
-
* @private
|
|
109
|
-
*/
|
|
110
86
|
_parseVersionRangeRules(rules) {
|
|
111
87
|
return rules.map((rule, index) => {
|
|
112
88
|
if (!rule.package || !rule.range || !rule.strategy) {
|
|
@@ -117,7 +93,6 @@ class SecurityFilterPlugin {
|
|
|
117
93
|
this.logger.warn(`Fallback strategy requires fallbackVersion for ${rule.package}, skipping`);
|
|
118
94
|
return null;
|
|
119
95
|
}
|
|
120
|
-
// Validate semver range
|
|
121
96
|
try {
|
|
122
97
|
const validRange = semver.validRange(rule.range);
|
|
123
98
|
if (!validRange) {
|
|
@@ -132,10 +107,6 @@ class SecurityFilterPlugin {
|
|
|
132
107
|
return rule;
|
|
133
108
|
}).filter((rule) => rule !== null);
|
|
134
109
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Log initialization details
|
|
137
|
-
* @private
|
|
138
|
-
*/
|
|
139
110
|
_logInitialization() {
|
|
140
111
|
var _a;
|
|
141
112
|
const features = this._getEnabledFeatures();
|
|
@@ -159,10 +130,6 @@ class SecurityFilterPlugin {
|
|
|
159
130
|
this.logger.info(`Author filtering enabled: ${summary.blockedAuthors} authors, ${summary.blockedEmails} emails, ${summary.blockedRegions} regions blocked`);
|
|
160
131
|
}
|
|
161
132
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Get list of enabled features
|
|
164
|
-
* @private
|
|
165
|
-
*/
|
|
166
133
|
_getEnabledFeatures() {
|
|
167
134
|
var _a, _b, _c, _d, _e;
|
|
168
135
|
const features = [];
|
|
@@ -188,40 +155,25 @@ class SecurityFilterPlugin {
|
|
|
188
155
|
features.push('scope-blocking');
|
|
189
156
|
return features;
|
|
190
157
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Check if package name matches blocked patterns
|
|
193
|
-
* @private
|
|
194
|
-
*/
|
|
195
158
|
_isBlockedByPattern(packageName) {
|
|
196
159
|
return this.securityRules.blockedPatterns.some(pattern => {
|
|
197
160
|
const regex = new RegExp(pattern);
|
|
198
161
|
return regex.test(packageName);
|
|
199
162
|
});
|
|
200
163
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Check if package scope is allowed
|
|
203
|
-
* @private
|
|
204
|
-
*/
|
|
205
164
|
_isScopeAllowed(packageName) {
|
|
206
|
-
// If not a scoped package, check if allowedScopes is empty
|
|
207
165
|
if (!packageName.startsWith('@')) {
|
|
208
166
|
return this.securityRules.allowedScopes.length === 0;
|
|
209
167
|
}
|
|
210
168
|
const scope = packageName.split('/')[0];
|
|
211
|
-
// Check blacklist
|
|
212
169
|
if (this.securityRules.blockedScopes.includes(scope)) {
|
|
213
170
|
return false;
|
|
214
171
|
}
|
|
215
|
-
// Check whitelist (if exists)
|
|
216
172
|
if (this.securityRules.allowedScopes.length > 0) {
|
|
217
173
|
return this.securityRules.allowedScopes.includes(scope);
|
|
218
174
|
}
|
|
219
175
|
return true;
|
|
220
176
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Get version range rule for a specific package and version
|
|
223
|
-
* @private
|
|
224
|
-
*/
|
|
225
177
|
_getVersionRangeRule(packageName, version) {
|
|
226
178
|
return this.securityRules.versionRangeRules.find(rule => {
|
|
227
179
|
if (rule.package !== packageName) {
|
|
@@ -236,43 +188,28 @@ class SecurityFilterPlugin {
|
|
|
236
188
|
}
|
|
237
189
|
}) || null;
|
|
238
190
|
}
|
|
239
|
-
/**
|
|
240
|
-
* Register Express middleware to intercept package requests
|
|
241
|
-
* Intercepts both metadata and tarball requests
|
|
242
|
-
*/
|
|
243
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
244
191
|
register_middlewares(app, _auth, _storage) {
|
|
245
192
|
this.logger.info('[Middleware] Registering security filter middleware');
|
|
246
193
|
this.logger.info(`[Middleware] Config mode: ${this.config.mode || 'NOT SET'}`);
|
|
247
|
-
// Middleware to intercept all package requests
|
|
248
194
|
app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
249
195
|
try {
|
|
250
|
-
// Extract package name and version from URL
|
|
251
|
-
// Tarball URLs: /:package/-/:filename.tgz
|
|
252
|
-
// Scoped tarball URLs: /@scope/:package/-/:filename.tgz
|
|
253
|
-
// Metadata URLs: /:package or /@scope/:package
|
|
254
196
|
const urlMatch = req.url.match(/^\/(@[^/]+\/)?([^/]+)(?:\/-\/([^/]+\.tgz))?/);
|
|
255
197
|
if (!urlMatch) {
|
|
256
|
-
// Not a package-related request, pass through
|
|
257
198
|
this.logger.debug(`[Middleware] Non-package request: ${req.method} ${req.url}`);
|
|
258
199
|
return next();
|
|
259
200
|
}
|
|
260
201
|
const scope = urlMatch[1] || '';
|
|
261
202
|
const packageName = scope + urlMatch[2];
|
|
262
203
|
const tarballFile = urlMatch[3];
|
|
263
|
-
// Extract version from tarball filename if present
|
|
264
204
|
let version;
|
|
265
205
|
if (tarballFile) {
|
|
266
|
-
// This is a tarball download request - block it immediately
|
|
267
206
|
const versionMatch = tarballFile.match(/-([\d.]+(?:-[a-zA-Z0-9.]+)?)\.tgz$/);
|
|
268
207
|
version = versionMatch ? versionMatch[1] : undefined;
|
|
269
208
|
this.logger.info(`[Middleware] TARBALL REQUEST: ${packageName}@${version || 'unknown'}`);
|
|
270
|
-
// Apply all security checks for tarball downloads
|
|
271
209
|
const blockResult = yield this._checkPackageBlock(packageName, version);
|
|
272
210
|
if (blockResult.blocked) {
|
|
273
211
|
this.logger.warn(`[Middleware] [X] BLOCKED TARBALL: ${packageName}@${version || '*'} - ${blockResult.reason}`);
|
|
274
212
|
this.metrics.recordBlock(packageName, version || '*', blockResult.reason);
|
|
275
|
-
// Return 403 Forbidden with detailed error
|
|
276
213
|
res.status(403).json({
|
|
277
214
|
error: 'Package blocked by security filter',
|
|
278
215
|
package: packageName,
|
|
@@ -285,90 +222,41 @@ class SecurityFilterPlugin {
|
|
|
285
222
|
this.logger.info(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
|
|
286
223
|
}
|
|
287
224
|
else {
|
|
288
|
-
// This is a metadata request - intercept response and modify it
|
|
289
225
|
this.logger.info(`[Middleware] METADATA REQUEST: ${packageName} - will intercept response`);
|
|
290
|
-
// Check if package should be blocked
|
|
291
226
|
const blockResult = yield this._checkPackageBlock(packageName);
|
|
292
227
|
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
|
-
}
|
|
228
|
+
this.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
|
|
229
|
+
const blockedResponse = {
|
|
230
|
+
name: packageName,
|
|
231
|
+
versions: {},
|
|
232
|
+
'dist-tags': {},
|
|
233
|
+
security: {
|
|
234
|
+
blocked: true,
|
|
235
|
+
reason: blockResult.reason,
|
|
236
|
+
plugin: {
|
|
237
|
+
name: 'verdaccio-security-filter',
|
|
238
|
+
version: '1.0.0',
|
|
239
|
+
mode: this.config.mode || 'blacklist',
|
|
240
|
+
},
|
|
241
|
+
blockedAt: new Date().toISOString(),
|
|
242
|
+
rules: this._getAppliedRules(packageName),
|
|
243
|
+
message: `This package has been blocked by security filter: ${blockResult.reason}`,
|
|
244
|
+
contact: 'Please contact your registry administrator for more information',
|
|
245
|
+
},
|
|
246
|
+
_security: {
|
|
247
|
+
blocked: true,
|
|
248
|
+
reason: blockResult.reason,
|
|
249
|
+
blockedBy: 'verdaccio-security-filter',
|
|
250
|
+
blockedAt: new Date().toISOString(),
|
|
363
251
|
}
|
|
364
|
-
return originalSend.call(this, body);
|
|
365
252
|
};
|
|
253
|
+
res.status(200).json(blockedResponse);
|
|
254
|
+
return;
|
|
366
255
|
}
|
|
367
256
|
else {
|
|
368
257
|
this.logger.info(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
|
|
369
258
|
}
|
|
370
259
|
}
|
|
371
|
-
// Continue to next middleware
|
|
372
260
|
next();
|
|
373
261
|
}
|
|
374
262
|
catch (error) {
|
|
@@ -378,22 +266,16 @@ class SecurityFilterPlugin {
|
|
|
378
266
|
}));
|
|
379
267
|
this.logger.info('[Middleware] Security filter middleware registered successfully');
|
|
380
268
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Filter package metadata before returning to client
|
|
383
|
-
* This provides CVE, License, and Age checking when metadata is available
|
|
384
|
-
*/
|
|
385
269
|
filter_metadata(packageInfo) {
|
|
386
270
|
return __awaiter(this, void 0, void 0, function* () {
|
|
387
271
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
388
272
|
const packageName = packageInfo.name;
|
|
389
273
|
this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
|
|
390
274
|
try {
|
|
391
|
-
// 1. Whitelist/blacklist check (basic filtering)
|
|
392
275
|
const blockResult = yield this._checkPackageBlock(packageName);
|
|
393
276
|
if (blockResult.blocked) {
|
|
394
277
|
this.logger.warn(`[filter_metadata] BLOCKED: ${packageName}@* - ${blockResult.reason}`);
|
|
395
278
|
this.metrics.recordBlock(packageName, '*', blockResult.reason);
|
|
396
|
-
// Return empty versions with security field
|
|
397
279
|
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
398
280
|
blocked: true,
|
|
399
281
|
reason: blockResult.reason,
|
|
@@ -406,18 +288,24 @@ class SecurityFilterPlugin {
|
|
|
406
288
|
rules: this._getAppliedRules(packageName),
|
|
407
289
|
} });
|
|
408
290
|
}
|
|
409
|
-
// 2. CVE Check - check all versions
|
|
410
291
|
if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
|
|
411
292
|
const versions = Object.keys(packageInfo.versions || {});
|
|
412
293
|
const vulnerableVersions = [];
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
294
|
+
const BATCH_SIZE = 10;
|
|
295
|
+
for (let i = 0; i < versions.length; i += BATCH_SIZE) {
|
|
296
|
+
const batch = versions.slice(i, i + BATCH_SIZE);
|
|
297
|
+
const results = yield Promise.allSettled(batch.map(version => this.cveChecker.checkPackage(packageName, version)));
|
|
298
|
+
results.forEach((result, index) => {
|
|
299
|
+
if (result.status === 'fulfilled' && result.value.isVulnerable) {
|
|
300
|
+
const version = batch[index];
|
|
301
|
+
vulnerableVersions.push(version);
|
|
302
|
+
this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${result.value.vulnerabilities.length} vulnerabilities`);
|
|
303
|
+
}
|
|
304
|
+
else if (result.status === 'rejected') {
|
|
305
|
+
this.logger.error(`[filter_metadata] CVE check failed for ${packageName}@${batch[index]}: ${result.reason}`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
419
308
|
}
|
|
420
|
-
// If configured to auto-block and vulnerabilities found
|
|
421
309
|
if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
|
|
422
310
|
const reason = `Package has ${vulnerableVersions.length} vulnerable version(s)`;
|
|
423
311
|
this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
|
|
@@ -434,7 +322,6 @@ class SecurityFilterPlugin {
|
|
|
434
322
|
} });
|
|
435
323
|
}
|
|
436
324
|
}
|
|
437
|
-
// 3. License Check - check latest version
|
|
438
325
|
if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
|
|
439
326
|
const latestVersion = (_b = packageInfo['dist-tags']) === null || _b === void 0 ? void 0 : _b.latest;
|
|
440
327
|
if (latestVersion && ((_c = packageInfo.versions) === null || _c === void 0 ? void 0 : _c[latestVersion])) {
|
|
@@ -456,7 +343,6 @@ class SecurityFilterPlugin {
|
|
|
456
343
|
}
|
|
457
344
|
}
|
|
458
345
|
}
|
|
459
|
-
// 4. Package Age Check
|
|
460
346
|
if ((_d = this.config.packageAge) === null || _d === void 0 ? void 0 : _d.enabled) {
|
|
461
347
|
const ageResult = this.packageAgeChecker.checkPackageAge(packageInfo);
|
|
462
348
|
if (!ageResult.allowed && !ageResult.warnOnly) {
|
|
@@ -477,7 +363,6 @@ class SecurityFilterPlugin {
|
|
|
477
363
|
this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
|
|
478
364
|
}
|
|
479
365
|
}
|
|
480
|
-
// 5. Author Filter Check
|
|
481
366
|
if ((_e = this.config.authorFilter) === null || _e === void 0 ? void 0 : _e.enabled) {
|
|
482
367
|
const latestVersion = (_f = packageInfo['dist-tags']) === null || _f === void 0 ? void 0 : _f.latest;
|
|
483
368
|
if (latestVersion && ((_g = packageInfo.versions) === null || _g === void 0 ? void 0 : _g[latestVersion])) {
|
|
@@ -537,34 +422,25 @@ class SecurityFilterPlugin {
|
|
|
537
422
|
}
|
|
538
423
|
});
|
|
539
424
|
}
|
|
540
|
-
/**
|
|
541
|
-
* Check if package should be blocked (used by middleware)
|
|
542
|
-
* @private
|
|
543
|
-
*/
|
|
544
425
|
_checkPackageBlock(packageName, version) {
|
|
545
426
|
return __awaiter(this, void 0, void 0, function* () {
|
|
546
|
-
// 1. Whitelist mode
|
|
547
427
|
if (this.config.mode === 'whitelist') {
|
|
548
428
|
const whitelistCheck = this.whitelistChecker.isWhitelisted(packageName, version);
|
|
549
429
|
if (!whitelistCheck.allowed) {
|
|
550
430
|
return { blocked: true, reason: whitelistCheck.reason || 'Package is not in whitelist' };
|
|
551
431
|
}
|
|
552
432
|
}
|
|
553
|
-
// 2. Blocked patterns
|
|
554
433
|
if (this._isBlockedByPattern(packageName)) {
|
|
555
434
|
return { blocked: true, reason: 'Package name matches blocked pattern' };
|
|
556
435
|
}
|
|
557
|
-
// 3. Scopes
|
|
558
436
|
if (!this._isScopeAllowed(packageName)) {
|
|
559
437
|
return { blocked: true, reason: 'Package scope not allowed' };
|
|
560
438
|
}
|
|
561
|
-
// 4. Exact version blocking
|
|
562
439
|
if (version) {
|
|
563
440
|
const versionKey = `${packageName}@${version}`;
|
|
564
441
|
if (this.securityRules.blockedVersions.includes(versionKey)) {
|
|
565
442
|
return { blocked: true, reason: 'Exact version match in blocklist' };
|
|
566
443
|
}
|
|
567
|
-
// 5. Version range rules
|
|
568
444
|
const rangeRule = this._getVersionRangeRule(packageName, version);
|
|
569
445
|
if (rangeRule && rangeRule.strategy === 'block') {
|
|
570
446
|
return { blocked: true, reason: `Version matches blocked range: ${rangeRule.range}` };
|
|
@@ -573,13 +449,8 @@ class SecurityFilterPlugin {
|
|
|
573
449
|
return { blocked: false, reason: '' };
|
|
574
450
|
});
|
|
575
451
|
}
|
|
576
|
-
/**
|
|
577
|
-
* Get applied security rules for a package
|
|
578
|
-
* @private
|
|
579
|
-
*/
|
|
580
452
|
_getAppliedRules(packageName) {
|
|
581
453
|
const rules = {};
|
|
582
|
-
// Check which rules apply to this package
|
|
583
454
|
if (this.config.mode === 'whitelist') {
|
|
584
455
|
rules.mode = 'whitelist';
|
|
585
456
|
rules.whitelisted = false;
|
|
@@ -610,9 +481,19 @@ class SecurityFilterPlugin {
|
|
|
610
481
|
}
|
|
611
482
|
return rules;
|
|
612
483
|
}
|
|
484
|
+
destroy() {
|
|
485
|
+
this.logger.info('[Cleanup] Shutting down security filter plugin');
|
|
486
|
+
try {
|
|
487
|
+
this.cveChecker.destroy();
|
|
488
|
+
this.metrics.cleanup();
|
|
489
|
+
this.logger.info('[Cleanup] Plugin resources cleaned up successfully');
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
this.logger.error(`[Cleanup] Error during cleanup: ${error.message}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
613
495
|
}
|
|
614
496
|
exports.default = SecurityFilterPlugin;
|
|
615
|
-
// Export utility classes for testing
|
|
616
497
|
var logger_2 = require("./lib/logger");
|
|
617
498
|
Object.defineProperty(exports, "SecurityLogger", { enumerable: true, get: function () { return logger_2.SecurityLogger; } });
|
|
618
499
|
var metrics_2 = require("./lib/metrics");
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { Version } from '@verdaccio/types';
|
|
2
2
|
import { AuthorFilterConfig, AuthorCheckResult } from '../types';
|
|
3
|
-
/**
|
|
4
|
-
* AuthorChecker validates package authors and maintainers
|
|
5
|
-
* against configured security policies.
|
|
6
|
-
*/
|
|
7
3
|
export declare class AuthorChecker {
|
|
8
4
|
private readonly config?;
|
|
9
5
|
private readonly blockedAuthors;
|
|
@@ -13,25 +9,10 @@ export declare class AuthorChecker {
|
|
|
13
9
|
private readonly blockedEmailDomains;
|
|
14
10
|
private readonly regionDomainMap;
|
|
15
11
|
constructor(config?: AuthorFilterConfig);
|
|
16
|
-
/**
|
|
17
|
-
* Check if package author/maintainer is allowed
|
|
18
|
-
*/
|
|
19
12
|
checkAuthor(versionData: Version): AuthorCheckResult;
|
|
20
|
-
/**
|
|
21
|
-
* Extract author information from package version data
|
|
22
|
-
*/
|
|
23
13
|
private _extractAuthors;
|
|
24
|
-
/**
|
|
25
|
-
* Normalize author data to AuthorInfo format
|
|
26
|
-
*/
|
|
27
14
|
private _normalizeAuthor;
|
|
28
|
-
/**
|
|
29
|
-
* Check a single author against all blocking rules
|
|
30
|
-
*/
|
|
31
15
|
private _checkSingleAuthor;
|
|
32
|
-
/**
|
|
33
|
-
* Get summary of configuration
|
|
34
|
-
*/
|
|
35
16
|
getSummary(): {
|
|
36
17
|
enabled: boolean;
|
|
37
18
|
blockedAuthors: number;
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AuthorChecker = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Region to TLD mapping for email domain filtering
|
|
6
|
-
*/
|
|
7
4
|
const REGION_DOMAINS = {
|
|
8
5
|
'ru': ['.ru', 'yandex.ru', 'mail.ru', 'rambler.ru', 'ya.ru', 'bk.ru', 'list.ru'],
|
|
9
6
|
'cn': ['.cn', 'qq.com', '163.com', '126.com', 'sina.com', 'sohu.com'],
|
|
10
7
|
'by': ['.by', 'tut.by', 'mail.by'],
|
|
11
|
-
'kp': ['.kp'],
|
|
12
|
-
'ir': ['.ir'],
|
|
13
|
-
'sy': ['.sy'],
|
|
14
|
-
'cu': ['.cu'],
|
|
15
|
-
'sd': ['.sd'],
|
|
8
|
+
'kp': ['.kp'],
|
|
9
|
+
'ir': ['.ir'],
|
|
10
|
+
'sy': ['.sy'],
|
|
11
|
+
'cu': ['.cu'],
|
|
12
|
+
'sd': ['.sd'],
|
|
16
13
|
};
|
|
17
|
-
/**
|
|
18
|
-
* AuthorChecker validates package authors and maintainers
|
|
19
|
-
* against configured security policies.
|
|
20
|
-
*/
|
|
21
14
|
class AuthorChecker {
|
|
22
15
|
constructor(config) {
|
|
23
16
|
this.config = config;
|
|
@@ -34,9 +27,6 @@ class AuthorChecker {
|
|
|
34
27
|
this.regionDomainMap.set(region.toLowerCase(), new Set(domains));
|
|
35
28
|
});
|
|
36
29
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Check if package author/maintainer is allowed
|
|
39
|
-
*/
|
|
40
30
|
checkAuthor(versionData) {
|
|
41
31
|
var _a;
|
|
42
32
|
if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.enabled)) {
|
|
@@ -61,9 +51,6 @@ class AuthorChecker {
|
|
|
61
51
|
}
|
|
62
52
|
return { allowed: true };
|
|
63
53
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Extract author information from package version data
|
|
66
|
-
*/
|
|
67
54
|
_extractAuthors(versionData) {
|
|
68
55
|
const authors = [];
|
|
69
56
|
if (versionData.author) {
|
|
@@ -81,9 +68,6 @@ class AuthorChecker {
|
|
|
81
68
|
}
|
|
82
69
|
return authors;
|
|
83
70
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Normalize author data to AuthorInfo format
|
|
86
|
-
*/
|
|
87
71
|
_normalizeAuthor(author) {
|
|
88
72
|
if (typeof author === 'string') {
|
|
89
73
|
const emailMatch = author.match(/<([^>]+)>/);
|
|
@@ -99,11 +83,7 @@ class AuthorChecker {
|
|
|
99
83
|
url: author.url,
|
|
100
84
|
};
|
|
101
85
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Check a single author against all blocking rules
|
|
104
|
-
*/
|
|
105
86
|
_checkSingleAuthor(author) {
|
|
106
|
-
// Check blocked author names
|
|
107
87
|
if (author.name) {
|
|
108
88
|
const normalizedName = author.name.toLowerCase().trim();
|
|
109
89
|
if (this.blockedAuthors.has(normalizedName)) {
|
|
@@ -125,7 +105,6 @@ class AuthorChecker {
|
|
|
125
105
|
}
|
|
126
106
|
}
|
|
127
107
|
}
|
|
128
|
-
// Check blocked emails
|
|
129
108
|
if (author.email) {
|
|
130
109
|
const normalizedEmail = author.email.toLowerCase().trim();
|
|
131
110
|
if (this.blockedEmails.has(normalizedEmail)) {
|
|
@@ -146,7 +125,6 @@ class AuthorChecker {
|
|
|
146
125
|
};
|
|
147
126
|
}
|
|
148
127
|
}
|
|
149
|
-
// Check blocked email domains
|
|
150
128
|
for (const domain of this.blockedEmailDomains) {
|
|
151
129
|
if (normalizedEmail.endsWith(domain) || normalizedEmail.includes(`@${domain}`)) {
|
|
152
130
|
return {
|
|
@@ -157,7 +135,6 @@ class AuthorChecker {
|
|
|
157
135
|
};
|
|
158
136
|
}
|
|
159
137
|
}
|
|
160
|
-
// Check blocked regions
|
|
161
138
|
for (const [region, domains] of this.regionDomainMap.entries()) {
|
|
162
139
|
for (const domain of domains) {
|
|
163
140
|
if (normalizedEmail.endsWith(domain) ||
|
|
@@ -175,9 +152,6 @@ class AuthorChecker {
|
|
|
175
152
|
}
|
|
176
153
|
return { allowed: true };
|
|
177
154
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Get summary of configuration
|
|
180
|
-
*/
|
|
181
155
|
getSummary() {
|
|
182
156
|
var _a;
|
|
183
157
|
return {
|