verdaccio-security-filter 1.3.7 → 1.3.8

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
@@ -430,10 +430,12 @@ middlewares:
430
430
  | `cveCheck.enabled` | `boolean` | `false` | Enable CVE scanning |
431
431
  | `cveCheck.databases` | `string[]` | `['osv']` | Databases: `osv`, `snyk`, `github` |
432
432
  | `cveCheck.severity` | `string[]` | `['high', 'critical']` | Severity levels to check |
433
- | `cveCheck.autoBlock` | `boolean` | `false` | Auto-block vulnerable packages |
433
+ | `cveCheck.autoBlock` | `boolean` | `false` | Remove vulnerable versions from metadata (whole package blocked if every version is vulnerable) |
434
434
  | `cveCheck.updateInterval` | `number` | `24` | Cache update interval (hours) |
435
435
  | `cveCheck.cacheDir` | `string` | `./.security-cache` | Cache directory path |
436
436
 
437
+ > **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`).
438
+
437
439
  ### License Options
438
440
 
439
441
  | Option | Type | Default | Description |
package/build/index.js CHANGED
@@ -318,18 +318,34 @@ class SecurityFilterPlugin {
318
318
  }
319
319
  if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
320
320
  const reason = `Package has ${vulnerableVersions.length} vulnerable version(s)`;
321
- this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
322
- this.metrics.recordBlock(packageName, '*', reason);
323
- return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
324
- blocked: true,
325
- reason,
326
- vulnerableVersions,
327
- plugin: {
328
- name: 'verdaccio-security-filter',
329
- version: '2.0.0',
330
- },
331
- blockedAt: new Date().toISOString(),
332
- } });
321
+ if (vulnerableVersions.length === versions.length) {
322
+ this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
323
+ this.metrics.recordBlock(packageName, '*', reason);
324
+ return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
325
+ blocked: true,
326
+ reason,
327
+ vulnerableVersions,
328
+ plugin: {
329
+ name: 'verdaccio-security-filter',
330
+ version: '2.0.0',
331
+ },
332
+ blockedAt: new Date().toISOString(),
333
+ } });
334
+ }
335
+ this.logger.warn(`[filter_metadata] CVE FILTERED: ${packageName} - ${reason}: ${vulnerableVersions.join(', ')}`);
336
+ const filteredVersions = Object.assign({}, pkg.versions);
337
+ for (const version of vulnerableVersions) {
338
+ delete filteredVersions[version];
339
+ this.metrics.recordBlock(packageName, version, reason);
340
+ }
341
+ const filteredDistTags = Object.assign({}, pkg['dist-tags']);
342
+ for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
343
+ if (vulnerableVersions.includes(tagVersion)) {
344
+ delete filteredDistTags[tag];
345
+ this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to vulnerable version ${tagVersion}`);
346
+ }
347
+ }
348
+ pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
333
349
  }
334
350
  }
335
351
  if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
