verdaccio-security-filter 1.3.11 → 1.3.12

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
@@ -422,7 +422,7 @@ middlewares:
422
422
  | `cveCheck.updateInterval` | `number` | `24` | How long a package's vulnerability data is cached (hours) |
423
423
  | `cveCheck.cacheDir` | `string` | `./.security-cache` | Cache directory path |
424
424
 
425
- > **Note on `autoBlock` and dist-tags:** only the vulnerable versions are removed; unaffected versions stay installable. Any dist-tag (including `latest`) that points to a removed version is dropped and **not** re-pointed to another version. If `latest` is vulnerable, `npm install <pkg>` without a version or range will fail - install an explicit safe version or range instead (e.g. `npm install <pkg>@^1.2.0`).
425
+ > **Note on removed versions and dist-tags:** whenever a check removes versions (CVE `autoBlock`, license, author, age, version rules, `whitelist.versions`), the other versions stay installable. The plugin drops every dist-tag that points to a removed version. Verdaccio then points `latest` at the highest version that is left (verified with Verdaccio 6.2), so `npm install <pkg>` keeps working. Other dist-tags that pointed to a removed version are gone.
426
426
 
427
427
  > **When OSV is unreachable:** a lookup gets one retry with a 5 second timeout each. After a failure the same package is not looked up again for 60 seconds. Previously cached data, even if older than `updateInterval`, keeps being used. With no cached data, `errorHandling.onCveCheckError` decides: `fail-open` serves the package unchecked, `fail-closed` blocks it.
428
428
 
@@ -432,7 +432,9 @@ middlewares:
432
432
  |--------|------|---------|-------------|
433
433
  | `licenses.allowed` | `string[]` | `[]` | Allowed license list |
434
434
  | `licenses.blocked` | `string[]` | `[]` | Blocked license list |
435
- | `licenses.requireLicense` | `boolean` | `false` | Require license field |
435
+ | `licenses.requireLicense` | `boolean` | `true` | Reject versions without a license field |
436
+
437
+ The license check runs when `licenses.allowed` is not empty. Every version is checked: versions with a rejected license are removed from the metadata, and the package is blocked as a whole only when no version is left.
436
438
 
437
439
  ### Whitelist Options
438
440
 
@@ -481,6 +483,8 @@ middlewares:
481
483
  | `authorFilter.requireVerifiedEmail` | `boolean` | `false` | Require author email information |
482
484
  | `authorFilter.warnOnly` | `boolean` | `false` | Only warn, don't block |
483
485
 
486
+ Every version is checked against the author rules: versions published by a blocked author are removed from the metadata, and the package is blocked as a whole only when no version is left.
487
+
484
488
  ### Error Handling Options
485
489
 
486
490
  | Option | Type | Default | Description |
package/build/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
23
23
  register_middlewares(app: Application, _auth: IBasicAuth<SecurityConfig>, _storage: IStorageManager<SecurityConfig>): void;
24
24
  private _blockedPackage;
25
25
  private _removeVersions;
26
+ private _rejectedVersions;
26
27
  filter_metadata(packageInfo: Package): Promise<Package>;
27
28
  private _versionBlockReason;
28
29
  private _checkPackageBlock;
package/build/index.js CHANGED
@@ -306,9 +306,23 @@ class SecurityFilterPlugin {
306
306
  }
307
307
  return Object.assign(Object.assign({}, pkg), { versions, 'dist-tags': distTags });
308
308
  }
