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 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
@@ -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
- const vulnerableVersions = [];
304
- const BATCH_SIZE = 10;
305
- for (let i = 0; i < versions.length; i += BATCH_SIZE) {
306
- const batch = versions.slice(i, i + BATCH_SIZE);
307
- const results = yield Promise.allSettled(batch.map(version => this.cveChecker.checkPackage(packageName, version)));
308
- results.forEach((result, index) => {
309
- if (result.status === 'fulfilled' && result.value.isVulnerable) {
310
- const version = batch[index];
311
- vulnerableVersions.push(version);
312
- this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${result.value.vulnerabilities.length} vulnerabilities`);
313
- }
314
- else if (result.status === 'rejected') {
315
- this.logger.error(`[filter_metadata] CVE check failed for ${packageName}@${batch[index]}: ${result.reason}`);
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
- 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
- } });
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 = (_b = pkg['dist-tags']) === null || _b === void 0 ? void 0 : _b.latest;
337
- if (latestVersion && ((_c = pkg.versions) === null || _c === void 0 ? void 0 : _c[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 ((_d = this.config.packageAge) === null || _d === void 0 ? void 0 : _d.enabled) {
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 ((_e = this.config.authorFilter) === null || _e === void 0 ? void 0 : _e.enabled) {
404
- const latestVersion = (_f = pkg['dist-tags']) === null || _f === void 0 ? void 0 : _f.latest;
405
- if (latestVersion && ((_g = pkg.versions) === null || _g === void 0 ? void 0 : _g[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 = ((_h = this.config.errorHandling) === null || _h === void 0 ? void 0 : _h.onFilterError) || 'fail-open';
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 lastUpdate;
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
- private fetchVulnerabilities;
14
+ checkVersions(packageName: string, versions: string[]): Promise<CVECheckResult[]>;
15
+ private getPackageEntry;
16
+ private refresh;
12
17
  private fetchFromOSV;
13
- private sleep;
18
+ private queryOSV;
19
+ private toCachedVulnerability;
14
20
  private mapOSVSeverity;
15
- private isCacheStale;
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;
@@ -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.lastUpdate = 0;
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
- if (!this.config.enabled) {
71
- return {
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: new Date().toISOString(),
77
- };
143
+ checkedAt,
144
+ }));
78
145
  }
79
- const cacheKey = `${packageName}@${version}`;
80
- if (this.cache.has(cacheKey) && !this.isCacheStale()) {
81
- return this.cache.get(cacheKey);
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 vulnerabilities = yield this.fetchVulnerabilities(packageName, version);
84
- const filteredVulnerabilities = vulnerabilities.filter(vuln => this.config.severity.includes(vuln.severity));
85
- const result = {
86
- package: packageName,
87
- version,
88
- vulnerabilities: filteredVulnerabilities,
89
- isVulnerable: filteredVulnerabilities.length > 0,
90
- checkedAt: new Date().toISOString(),
91
- };
92
- this.cache.set(cacheKey, result);
93
- this.debouncedSaveCache();
94
- return result;
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
- fetchVulnerabilities(packageName, version) {
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
- const vulnerabilities = [];
100
- for (const database of this.config.databases) {
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
- switch (database) {
103
- case 'osv':
104
- vulnerabilities.push(...yield this.fetchFromOSV(packageName, version));
105
- break;
106
- case 'snyk':
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
- const message = error instanceof Error ? error.message : String(error);
114
- console.error(`[CVE Checker] Error fetching from ${database}:`, message);
199
+ failure = error;
200
+ this.failedUntil.set(packageName, Date.now() + FAILURE_TTL_MS);
115
201
  }
116
202
  }
117
- return vulnerabilities;
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
- fetchFromOSV(packageName, version) {
228
+ queryOSV(body) {
121
229
  return __awaiter(this, void 0, void 0, function* () {
122
- const maxRetries = 3;
123
- const retryDelay = 1000;
124
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
230
+ for (let attempt = 1;; attempt++) {
125
231
  try {
126
- const controller = new AbortController();
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
- 'Content-Type': 'application/json',
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
- const data = yield response.json();
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
- const message = error instanceof Error ? error.message : String(error);
171
- if (attempt === maxRetries) {
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
- else {
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
- sleep(ms) {
188
- return new Promise(resolve => setTimeout(resolve, ms));
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(osvSeverity) {
191
- if (!osvSeverity || osvSeverity.length === 0) {
192
- return 'medium';
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 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';
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
- isCacheStale() {
208
- const now = Date.now();
209
- const updateIntervalMs = this.config.updateInterval * 60 * 60 * 1000;
210
- return now - this.lastUpdate > updateIntervalMs;
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
- const cacheFile = path.join(this.config.cacheDir, 'cve-cache.json');
215
- if (fs.existsSync(cacheFile)) {
216
- const data = fs.readFileSync(cacheFile, 'utf8');
217
- const parsed = JSON.parse(data);
218
- const entries = Object.entries(parsed.cache);
219
- const sortedEntries = entries.slice(-this.MAX_CACHE_SIZE);
220
- sortedEntries.forEach(([key, value]) => {
221
- this.cache.set(key, value);
222
- });
223
- this.lastUpdate = parsed.lastUpdate || 0;
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
- try {
242
- const cacheFile = path.join(this.config.cacheDir, 'cve-cache.json');
243
- const dir = path.dirname(cacheFile);
244
- if (!fs.existsSync(dir)) {
245
- fs.mkdirSync(dir, { recursive: true });
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
- const data = {
248
- cache: Object.fromEntries(this.cache.entries()),
249
- lastUpdate: Date.now(),
250
- };
251
- fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
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.lastUpdate = 0;
262
- this.saveCache();
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.saveCache();
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.7",
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.14.2",
57
+ "qs": "6.16.0",
58
58
  "@cypress/request": "latest"
59
59
  },
60
60
  "author": {