verdaccio-security-filter 1.3.7 → 1.3.9
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 +59 -35
- package/build/lib/cve-checker.d.ts +14 -4
- package/build/lib/cve-checker.js +290 -137
- 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
|
@@ -277,7 +277,7 @@ class SecurityFilterPlugin {
|
|
|
277
277
|
}
|
|
278
278
|
filter_metadata(packageInfo) {
|
|
279
279
|
return __awaiter(this, void 0, void 0, function* () {
|
|
280
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
280
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
281
281
|
let pkg = packageInfo;
|
|
282
282
|
const packageName = pkg.name;
|
|
283
283
|
this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
|
|
@@ -300,41 +300,65 @@ class SecurityFilterPlugin {
|
|
|
300
300
|
}
|
|
301
301
|
if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
|
|
302
302
|
const versions = Object.keys(pkg.versions || {});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
303
|
+
let vulnerableVersions = [];
|
|
304
|
+
try {
|
|
305
|
+
const results = yield this.cveChecker.checkVersions(packageName, versions);
|
|
306
|
+
vulnerableVersions = results.filter(result => result.isVulnerable).map(result => result.version);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
this.logger.error(`[filter_metadata] CVE check failed for ${packageName}: ${error.message}`);
|
|
310
|
+
if (((_b = this.config.errorHandling) === null || _b === void 0 ? void 0 : _b.onCveCheckError) === 'fail-closed') {
|
|
311
|
+
const reason = `CVE check failed (fail-closed mode): ${error.message}`;
|
|
312
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
313
|
+
return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
|
|
314
|
+
blocked: true,
|
|
315
|
+
reason,
|
|
316
|
+
plugin: {
|
|
317
|
+
name: 'verdaccio-security-filter',
|
|
318
|
+
version: '2.0.0',
|
|
319
|
+
},
|
|
320
|
+
blockedAt: new Date().toISOString(),
|
|
321
|
+
} });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (vulnerableVersions.length > 0 && !this.config.cveCheck.autoBlock) {
|
|
325
|
+
this.logger.warn(`[filter_metadata] CVE found in ${packageName}: ${vulnerableVersions.length} vulnerable version(s), autoBlock is off`);
|
|
318
326
|
}
|
|
319
327
|
if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
|
|
320
328
|
const reason = `Package has ${vulnerableVersions.length} vulnerable version(s)`;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
if (vulnerableVersions.length === versions.length) {
|
|
330
|
+
this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
|
|
331
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
332
|
+
return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
|
|
333
|
+
blocked: true,
|
|
334
|
+
reason,
|
|
335
|
+
vulnerableVersions,
|
|
336
|
+
plugin: {
|
|
337
|
+
name: 'verdaccio-security-filter',
|
|
338
|
+
version: '2.0.0',
|
|
339
|
+
},
|
|
340
|
+
blockedAt: new Date().toISOString(),
|
|
341
|
+
} });
|
|
342
|
+
}
|
|
343
|
+
this.logger.warn(`[filter_metadata] CVE FILTERED: ${packageName} - ${reason}: ${vulnerableVersions.join(', ')}`);
|
|
344
|
+
const filteredVersions = Object.assign({}, pkg.versions);
|
|
345
|
+
for (const version of vulnerableVersions) {
|
|
346
|
+
delete filteredVersions[version];
|
|
347
|
+
this.metrics.recordBlock(packageName, version, reason);
|
|
348
|
+
}
|
|
349
|
+
const filteredDistTags = Object.assign({}, pkg['dist-tags']);
|
|
350
|
+
for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
|
|
351
|
+
if (vulnerableVersions.includes(tagVersion)) {
|
|
352
|
+
delete filteredDistTags[tag];
|
|
353
|
+
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to vulnerable version ${tagVersion}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
|
|
333
357
|
}
|
|
334
358
|
}
|
|
335
359
|
if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
|
|
336
|
-
const latestVersion = (
|
|
337
|
-
if (latestVersion && ((
|
|
360
|
+
const latestVersion = (_c = pkg['dist-tags']) === null || _c === void 0 ? void 0 : _c.latest;
|
|
361
|
+
if (latestVersion && ((_d = pkg.versions) === null || _d === void 0 ? void 0 : _d[latestVersion])) {
|
|
338
362
|
const versionData = pkg.versions[latestVersion];
|
|
339
363
|
const licenseResult = this.licenseChecker.checkLicense(versionData);
|
|
340
364
|
if (!licenseResult.allowed) {
|
|
@@ -353,7 +377,7 @@ class SecurityFilterPlugin {
|
|
|
353
377
|
}
|
|
354
378
|
}
|
|
355
379
|
}
|
|
356
|
-
if ((
|
|
380
|
+
if ((_e = this.config.packageAge) === null || _e === void 0 ? void 0 : _e.enabled) {
|
|
357
381
|
const ageResult = this.packageAgeChecker.checkPackageAge(pkg);
|
|
358
382
|
if (!ageResult.allowed && !ageResult.warnOnly) {
|
|
359
383
|
this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${ageResult.reason}`);
|
|
@@ -400,9 +424,9 @@ class SecurityFilterPlugin {
|
|
|
400
424
|
}
|
|
401
425
|
}
|
|
402
426
|
}
|
|
403
|
-
if ((
|
|
404
|
-
const latestVersion = (
|
|
405
|
-
if (latestVersion && ((
|
|
427
|
+
if ((_f = this.config.authorFilter) === null || _f === void 0 ? void 0 : _f.enabled) {
|
|
428
|
+
const latestVersion = (_g = pkg['dist-tags']) === null || _g === void 0 ? void 0 : _g.latest;
|
|
429
|
+
if (latestVersion && ((_h = pkg.versions) === null || _h === void 0 ? void 0 : _h[latestVersion])) {
|
|
406
430
|
const versionData = pkg.versions[latestVersion];
|
|
407
431
|
const authorResult = this.authorChecker.checkAuthor(versionData);
|
|
408
432
|
if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
|
|
@@ -440,7 +464,7 @@ class SecurityFilterPlugin {
|
|
|
440
464
|
}
|
|
441
465
|
catch (error) {
|
|
442
466
|
this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
|
|
443
|
-
const errorStrategy = ((
|
|
467
|
+
const errorStrategy = ((_j = this.config.errorHandling) === null || _j === void 0 ? void 0 : _j.onFilterError) || 'fail-open';
|
|
444
468
|
if (errorStrategy === 'fail-closed') {
|
|
445
469
|
this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
|
|
446
470
|
this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
|
|
@@ -2,19 +2,29 @@ import { CVECheckConfig, CVECheckResult } from '../types';
|
|
|
2
2
|
export declare class CVEChecker {
|
|
3
3
|
private config;
|
|
4
4
|
private cache;
|
|
5
|
-
private
|
|
5
|
+
private failedUntil;
|
|
6
|
+
private inFlight;
|
|
7
|
+
private matches;
|
|
6
8
|
private readonly MAX_CACHE_SIZE;
|
|
7
9
|
private saveTimer;
|
|
8
10
|
private readonly SAVE_DEBOUNCE_MS;
|
|
11
|
+
private dirty;
|
|
9
12
|
constructor(config?: CVECheckConfig);
|
|
10
13
|
checkPackage(packageName: string, version: string): Promise<CVECheckResult>;
|
|
11
|
-
|
|
14
|
+
checkVersions(packageName: string, versions: string[]): Promise<CVECheckResult[]>;
|
|
15
|
+
private getPackageEntry;
|
|
16
|
+
private refresh;
|
|
12
17
|
private fetchFromOSV;
|
|
13
|
-
private
|
|
18
|
+
private queryOSV;
|
|
19
|
+
private toCachedVulnerability;
|
|
14
20
|
private mapOSVSeverity;
|
|
15
|
-
private
|
|
21
|
+
private mapDatabaseSpecificSeverity;
|
|
22
|
+
private scoreToSeverity;
|
|
23
|
+
private computeCVSSv3BaseScore;
|
|
24
|
+
private get cacheFile();
|
|
16
25
|
private loadCache;
|
|
17
26
|
private debouncedSaveCache;
|
|
27
|
+
private serializeCache;
|
|
18
28
|
private saveCache;
|
|
19
29
|
clearCache(): void;
|
|
20
30
|
destroy(): void;
|
package/build/lib/cve-checker.js
CHANGED
|
@@ -45,13 +45,73 @@ 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 semver = __importStar(require("semver"));
|
|
48
49
|
const lru_cache_1 = require("./lru-cache");
|
|
50
|
+
const CVSS_V3_WEIGHTS = {
|
|
51
|
+
AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
|
|
52
|
+
AC: { L: 0.77, H: 0.44 },
|
|
53
|
+
UI: { N: 0.85, R: 0.62 },
|
|
54
|
+
PR_UNCHANGED: { N: 0.85, L: 0.62, H: 0.27 },
|
|
55
|
+
PR_CHANGED: { N: 0.85, L: 0.68, H: 0.5 },
|
|
56
|
+
CIA: { H: 0.56, L: 0.22, N: 0 },
|
|
57
|
+
};
|
|
58
|
+
function roundUpCvssScore(value) {
|
|
59
|
+
const intValue = Math.round(value * 100000);
|
|
60
|
+
if (intValue % 10000 === 0) {
|
|
61
|
+
return intValue / 100000;
|
|
62
|
+
}
|
|
63
|
+
return (Math.floor(intValue / 10000) + 1) / 10;
|
|
64
|
+
}
|
|
65
|
+
const OSV_QUERY_URL = 'https://api.osv.dev/v1/query';
|
|
66
|
+
const OSV_TIMEOUT_MS = 5000;
|
|
67
|
+
const OSV_ATTEMPTS = 2;
|
|
68
|
+
const OSV_RETRY_DELAY_MS = 500;
|
|
69
|
+
const FAILURE_TTL_MS = 60000;
|
|
70
|
+
function compareVersions(a, b) {
|
|
71
|
+
try {
|
|
72
|
+
return semver.compare(a, b);
|
|
73
|
+
}
|
|
74
|
+
catch (_a) {
|
|
75
|
+
return NaN;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isInRange(events, version) {
|
|
79
|
+
let affected = false;
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
if (event.introduced !== undefined) {
|
|
82
|
+
if (event.introduced === '0' || compareVersions(version, event.introduced) >= 0)
|
|
83
|
+
affected = true;
|
|
84
|
+
}
|
|
85
|
+
else if (event.fixed !== undefined) {
|
|
86
|
+
if (compareVersions(version, event.fixed) >= 0)
|
|
87
|
+
affected = false;
|
|
88
|
+
}
|
|
89
|
+
else if (event.last_affected !== undefined) {
|
|
90
|
+
if (compareVersions(version, event.last_affected) > 0)
|
|
91
|
+
affected = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return affected;
|
|
95
|
+
}
|
|
96
|
+
function isAffected(vuln, version, parsed) {
|
|
97
|
+
const informative = vuln.affected.filter(a => a.versions || a.ranges.length > 0);
|
|
98
|
+
if (informative.length === 0) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return informative.some(a => {
|
|
102
|
+
var _a;
|
|
103
|
+
return ((_a = a.versions) === null || _a === void 0 ? void 0 : _a.includes(version)) ||
|
|
104
|
+
(parsed !== null && a.ranges.some(events => isInRange(events, parsed)));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
49
107
|
class CVEChecker {
|
|
50
108
|
constructor(config) {
|
|
51
|
-
this.
|
|
109
|
+
this.inFlight = new Map();
|
|
110
|
+
this.matches = new WeakMap();
|
|
52
111
|
this.MAX_CACHE_SIZE = 1000;
|
|
53
112
|
this.saveTimer = null;
|
|
54
113
|
this.SAVE_DEBOUNCE_MS = 5000;
|
|
114
|
+
this.dirty = false;
|
|
55
115
|
this.config = {
|
|
56
116
|
enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
|
|
57
117
|
databases: (config === null || config === void 0 ? void 0 : config.databases) || ['osv'],
|
|
@@ -61,166 +121,249 @@ class CVEChecker {
|
|
|
61
121
|
cacheDir: (config === null || config === void 0 ? void 0 : config.cacheDir) || './.security-cache',
|
|
62
122
|
};
|
|
63
123
|
this.cache = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
|
|
124
|
+
this.failedUntil = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
|
|
64
125
|
if (this.config.enabled) {
|
|
65
126
|
this.loadCache();
|
|
66
127
|
}
|
|
67
128
|
}
|
|
68
129
|
checkPackage(packageName, version) {
|
|
69
130
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
-
|
|
71
|
-
|
|
131
|
+
return (yield this.checkVersions(packageName, [version]))[0];
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
checkVersions(packageName, versions) {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
const checkedAt = new Date().toISOString();
|
|
137
|
+
if (!this.config.enabled || !this.config.databases.includes('osv')) {
|
|
138
|
+
return versions.map(version => ({
|
|
72
139
|
package: packageName,
|
|
73
140
|
version,
|
|
74
141
|
vulnerabilities: [],
|
|
75
142
|
isVulnerable: false,
|
|
76
|
-
checkedAt
|
|
77
|
-
};
|
|
143
|
+
checkedAt,
|
|
144
|
+
}));
|
|
78
145
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
146
|
+
const entry = yield this.getPackageEntry(packageName);
|
|
147
|
+
let memo = this.matches.get(entry);
|
|
148
|
+
if (!memo) {
|
|
149
|
+
memo = new Map();
|
|
150
|
+
this.matches.set(entry, memo);
|
|
82
151
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
152
|
+
const relevant = entry.vulns.filter(vuln => this.config.severity.includes(vuln.severity));
|
|
153
|
+
return versions.map(version => {
|
|
154
|
+
let vulnerabilities = memo.get(version);
|
|
155
|
+
if (!vulnerabilities) {
|
|
156
|
+
const parsed = semver.parse(version);
|
|
157
|
+
vulnerabilities = relevant.filter(vuln => isAffected(vuln, version, parsed));
|
|
158
|
+
memo.set(version, vulnerabilities);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
package: packageName,
|
|
162
|
+
version,
|
|
163
|
+
vulnerabilities,
|
|
164
|
+
isVulnerable: vulnerabilities.length > 0,
|
|
165
|
+
checkedAt,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
95
168
|
});
|
|
96
169
|
}
|
|
97
|
-
|
|
170
|
+
getPackageEntry(packageName) {
|
|
171
|
+
const cached = this.cache.get(packageName);
|
|
172
|
+
const ttlMs = this.config.updateInterval * 60 * 60 * 1000;
|
|
173
|
+
if (cached && Date.now() - cached.checkedAt <= ttlMs) {
|
|
174
|
+
return Promise.resolve(cached);
|
|
175
|
+
}
|
|
176
|
+
let pending = this.inFlight.get(packageName);
|
|
177
|
+
if (!pending) {
|
|
178
|
+
pending = this.refresh(packageName, cached).finally(() => this.inFlight.delete(packageName));
|
|
179
|
+
this.inFlight.set(packageName, pending);
|
|
180
|
+
}
|
|
181
|
+
return pending;
|
|
182
|
+
}
|
|
183
|
+
refresh(packageName, stale) {
|
|
98
184
|
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
-
|
|
100
|
-
|
|
185
|
+
var _a;
|
|
186
|
+
let failure;
|
|
187
|
+
if (((_a = this.failedUntil.get(packageName)) !== null && _a !== void 0 ? _a : 0) > Date.now()) {
|
|
188
|
+
failure = new Error('OSV lookup failed recently, not retrying yet');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
101
191
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
break;
|
|
108
|
-
case 'github':
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
192
|
+
const entry = { checkedAt: Date.now(), vulns: yield this.fetchFromOSV(packageName) };
|
|
193
|
+
this.cache.set(packageName, entry);
|
|
194
|
+
this.failedUntil.delete(packageName);
|
|
195
|
+
this.debouncedSaveCache();
|
|
196
|
+
return entry;
|
|
111
197
|
}
|
|
112
198
|
catch (error) {
|
|
113
|
-
|
|
114
|
-
|
|
199
|
+
failure = error;
|
|
200
|
+
this.failedUntil.set(packageName, Date.now() + FAILURE_TTL_MS);
|
|
115
201
|
}
|
|
116
202
|
}
|
|
117
|
-
|
|
203
|
+
if (stale) {
|
|
204
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
205
|
+
console.warn(`[CVE Checker] Using stale data for ${packageName}: ${message}`);
|
|
206
|
+
return stale;
|
|
207
|
+
}
|
|
208
|
+
throw failure;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
fetchFromOSV(packageName) {
|
|
212
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
+
const vulns = [];
|
|
214
|
+
let pageToken;
|
|
215
|
+
do {
|
|
216
|
+
const data = yield this.queryOSV({
|
|
217
|
+
package: { name: packageName, ecosystem: 'npm' },
|
|
218
|
+
page_token: pageToken,
|
|
219
|
+
});
|
|
220
|
+
for (const vuln of data.vulns || []) {
|
|
221
|
+
vulns.push(this.toCachedVulnerability(vuln, packageName));
|
|
222
|
+
}
|
|
223
|
+
pageToken = data.next_page_token;
|
|
224
|
+
} while (pageToken);
|
|
225
|
+
return vulns;
|
|
118
226
|
});
|
|
119
227
|
}
|
|
120
|
-
|
|
228
|
+
queryOSV(body) {
|
|
121
229
|
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
-
|
|
123
|
-
const retryDelay = 1000;
|
|
124
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
230
|
+
for (let attempt = 1;; attempt++) {
|
|
125
231
|
try {
|
|
126
|
-
const
|
|
127
|
-
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
128
|
-
const response = yield fetch('https://api.osv.dev/v1/query', {
|
|
232
|
+
const response = yield fetch(OSV_QUERY_URL, {
|
|
129
233
|
method: 'POST',
|
|
130
|
-
headers: {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
body: JSON.stringify({
|
|
134
|
-
package: {
|
|
135
|
-
name: packageName,
|
|
136
|
-
ecosystem: 'npm',
|
|
137
|
-
},
|
|
138
|
-
version: version,
|
|
139
|
-
}),
|
|
140
|
-
signal: controller.signal,
|
|
234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
signal: AbortSignal.timeout(OSV_TIMEOUT_MS),
|
|
141
237
|
});
|
|
142
|
-
clearTimeout(timeoutId);
|
|
143
238
|
if (!response.ok) {
|
|
144
|
-
if (response.status === 429 && attempt < maxRetries) {
|
|
145
|
-
const waitTime = retryDelay * Math.pow(2, attempt - 1);
|
|
146
|
-
console.warn(`[CVE Checker] Rate limited, retrying in ${waitTime}ms (attempt ${attempt}/${maxRetries})`);
|
|
147
|
-
yield this.sleep(waitTime);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
239
|
throw new Error(`OSV API returned ${response.status}`);
|
|
151
240
|
}
|
|
152
|
-
|
|
153
|
-
if (!data.vulns || data.vulns.length === 0) {
|
|
154
|
-
return [];
|
|
155
|
-
}
|
|
156
|
-
return data.vulns.map((vuln) => {
|
|
157
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
158
|
-
return ({
|
|
159
|
-
id: vuln.id,
|
|
160
|
-
severity: this.mapOSVSeverity(vuln.severity),
|
|
161
|
-
summary: vuln.summary || vuln.details || 'No description available',
|
|
162
|
-
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
|
-
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,
|
|
164
|
-
publishedDate: vuln.published || new Date().toISOString(),
|
|
165
|
-
source: 'osv',
|
|
166
|
-
});
|
|
167
|
-
});
|
|
241
|
+
return yield response.json();
|
|
168
242
|
}
|
|
169
243
|
catch (error) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.error(`[CVE Checker] OSV API error after ${maxRetries} attempts:`, message);
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
176
|
-
console.warn(`[CVE Checker] Request timeout, retrying (attempt ${attempt}/${maxRetries})`);
|
|
244
|
+
if (attempt >= OSV_ATTEMPTS) {
|
|
245
|
+
throw error;
|
|
177
246
|
}
|
|
178
|
-
|
|
179
|
-
console.warn(`[CVE Checker] Request failed, retrying (attempt ${attempt}/${maxRetries}):`, message);
|
|
180
|
-
}
|
|
181
|
-
yield this.sleep(retryDelay * attempt);
|
|
247
|
+
yield new Promise(resolve => setTimeout(resolve, OSV_RETRY_DELAY_MS));
|
|
182
248
|
}
|
|
183
249
|
}
|
|
184
|
-
return [];
|
|
185
250
|
});
|
|
186
251
|
}
|
|
187
|
-
|
|
188
|
-
|
|
252
|
+
toCachedVulnerability(vuln, packageName) {
|
|
253
|
+
var _a;
|
|
254
|
+
const affected = (vuln.affected || [])
|
|
255
|
+
.filter(a => { var _a; return !((_a = a.package) === null || _a === void 0 ? void 0 : _a.name) || a.package.name === packageName; })
|
|
256
|
+
.map(a => {
|
|
257
|
+
var _a;
|
|
258
|
+
return ({
|
|
259
|
+
versions: ((_a = a.versions) === null || _a === void 0 ? void 0 : _a.length) ? a.versions : undefined,
|
|
260
|
+
ranges: (a.ranges || []).filter(r => r.type !== 'GIT').map(r => r.events || []),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
const events = affected.flatMap(a => a.ranges.flat());
|
|
264
|
+
return {
|
|
265
|
+
id: vuln.id || 'unknown',
|
|
266
|
+
severity: this.mapOSVSeverity(vuln),
|
|
267
|
+
summary: vuln.summary || 'No description available',
|
|
268
|
+
affectedVersions: events.map(e => e.introduced || e.fixed).filter((v) => !!v),
|
|
269
|
+
fixedVersion: (_a = events.find(e => e.fixed)) === null || _a === void 0 ? void 0 : _a.fixed,
|
|
270
|
+
publishedDate: vuln.published || new Date().toISOString(),
|
|
271
|
+
source: 'osv',
|
|
272
|
+
affected,
|
|
273
|
+
};
|
|
189
274
|
}
|
|
190
|
-
mapOSVSeverity(
|
|
191
|
-
|
|
192
|
-
|
|
275
|
+
mapOSVSeverity(vuln) {
|
|
276
|
+
var _a, _b, _c, _d;
|
|
277
|
+
const databaseSeverity = this.mapDatabaseSpecificSeverity((_a = vuln === null || vuln === void 0 ? void 0 : vuln.database_specific) === null || _a === void 0 ? void 0 : _a.severity);
|
|
278
|
+
if (databaseSeverity) {
|
|
279
|
+
return databaseSeverity;
|
|
193
280
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (score >= 4.0)
|
|
202
|
-
return 'medium';
|
|
203
|
-
return 'low';
|
|
281
|
+
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');
|
|
282
|
+
const score = cvssV3 ? this.computeCVSSv3BaseScore(cvssV3.score) : null;
|
|
283
|
+
if (score !== null) {
|
|
284
|
+
return this.scoreToSeverity(score);
|
|
285
|
+
}
|
|
286
|
+
if ((_c = vuln === null || vuln === void 0 ? void 0 : vuln.severity) === null || _c === void 0 ? void 0 : _c.some(entry => entry.type === 'CVSS_V4')) {
|
|
287
|
+
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
288
|
}
|
|
205
289
|
return 'medium';
|
|
206
290
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
291
|
+
mapDatabaseSpecificSeverity(severity) {
|
|
292
|
+
if (typeof severity !== 'string') {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
switch (severity.toUpperCase()) {
|
|
296
|
+
case 'LOW': return 'low';
|
|
297
|
+
case 'MODERATE': return 'medium';
|
|
298
|
+
case 'HIGH': return 'high';
|
|
299
|
+
case 'CRITICAL': return 'critical';
|
|
300
|
+
default: return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
scoreToSeverity(score) {
|
|
304
|
+
if (score >= 9.0)
|
|
305
|
+
return 'critical';
|
|
306
|
+
if (score >= 7.0)
|
|
307
|
+
return 'high';
|
|
308
|
+
if (score >= 4.0)
|
|
309
|
+
return 'medium';
|
|
310
|
+
return 'low';
|
|
311
|
+
}
|
|
312
|
+
computeCVSSv3BaseScore(vector) {
|
|
313
|
+
if (typeof vector !== 'string') {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const metrics = {};
|
|
317
|
+
for (const part of vector.split('/')) {
|
|
318
|
+
const [key, value] = part.split(':');
|
|
319
|
+
if (key && value) {
|
|
320
|
+
metrics[key] = value;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const scope = metrics.S;
|
|
324
|
+
const av = CVSS_V3_WEIGHTS.AV[metrics.AV];
|
|
325
|
+
const ac = CVSS_V3_WEIGHTS.AC[metrics.AC];
|
|
326
|
+
const ui = CVSS_V3_WEIGHTS.UI[metrics.UI];
|
|
327
|
+
const pr = scope === 'C'
|
|
328
|
+
? CVSS_V3_WEIGHTS.PR_CHANGED[metrics.PR]
|
|
329
|
+
: CVSS_V3_WEIGHTS.PR_UNCHANGED[metrics.PR];
|
|
330
|
+
const c = CVSS_V3_WEIGHTS.CIA[metrics.C];
|
|
331
|
+
const i = CVSS_V3_WEIGHTS.CIA[metrics.I];
|
|
332
|
+
const a = CVSS_V3_WEIGHTS.CIA[metrics.A];
|
|
333
|
+
if ((scope !== 'U' && scope !== 'C') || [av, ac, ui, pr, c, i, a].some(value => value === undefined)) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const impactSubScoreBase = 1 - (1 - c) * (1 - i) * (1 - a);
|
|
337
|
+
const impactSubScore = scope === 'U'
|
|
338
|
+
? 6.42 * impactSubScoreBase
|
|
339
|
+
: 7.52 * (impactSubScoreBase - 0.029) - 3.25 * Math.pow(impactSubScoreBase - 0.02, 15);
|
|
340
|
+
if (impactSubScore <= 0) {
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
const exploitability = 8.22 * av * ac * pr * ui;
|
|
344
|
+
const rawScore = scope === 'U'
|
|
345
|
+
? impactSubScore + exploitability
|
|
346
|
+
: 1.08 * (impactSubScore + exploitability);
|
|
347
|
+
const cappedScore = Math.min(rawScore, 10);
|
|
348
|
+
return metrics.CVSS === '3.0'
|
|
349
|
+
? Math.ceil(cappedScore * 10) / 10
|
|
350
|
+
: roundUpCvssScore(cappedScore);
|
|
351
|
+
}
|
|
352
|
+
get cacheFile() {
|
|
353
|
+
return path.join(this.config.cacheDir, 'cve-cache.json');
|
|
211
354
|
}
|
|
212
355
|
loadCache() {
|
|
213
356
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
357
|
+
if (!fs.existsSync(this.cacheFile)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const parsed = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'));
|
|
361
|
+
for (const [key, value] of Object.entries((parsed === null || parsed === void 0 ? void 0 : parsed.cache) || {}).slice(-this.MAX_CACHE_SIZE)) {
|
|
362
|
+
const entry = value;
|
|
363
|
+
if (typeof (entry === null || entry === void 0 ? void 0 : entry.checkedAt) === 'number' && Array.isArray(entry.vulns)
|
|
364
|
+
&& entry.vulns.every(vuln => Array.isArray(vuln === null || vuln === void 0 ? void 0 : vuln.affected))) {
|
|
365
|
+
this.cache.set(key, entry);
|
|
366
|
+
}
|
|
224
367
|
}
|
|
225
368
|
}
|
|
226
369
|
catch (error) {
|
|
@@ -229,44 +372,54 @@ class CVEChecker {
|
|
|
229
372
|
}
|
|
230
373
|
}
|
|
231
374
|
debouncedSaveCache() {
|
|
375
|
+
this.dirty = true;
|
|
232
376
|
if (this.saveTimer) {
|
|
233
377
|
clearTimeout(this.saveTimer);
|
|
234
378
|
}
|
|
235
379
|
this.saveTimer = setTimeout(() => {
|
|
236
|
-
this.saveCache();
|
|
237
380
|
this.saveTimer = null;
|
|
381
|
+
void this.saveCache();
|
|
238
382
|
}, this.SAVE_DEBOUNCE_MS);
|
|
383
|
+
this.saveTimer.unref();
|
|
384
|
+
}
|
|
385
|
+
serializeCache() {
|
|
386
|
+
return JSON.stringify({ cache: Object.fromEntries(this.cache.entries()) });
|
|
239
387
|
}
|
|
240
388
|
saveCache() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
fs.
|
|
389
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
390
|
+
try {
|
|
391
|
+
this.dirty = false;
|
|
392
|
+
const tmpFile = `${this.cacheFile}.${process.pid}.tmp`;
|
|
393
|
+
yield fs.promises.mkdir(path.dirname(this.cacheFile), { recursive: true });
|
|
394
|
+
yield fs.promises.writeFile(tmpFile, this.serializeCache(), 'utf8');
|
|
395
|
+
yield fs.promises.rename(tmpFile, this.cacheFile);
|
|
246
396
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
this.lastUpdate = Date.now();
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
256
|
-
console.error('[CVE Checker] Failed to save cache:', message);
|
|
257
|
-
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
399
|
+
console.error('[CVE Checker] Failed to save cache:', message);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
258
402
|
}
|
|
259
403
|
clearCache() {
|
|
260
404
|
this.cache.clear();
|
|
261
|
-
this.
|
|
262
|
-
this.
|
|
405
|
+
this.failedUntil.clear();
|
|
406
|
+
this.debouncedSaveCache();
|
|
263
407
|
}
|
|
264
408
|
destroy() {
|
|
265
409
|
if (this.saveTimer) {
|
|
266
410
|
clearTimeout(this.saveTimer);
|
|
267
411
|
this.saveTimer = null;
|
|
268
412
|
}
|
|
269
|
-
this.
|
|
413
|
+
if (this.dirty) {
|
|
414
|
+
try {
|
|
415
|
+
fs.mkdirSync(path.dirname(this.cacheFile), { recursive: true });
|
|
416
|
+
fs.writeFileSync(this.cacheFile, this.serializeCache(), 'utf8');
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
420
|
+
console.error('[CVE Checker] Failed to save cache:', message);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
270
423
|
this.cache.clear();
|
|
271
424
|
}
|
|
272
425
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verdaccio-security-filter",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
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": {
|