309
+ _rejectedVersions(pkg, check) {
310
+ var _a;
311
+ const all = Object.entries(pkg.versions || {});
312
+ const results = all
313
+ .map(([version, data]) => [version, check(data)])
314
+ .filter(([, result]) => !result.allowed);
315
+ const latest = results.find(([version]) => { var _a; return version === ((_a = pkg['dist-tags']) === null || _a === void 0 ? void 0 : _a.latest); });
316
+ return {
317
+ results,
318
+ versions: results.map(([version]) => version),
319
+ first: (_a = (latest || results[0])) === null || _a === void 0 ? void 0 : _a[1],
320
+ all: all.length > 0 && results.length === all.length,
321
+ };
322
+ }
309
323
  filter_metadata(packageInfo) {
310
324
  return __awaiter(this, void 0, void 0, function* () {
311
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
325
+ var _a, _b, _c, _d, _e;
312
326
  let pkg = packageInfo;
313
327
  const packageName = pkg.name;
314
328
  this.logger.debug(`[filter_metadata] --> Processing: ${packageName}`);
@@ -361,19 +375,22 @@ class SecurityFilterPlugin {
361
375
  }
362
376
  }
363
377
  if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
364
- const latestVersion = (_c = pkg['dist-tags']) === null || _c === void 0 ? void 0 : _c.latest;
365
- if (latestVersion && ((_d = pkg.versions) === null || _d === void 0 ? void 0 : _d[latestVersion])) {
366
- const versionData = pkg.versions[latestVersion];
367
- const licenseResult = this.licenseChecker.checkLicense(versionData);
368
- if (!licenseResult.allowed) {
369
- const reason = licenseResult.reason || 'License not allowed';
370
- this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${reason}`);
371
- this.metrics.recordBlock(packageName, '*', reason);
372
- return this._blockedPackage(pkg, reason, { license: licenseResult.license });
378
+ const rejected = this._rejectedVersions(pkg, data => this.licenseChecker.checkLicense(data));
379
+ if (rejected.all) {
380
+ const reason = rejected.first.reason || 'License not allowed';
381
+ this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${reason}`);
382
+ this.metrics.recordBlock(packageName, '*', reason);
383
+ return this._blockedPackage(pkg, reason, { license: rejected.first.license });
384
+ }
385
+ if (rejected.versions.length > 0) {
386
+ this.logger.warn(`[filter_metadata] LICENSE FILTERED: ${packageName}: ${rejected.versions.join(', ')}`);
387
+ for (const [version, result] of rejected.results) {
388
+ this.metrics.recordBlock(packageName, version, result.reason || 'License not allowed');
373
389
  }
390
+ pkg = this._removeVersions(pkg, rejected.versions);
374
391
  }
375
392
  }
376
- if ((_e = this.config.packageAge) === null || _e === void 0 ? void 0 : _e.enabled) {
393
+ if ((_c = this.config.packageAge) === null || _c === void 0 ? void 0 : _c.enabled) {
377
394
  const ageResult = this.packageAgeChecker.checkPackageAge(pkg);
378
395
  if (!ageResult.allowed && !ageResult.warnOnly) {
379
396
  const reason = ageResult.reason || 'Package too new';
@@ -400,33 +417,38 @@ class SecurityFilterPlugin {
400
417
  pkg = this._removeVersions(pkg, removedVersions);
401
418
  }
402
419
  }
403
- if ((_f = this.config.authorFilter) === null || _f === void 0 ? void 0 : _f.enabled) {
404
- const latestVersion = (_g = pkg['dist-tags']) === null || _g === void 0 ? void 0 : _g.latest;
405
- if (latestVersion && ((_h = pkg.versions) === null || _h === void 0 ? void 0 : _h[latestVersion])) {
406
- const versionData = pkg.versions[latestVersion];
407
- const authorResult = this.authorChecker.checkAuthor(versionData);
408
- if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
409
- const reason = authorResult.reason || 'Author not allowed';
410
- this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${reason}`);
420
+ if ((_d = this.config.authorFilter) === null || _d === void 0 ? void 0 : _d.enabled) {
421
+ const rejected = this._rejectedVersions(pkg, data => this.authorChecker.checkAuthor(data));
422
+ if (rejected.versions.length > 0 && this.config.authorFilter.warnOnly) {
423
+ this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName}: ${rejected.versions.join(', ')} - ${rejected.first.reason} (warn-only mode)`);
424
+ }
425
+ else if (rejected.versions.length > 0) {
426
+ for (const [version, result] of rejected.results) {
411
427
  this.metrics.record({
412
428
  timestamp: new Date().toISOString(),
413
429
  event: 'author_blocked',
414
430
  packageName,
415
- version: latestVersion,
416
- reason,
431
+ version: rejected.all ? '*' : version,
432
+ reason: result.reason || 'Author not allowed',
417
433
  metadata: {
418
- blockedBy: authorResult.blockedBy,
419
- authorInfo: authorResult.authorInfo,
434
+ blockedBy: result.blockedBy,
435
+ authorInfo: result.authorInfo,
420
436
  },
421
437
  });
438
+ if (rejected.all) {
439
+ break;
440
+ }
441
+ }
442
+ if (rejected.all) {
443
+ const reason = rejected.first.reason || 'Author not allowed';
444
+ this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${reason}`);
422
445
  return this._blockedPackage(pkg, reason, {
423
- blockedBy: authorResult.blockedBy,
424
- authorInfo: authorResult.authorInfo,
446
+ blockedBy: rejected.first.blockedBy,
447
+ authorInfo: rejected.first.authorInfo,
425
448
  });
426
449
  }
427
- else if (!authorResult.allowed && this.config.authorFilter.warnOnly) {
428
- this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName} - ${authorResult.reason} (warn-only mode)`);
429
- }
450
+ this.logger.warn(`[filter_metadata] AUTHOR FILTERED: ${packageName}: ${rejected.versions.join(', ')}`);
451
+ pkg = this._removeVersions(pkg, rejected.versions);
430
452
  }
431
453
  }
432
454
  this.logger.debug(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
@@ -434,7 +456,7 @@ class SecurityFilterPlugin {
434
456
  }
435
457
  catch (error) {
436
458
  this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
437
- const errorStrategy = ((_j = this.config.errorHandling) === null || _j === void 0 ? void 0 : _j.onFilterError) || 'fail-open';
459
+ const errorStrategy = ((_e = this.config.errorHandling) === null || _e === void 0 ? void 0 : _e.onFilterError) || 'fail-open';
438
460
  if (errorStrategy === 'fail-closed') {
439
461
  this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
440
462
  this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
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",