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 +3 -1
- package/build/index.js +28 -12
- package/build/lib/cve-checker.d.ts +3 -0
- package/build/lib/cve-checker.js +89 -14
- package/package.json +2 -2
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` |
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
package/build/lib/cve-checker.js
CHANGED
|
@@ -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
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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.
|
|
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.
|
|
57
|
+
"qs": "6.16.0",
|
|
58
58
|
"@cypress/request": "latest"
|
|
59
59
|
},
|
|
60
60
|
"author": {
|