verdaccio-security-filter 1.3.8 → 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/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,21 +300,29 @@ 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)`;
@@ -349,8 +357,8 @@ class SecurityFilterPlugin {
349
357
  }
350
358
  }
351
359
  if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
352
- const latestVersion = (_b = pkg['dist-tags']) === null || _b === void 0 ? void 0 : _b.latest;
353
- 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])) {
354
362
  const versionData = pkg.versions[latestVersion];
355
363
  const licenseResult = this.licenseChecker.checkLicense(versionData);
356
364
  if (!licenseResult.allowed) {
@@ -369,7 +377,7 @@ class SecurityFilterPlugin {
369
377
  }
370
378
  }
371
379
  }
372
- 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) {
373
381
  const ageResult = this.packageAgeChecker.checkPackageAge(pkg);
374
382
  if (!ageResult.allowed && !ageResult.warnOnly) {
375
383
  this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${ageResult.reason}`);
@@ -416,9 +424,9 @@ class SecurityFilterPlugin {
416
424
  }
417
425
  }
418
426
  }
419
- if ((_e = this.config.authorFilter) === null || _e === void 0 ? void 0 : _e.enabled) {
420
- const latestVersion = (_f = pkg['dist-tags']) === null || _f === void 0 ? void 0 : _f.latest;
421
- 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])) {
422
430
  const versionData = pkg.versions[latestVersion];
423
431
  const authorResult = this.authorChecker.checkAuthor(versionData);
424
432
  if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
@@ -456,7 +464,7 @@ class SecurityFilterPlugin {
456
464
  }
457
465
  catch (error) {
458
466
  this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
459
- 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';
460
468
  if (errorStrategy === 'fail-closed') {
461
469
  this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
462
470
  this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
@@ -2,22 +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
21
  private mapDatabaseSpecificSeverity;
16
22
  private scoreToSeverity;
17
23
  private computeCVSSv3BaseScore;
18
- private isCacheStale;
24
+ private get cacheFile();
19
25
  private loadCache;
20
26
  private debouncedSaveCache;
27
+ private serializeCache;
21
28
  private saveCache;
22
29
  clearCache(): void;
23
30
  destroy(): void;
@@ -45,6 +45,7 @@ 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");
49
50
  const CVSS_V3_WEIGHTS = {
50
51
  AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
@@ -61,12 +62,56 @@ function roundUpCvssScore(value) {
61
62
  }
62
63
  return (Math.floor(intValue / 10000) + 1) / 10;
63
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
+ }
64
107
  class CVEChecker {
65
108
  constructor(config) {
66
- this.lastUpdate = 0;
109
+ this.inFlight = new Map();
110
+ this.matches = new WeakMap();
67
111
  this.MAX_CACHE_SIZE = 1000;
68
112
  this.saveTimer = null;
69
113
  this.SAVE_DEBOUNCE_MS = 5000;
114
+ this.dirty = false;
70
115
  this.config = {
71
116
  enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
72
117
  databases: (config === null || config === void 0 ? void 0 : config.databases) || ['osv'],
@@ -76,131 +121,156 @@ class CVEChecker {
76
121
  cacheDir: (config === null || config === void 0 ? void 0 : config.cacheDir) || './.security-cache',
77
122
  };
78
123
  this.cache = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
124
+ this.failedUntil = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
79
125
  if (this.config.enabled) {
80
126
  this.loadCache();
81
127
  }
82
128
  }
83
129
  checkPackage(packageName, version) {
84
130
  return __awaiter(this, void 0, void 0, function* () {
85
- if (!this.config.enabled) {
86
- 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 => ({
87
139
  package: packageName,
88
140
  version,
89
141
  vulnerabilities: [],
90
142
  isVulnerable: false,
91
- checkedAt: new Date().toISOString(),
92
- };
143
+ checkedAt,
144
+ }));
93
145
  }
94
- const cacheKey = `${packageName}@${version}`;
95
- if (this.cache.has(cacheKey) && !this.isCacheStale()) {
96
- 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);
97
151
  }
98
- const vulnerabilities = yield this.fetchVulnerabilities(packageName, version);
99
- const filteredVulnerabilities = vulnerabilities.filter(vuln => this.config.severity.includes(vuln.severity));
100
- const result = {
101
- package: packageName,
102
- version,
103
- vulnerabilities: filteredVulnerabilities,
104
- isVulnerable: filteredVulnerabilities.length > 0,
105
- checkedAt: new Date().toISOString(),
106
- };
107
- this.cache.set(cacheKey, result);
108
- this.debouncedSaveCache();
109
- 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
+ });
110
168
  });
111
169
  }