@@ -12,6 +12,9 @@ export declare class CVEChecker {
12
12
  private fetchFromOSV;
13
13
  private sleep;
14
14
  private mapOSVSeverity;
15
+ private mapDatabaseSpecificSeverity;
16
+ private scoreToSeverity;
17
+ private computeCVSSv3BaseScore;
15
18
  private isCacheStale;
16
19
  private loadCache;
17
20
  private debouncedSaveCache;
@@ -46,6 +46,21 @@ exports.CVEChecker = void 0;
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const lru_cache_1 = require("./lru-cache");
49
+ const CVSS_V3_WEIGHTS = {
50
+ AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
51
+ AC: { L: 0.77, H: 0.44 },
52
+ UI: { N: 0.85, R: 0.62 },
53
+ PR_UNCHANGED: { N: 0.85, L: 0.62, H: 0.27 },
54
+ PR_CHANGED: { N: 0.85, L: 0.68, H: 0.5 },
55
+ CIA: { H: 0.56, L: 0.22, N: 0 },
56
+ };
57
+ function roundUpCvssScore(value) {
58
+ const intValue = Math.round(value * 100000);
59
+ if (intValue % 10000 === 0) {
60
+ return intValue / 100000;
61
+ }
62
+ return (Math.floor(intValue / 10000) + 1) / 10;
63
+ }
49
64
  class CVEChecker {
50
65
  constructor(config) {
51
66
  this.lastUpdate = 0;
@@ -157,7 +172,7 @@ class CVEChecker {
157
172
  var _a, _b, _c, _d, _e, _f, _g;
158
173
  return ({
159
174
  id: vuln.id,
160
- severity: this.mapOSVSeverity(vuln.severity),
175
+ severity: this.mapOSVSeverity(vuln),
161
176
  summary: vuln.summary || vuln.details || 'No description available',
162
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()) || [],
163
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,
@@ -187,23 +202,83 @@ class CVEChecker {
187
202
  sleep(ms) {
188
203
  return new Promise(resolve => setTimeout(resolve, ms));
189
204
  }
190
- mapOSVSeverity(osvSeverity) {
191
- if (!osvSeverity || osvSeverity.length === 0) {
192
- return 'medium';
205
+ mapOSVSeverity(vuln) {
206
+ var _a, _b, _c, _d;
207
+ const databaseSeverity = this.mapDatabaseSpecificSeverity((_a = vuln === null || vuln === void 0 ? void 0 : vuln.database_specific) === null || _a === void 0 ? void 0 : _a.severity);
208
+ if (databaseSeverity) {
209
+ return databaseSeverity;
193
210
  }
194
- const severity = osvSeverity[0];
195
- if (severity.type === 'CVSS_V3') {
196
- const score = parseFloat(severity.score);
197
- if (score >= 9.0)
198
- return 'critical';
199
- if (score >= 7.0)
200
- return 'high';
201
- if (score >= 4.0)
202
- return 'medium';
203
- return 'low';
211
+ const cvssV3 = (_b = vuln === null || vuln === void 0 ? void 0 : vuln.severity) === null || _b === void 0 ? void 0 : _b.find(entry => entry.type === 'CVSS_V3');
212
+ const score = cvssV3 ? this.computeCVSSv3BaseScore(cvssV3.score) : null;
213
+ if (score !== null) {
214
+ return this.scoreToSeverity(score);
215
+ }
216
+ if ((_c = vuln === null || vuln === void 0 ? void 0 : vuln.severity) === null || _c === void 0 ? void 0 : _c.some(entry => entry.type === 'CVSS_V4')) {
217
+ console.debug(`[CVE Checker] ${(_d = vuln.id) !== null && _d !== void 0 ? _d : 'unknown vulnerability'} only has a CVSS_V4 vector, which is not scored yet - defaulting severity to medium`);
204
218
  }
205
219
  return 'medium';
206
220
  }
221
+ mapDatabaseSpecificSeverity(severity) {
222
+ if (typeof severity !== 'string') {
223
+ return null;
224
+ }
225
+ switch (severity.toUpperCase()) {
226
+ case 'LOW': return 'low';
227
+ case 'MODERATE': return 'medium';
228
+ case 'HIGH': return 'high';
229
+ case 'CRITICAL': return 'critical';
230
+ default: return null;
231
+ }
232
+ }
233
+ scoreToSeverity(score) {
234
+ if (score >= 9.0)
235
+ return 'critical';
236
+ if (score >= 7.0)
237
+ return 'high';
238
+ if (score >= 4.0)
239
+ return 'medium';
240
+ return 'low';
241
+ }
242
+ computeCVSSv3BaseScore(vector) {
243
+ if (typeof vector !== 'string') {
244
+ return null;
245
+ }
246
+ const metrics = {};
247
+ for (const part of vector.split('/')) {
248
+ const [key, value] = part.split(':');
249
+ if (key && value) {
250
+ metrics[key] = value;
251
+ }
252
+ }
253
+ const scope = metrics.S;
254
+ const av = CVSS_V3_WEIGHTS.AV[metrics.AV];
255
+ const ac = CVSS_V3_WEIGHTS.AC[metrics.AC];
256
+ const ui = CVSS_V3_WEIGHTS.UI[metrics.UI];
257
+ const pr = scope === 'C'
258
+ ? CVSS_V3_WEIGHTS.PR_CHANGED[metrics.PR]
259
+ : CVSS_V3_WEIGHTS.PR_UNCHANGED[metrics.PR];
260
+ const c = CVSS_V3_WEIGHTS.CIA[metrics.C];
261
+ const i = CVSS_V3_WEIGHTS.CIA[metrics.I];
262
+ const a = CVSS_V3_WEIGHTS.CIA[metrics.A];
263
+ if ((scope !== 'U' && scope !== 'C') || [av, ac, ui, pr, c, i, a].some(value => value === undefined)) {
264
+ return null;
265
+ }
266
+ const impactSubScoreBase = 1 - (1 - c) * (1 - i) * (1 - a);
267
+ const impactSubScore = scope === 'U'
268
+ ? 6.42 * impactSubScoreBase
269
+ : 7.52 * (impactSubScoreBase - 0.029) - 3.25 * Math.pow(impactSubScoreBase - 0.02, 15);
270
+ if (impactSubScore <= 0) {
271
+ return 0;
272
+ }
273
+ const exploitability = 8.22 * av * ac * pr * ui;
274
+ const rawScore = scope === 'U'
275
+ ? impactSubScore + exploitability
276
+ : 1.08 * (impactSubScore + exploitability);
277
+ const cappedScore = Math.min(rawScore, 10);
278
+ return metrics.CVSS === '3.0'
279
+ ? Math.ceil(cappedScore * 10) / 10
280
+ : roundUpCvssScore(cappedScore);
281
+ }
207
282
  isCacheStale() {
208
283
  const now = Date.now();
209
284
  const updateIntervalMs = this.config.updateInterval * 60 * 60 * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
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",
@@ -54,7 +54,7 @@
54
54
  "typescript-eslint": "^8.0.0"
55
55
  },
56
56
  "overrides": {
57
- "qs": "6.14.2",
57
+ "qs": "6.16.0",
58
58
  "@cypress/request": "latest"
59
59
  },
60
60
  "author": {