112
- 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) {
113
184
  return __awaiter(this, void 0, void 0, function* () {
114
- const vulnerabilities = [];
115
- 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 {
116
191
  try {
117
- switch (database) {
118
- case 'osv':
119
- vulnerabilities.push(...yield this.fetchFromOSV(packageName, version));
120
- break;
121
- case 'snyk':
122
- break;
123
- case 'github':
124
- break;
125
- }
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;
126
197
  }
127
198
  catch (error) {
128
- const message = error instanceof Error ? error.message : String(error);
129
- console.error(`[CVE Checker] Error fetching from ${database}:`, message);
199
+ failure = error;
200
+ this.failedUntil.set(packageName, Date.now() + FAILURE_TTL_MS);
130
201
  }
131
202
  }
132
- 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;
133
209
  });
134
210
  }
135
- fetchFromOSV(packageName, version) {
211
+ fetchFromOSV(packageName) {
136
212
  return __awaiter(this, void 0, void 0, function* () {
137
- const maxRetries = 3;
138
- const retryDelay = 1000;
139
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
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;
226
+ });
227
+ }
228
+ queryOSV(body) {
229
+ return __awaiter(this, void 0, void 0, function* () {
230
+ for (let attempt = 1;; attempt++) {
140
231
  try {
141
- const controller = new AbortController();
142
- const timeoutId = setTimeout(() => controller.abort(), 10000);
143
- const response = yield fetch('https://api.osv.dev/v1/query', {
232
+ const response = yield fetch(OSV_QUERY_URL, {
144
233
  method: 'POST',
145
- headers: {
146
- 'Content-Type': 'application/json',
147
- },
148
- body: JSON.stringify({
149
- package: {
150
- name: packageName,
151
- ecosystem: 'npm',
152
- },
153
- version: version,
154
- }),
155
- signal: controller.signal,
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify(body),
236
+ signal: AbortSignal.timeout(OSV_TIMEOUT_MS),
156
237
  });
157
- clearTimeout(timeoutId);
158
238
  if (!response.ok) {
159
- if (response.status === 429 && attempt < maxRetries) {
160
- const waitTime = retryDelay * Math.pow(2, attempt - 1);
161
- console.warn(`[CVE Checker] Rate limited, retrying in ${waitTime}ms (attempt ${attempt}/${maxRetries})`);
162
- yield this.sleep(waitTime);
163
- continue;
164
- }
165
239
  throw new Error(`OSV API returned ${response.status}`);
166
240
  }
167
- const data = yield response.json();
168
- if (!data.vulns || data.vulns.length === 0) {
169
- return [];
170
- }
171
- return data.vulns.map((vuln) => {
172
- var _a, _b, _c, _d, _e, _f, _g;
173
- return ({
174
- id: vuln.id,
175
- severity: this.mapOSVSeverity(vuln),
176
- summary: vuln.summary || vuln.details || 'No description available',
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()) || [],
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,
179
- publishedDate: vuln.published || new Date().toISOString(),
180
- source: 'osv',
181
- });
182
- });
241
+ return yield response.json();
183
242
  }
184
243
  catch (error) {
185
- const message = error instanceof Error ? error.message : String(error);
186
- if (attempt === maxRetries) {
187
- console.error(`[CVE Checker] OSV API error after ${maxRetries} attempts:`, message);
188
- return [];
244
+ if (attempt >= OSV_ATTEMPTS) {
245
+ throw error;
189
246
  }
190
- if (error instanceof Error && error.name === 'AbortError') {
191
- console.warn(`[CVE Checker] Request timeout, retrying (attempt ${attempt}/${maxRetries})`);
192
- }
193
- else {
194
- console.warn(`[CVE Checker] Request failed, retrying (attempt ${attempt}/${maxRetries}):`, message);
195
- }
196
- yield this.sleep(retryDelay * attempt);
247
+ yield new Promise(resolve => setTimeout(resolve, OSV_RETRY_DELAY_MS));
197
248
  }
198
249
  }
199
- return [];
200
250
  });
201
251
  }
202
- sleep(ms) {
203
- 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
+ };
204
274
  }
205
275
  mapOSVSeverity(vuln) {
206
276
  var _a, _b, _c, _d;
@@ -279,23 +349,21 @@ class CVEChecker {
279
349
  ? Math.ceil(cappedScore * 10) / 10
280
350
  : roundUpCvssScore(cappedScore);
281
351
  }
282
- isCacheStale() {
283
- const now = Date.now();
284
- const updateIntervalMs = this.config.updateInterval * 60 * 60 * 1000;
285
- return now - this.lastUpdate > updateIntervalMs;
352
+ get cacheFile() {
353
+ return path.join(this.config.cacheDir, 'cve-cache.json');
286
354
  }
287
355
  loadCache() {
288
356
  try {
289
- const cacheFile = path.join(this.config.cacheDir, 'cve-cache.json');
290
- if (fs.existsSync(cacheFile)) {
291
- const data = fs.readFileSync(cacheFile, 'utf8');
292
- const parsed = JSON.parse(data);
293
- const entries = Object.entries(parsed.cache);
294
- const sortedEntries = entries.slice(-this.MAX_CACHE_SIZE);
295
- sortedEntries.forEach(([key, value]) => {
296
- this.cache.set(key, value);
297
- });
298
- 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
+ }
299
367
  }
300
368
  }
301
369
  catch (error) {
@@ -304,44 +372,54 @@ class CVEChecker {
304
372
  }
305
373
  }
306
374
  debouncedSaveCache() {
375
+ this.dirty = true;
307
376
  if (this.saveTimer) {
308
377
  clearTimeout(this.saveTimer);
309
378
  }
310
379
  this.saveTimer = setTimeout(() => {
311
- this.saveCache();
312
380
  this.saveTimer = null;
381
+ void this.saveCache();
313
382
  }, this.SAVE_DEBOUNCE_MS);
383
+ this.saveTimer.unref();
384
+ }
385
+ serializeCache() {
386
+ return JSON.stringify({ cache: Object.fromEntries(this.cache.entries()) });
314
387
  }
315
388
  saveCache() {
316
- try {
317
- const cacheFile = path.join(this.config.cacheDir, 'cve-cache.json');
318
- const dir = path.dirname(cacheFile);
319
- if (!fs.existsSync(dir)) {
320
- 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);
321
396
  }
322
- const data = {
323
- cache: Object.fromEntries(this.cache.entries()),
324
- lastUpdate: Date.now(),
325
- };
326
- fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
327
- this.lastUpdate = Date.now();
328
- }
329
- catch (error) {
330
- const message = error instanceof Error ? error.message : String(error);
331
- console.error('[CVE Checker] Failed to save cache:', message);
332
- }
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
+ });
333
402
  }
334
403
  clearCache() {
335
404
  this.cache.clear();
336
- this.lastUpdate = 0;
337
- this.saveCache();
405
+ this.failedUntil.clear();
406
+ this.debouncedSaveCache();
338
407
  }
339
408
  destroy() {
340
409
  if (this.saveTimer) {
341
410
  clearTimeout(this.saveTimer);
342
411
  this.saveTimer = null;
343
412
  }
344
- 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
+ }
345
423
  this.cache.clear();
346
424
  }
347
425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.3.8",
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",