verdaccio-security-filter 1.3.8 → 1.3.10

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.d.ts CHANGED
@@ -6,6 +6,7 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
6
6
  logger: SecurityLogger;
7
7
  config: SecurityConfig;
8
8
  private readonly securityRules;
9
+ private readonly blockedPatternRegexes;
9
10
  private readonly metrics;
10
11
  private readonly whitelistChecker;
11
12
  private readonly cveChecker;
@@ -20,7 +21,10 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
20
21
  private _isScopeAllowed;
21
22
  private _getVersionRangeRule;
22
23
  register_middlewares(app: Application, _auth: IBasicAuth<SecurityConfig>, _storage: IStorageManager<SecurityConfig>): void;
24
+ private _blockedPackage;
25
+ private _removeVersions;
23
26
  filter_metadata(packageInfo: Package): Promise<Package>;
27
+ private _versionBlockReason;
24
28
  private _checkPackageBlock;
25
29
  private _getAppliedRules;
26
30
  destroy(): void;
package/build/index.js CHANGED
@@ -47,6 +47,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.AuthorChecker = exports.PackageAgeChecker = exports.LicenseChecker = exports.CVEChecker = exports.WhitelistChecker = exports.MetricsCollector = exports.SecurityLogger = void 0;
49
49
  const semver = __importStar(require("semver"));
50
+ const fs_1 = require("fs");
51
+ const path_1 = require("path");
50
52
  const logger_1 = require("./lib/logger");
51
53
  const metrics_1 = require("./lib/metrics");
52
54
  const whitelist_checker_1 = require("./lib/whitelist-checker");
@@ -54,6 +56,7 @@ const cve_checker_1 = require("./lib/cve-checker");
54
56
  const license_checker_1 = require("./lib/license-checker");
55
57
  const package_age_checker_1 = require("./lib/package-age-checker");
56
58
  const author_checker_1 = require("./lib/author-checker");
59
+ const PLUGIN_VERSION = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf8')).version;
57
60
  class SecurityFilterPlugin {
58
61
  constructor(config, options) {
59
62
  this.config = config || {};
@@ -81,6 +84,15 @@ class SecurityFilterPlugin {
81
84
  enforceChecksum: this.config.enforceChecksum !== false,
82
85
  versionRangeRules: this._parseVersionRangeRules(this.config.versionRangeRules || [])
83
86
  };
87
+ this.blockedPatternRegexes = this.securityRules.blockedPatterns.flatMap(pattern => {
88
+ try {
89
+ return [new RegExp(pattern)];
90
+ }
91
+ catch (error) {
92
+ this.logger.warn(`Invalid blocked pattern "${pattern}", skipping: ${error.message}`);
93
+ return [];
94
+ }
95
+ });
84
96
  this._logInitialization();
85
97
  }
86
98
  _parseVersionRangeRules(rules) {
@@ -89,9 +101,8 @@ class SecurityFilterPlugin {
89
101
  this.logger.warn(`Invalid version range rule at index ${index}, skipping`);
90
102
  return null;
91
103
  }
92
- if (rule.strategy === 'fallback' && !rule.fallbackVersion) {
93
- this.logger.warn(`Fallback strategy requires fallbackVersion for ${rule.package}, skipping`);
94
- return null;
104
+ if (rule.strategy === 'fallback') {
105
+ this.logger.warn(`Strategy "fallback" for ${rule.package} is deprecated and works like "block": matching versions are hidden, so npm resolves to the highest allowed one`);
95
106
  }
96
107
  try {
97
108
  const validRange = semver.validRange(rule.range);
@@ -156,10 +167,7 @@ class SecurityFilterPlugin {
156
167
  return features;
157
168
  }
158
169
  _isBlockedByPattern(packageName) {
159
- return this.securityRules.blockedPatterns.some(pattern => {
160
- const regex = new RegExp(pattern);
161
- return regex.test(packageName);
162
- });
170
+ return this.blockedPatternRegexes.some(regex => regex.test(packageName));
163
171
  }
164
172
  _isScopeAllowed(packageName) {
165
173
  if (!packageName.startsWith('@')) {
@@ -212,9 +220,11 @@ class SecurityFilterPlugin {
212
220
  const tarballFile = urlMatch[3];
213
221
  let version;
214
222
  if (tarballFile) {
215
- const versionMatch = tarballFile.match(/-([\d.]+(?:-[a-zA-Z0-9.]+)?)\.tgz$/);
216
- version = versionMatch ? versionMatch[1] : undefined;
217
- this.logger.info(`[Middleware] TARBALL REQUEST: ${packageName}@${version || 'unknown'}`);
223
+ const filePrefix = `${urlMatch[2]}-`;
224
+ version = tarballFile.startsWith(filePrefix)
225
+ ? tarballFile.slice(filePrefix.length, -'.tgz'.length)
226
+ : undefined;
227
+ this.logger.debug(`[Middleware] TARBALL REQUEST: ${packageName}@${version || 'unknown'}`);
218
228
  const blockResult = yield this._checkPackageBlock(packageName, version);
219
229
  if (blockResult.blocked) {
220
230
  this.logger.warn(`[Middleware] [X] BLOCKED TARBALL: ${packageName}@${version || '*'} - ${blockResult.reason}`);
@@ -228,10 +238,10 @@ class SecurityFilterPlugin {
228
238
  });
229
239
  return;
230
240
  }
231
- this.logger.info(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
241
+ this.logger.debug(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
232
242
  }
233
243
  else {
234
- this.logger.info(`[Middleware] METADATA REQUEST: ${packageName} - will intercept response`);
244
+ this.logger.debug(`[Middleware] METADATA REQUEST: ${packageName} - will intercept response`);
235
245
  const blockResult = yield this._checkPackageBlock(packageName);
236
246
  if (blockResult.blocked) {
237
247
  this.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
@@ -244,7 +254,7 @@ class SecurityFilterPlugin {
244
254
  reason: blockResult.reason,
245
255
  plugin: {
246
256
  name: 'verdaccio-security-filter',
247
- version: '1.0.0',
257
+ version: PLUGIN_VERSION,
248
258
  mode: this.config.mode || 'blacklist',
249
259
  },
250
260
  blockedAt: new Date().toISOString(),
@@ -263,7 +273,7 @@ class SecurityFilterPlugin {
263
273
  return;
264
274
  }
265
275
  else {
266
- this.logger.info(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
276
+ this.logger.debug(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
267
277
  }
268
278
  }
269
279
  next();
@@ -275,206 +285,176 @@ class SecurityFilterPlugin {
275
285
  }));
276
286
  this.logger.info('[Middleware] Security filter middleware registered successfully');
277
287
  }
288
+ _blockedPackage(pkg, reason, extra = {}) {
289
+ return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: Object.assign(Object.assign({ blocked: true, reason }, extra), { plugin: {
290
+ name: 'verdaccio-security-filter',
291
+ version: PLUGIN_VERSION,
292
+ mode: this.config.mode || 'blacklist',
293
+ }, blockedAt: new Date().toISOString() }) });
294
+ }
295
+ _removeVersions(pkg, removed) {
296
+ if (removed.length === 0) {
297
+ return pkg;
298
+ }
299
+ const gone = new Set(removed);
300
+ const versions = Object.assign({}, pkg.versions);
301
+ for (const version of gone) {
302
+ delete versions[version];
303
+ }
304
+ const distTags = Object.assign({}, pkg['dist-tags']);
305
+ for (const [tag, tagVersion] of Object.entries(distTags)) {
306
+ if (gone.has(tagVersion)) {
307
+ delete distTags[tag];
308
+ this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to filtered version ${tagVersion}`);
309
+ }
310
+ }
311
+ return Object.assign(Object.assign({}, pkg), { versions, 'dist-tags': distTags });
312
+ }
278
313
  filter_metadata(packageInfo) {
279
314
  return __awaiter(this, void 0, void 0, function* () {
280
- var _a, _b, _c, _d, _e, _f, _g, _h;
315
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
281
316
  let pkg = packageInfo;
282
317
  const packageName = pkg.name;
283
- this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
318
+ this.logger.debug(`[filter_metadata] --> Processing: ${packageName}`);
284
319
  try {
285
320
  const blockResult = yield this._checkPackageBlock(packageName);
286
321
  if (blockResult.blocked) {
287
322
  this.logger.warn(`[filter_metadata] BLOCKED: ${packageName}@* - ${blockResult.reason}`);
288
323
  this.metrics.recordBlock(packageName, '*', blockResult.reason);
289
- return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
290
- blocked: true,
291
- reason: blockResult.reason,
292
- plugin: {
293
- name: 'verdaccio-security-filter',
294
- version: '2.0.0',
295
- mode: this.config.mode || 'blacklist',
296
- },
297
- blockedAt: new Date().toISOString(),
298
- rules: this._getAppliedRules(packageName),
299
- } });
324
+ return this._blockedPackage(pkg, blockResult.reason, { rules: this._getAppliedRules(packageName) });
325
+ }
326
+ if (this.securityRules.blockedVersions.length > 0 || this.securityRules.versionRangeRules.length > 0) {
327
+ const ruleBlocked = Object.keys(pkg.versions || {})
328
+ .filter(version => this._versionBlockReason(packageName, version) !== null);
329
+ if (ruleBlocked.length > 0) {
330
+ this.logger.warn(`[filter_metadata] RULE FILTERED: ${packageName}: ${ruleBlocked.join(', ')}`);
331
+ pkg = this._removeVersions(pkg, ruleBlocked);
332
+ }
300
333
  }
301
334
  if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
302
335
  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
- });
336
+ let vulnerableVersions = [];
337
+ try {
338
+ const results = yield this.cveChecker.checkVersions(packageName, versions);
339
+ vulnerableVersions = results.filter(result => result.isVulnerable).map(result => result.version);
340
+ }
341
+ catch (error) {
342
+ this.logger.error(`[filter_metadata] CVE check failed for ${packageName}: ${error.message}`);
343
+ if (((_b = this.config.errorHandling) === null || _b === void 0 ? void 0 : _b.onCveCheckError) === 'fail-closed') {
344
+ const reason = `CVE check failed (fail-closed mode): ${error.message}`;
345
+ this.metrics.recordBlock(packageName, '*', reason);
346
+ return this._blockedPackage(pkg, reason);
347
+ }
348
+ }
349
+ if (vulnerableVersions.length > 0 && !this.config.cveCheck.autoBlock) {
350
+ this.logger.warn(`[filter_metadata] CVE found in ${packageName}: ${vulnerableVersions.length} vulnerable version(s), autoBlock is off`);
318
351
  }
319
352
  if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
320
353
  const reason = `Package has ${vulnerableVersions.length} vulnerable version(s)`;
321
354
  if (vulnerableVersions.length === versions.length) {
322
355
  this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
323
356
  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
- } });
357
+ return this._blockedPackage(pkg, reason, { vulnerableVersions });
334
358
  }
335
359
  this.logger.warn(`[filter_metadata] CVE FILTERED: ${packageName} - ${reason}: ${vulnerableVersions.join(', ')}`);
336
- const filteredVersions = Object.assign({}, pkg.versions);
337
360
  for (const version of vulnerableVersions) {
338
- delete filteredVersions[version];
339
361
  this.metrics.recordBlock(packageName, version, reason);
340
362
  }
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 });
363
+ pkg = this._removeVersions(pkg, vulnerableVersions);
349
364
  }
350
365
  }
351
366
  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])) {
367
+ const latestVersion = (_c = pkg['dist-tags']) === null || _c === void 0 ? void 0 : _c.latest;
368
+ if (latestVersion && ((_d = pkg.versions) === null || _d === void 0 ? void 0 : _d[latestVersion])) {
354
369
  const versionData = pkg.versions[latestVersion];
355
370
  const licenseResult = this.licenseChecker.checkLicense(versionData);
356
371
  if (!licenseResult.allowed) {
357
- this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${licenseResult.reason}`);
358
- this.metrics.recordBlock(packageName, '*', licenseResult.reason || 'License not allowed');
359
- return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
360
- blocked: true,
361
- reason: licenseResult.reason || 'License not allowed',
362
- license: licenseResult.license,
363
- plugin: {
364
- name: 'verdaccio-security-filter',
365
- version: '2.0.0',
366
- },
367
- blockedAt: new Date().toISOString(),
368
- } });
372
+ const reason = licenseResult.reason || 'License not allowed';
373
+ this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${reason}`);
374
+ this.metrics.recordBlock(packageName, '*', reason);
375
+ return this._blockedPackage(pkg, reason, { license: licenseResult.license });
369
376
  }
370
377
  }
371
378
  }
372
- if ((_d = this.config.packageAge) === null || _d === void 0 ? void 0 : _d.enabled) {
379
+ if ((_e = this.config.packageAge) === null || _e === void 0 ? void 0 : _e.enabled) {
373
380
  const ageResult = this.packageAgeChecker.checkPackageAge(pkg);
374
381
  if (!ageResult.allowed && !ageResult.warnOnly) {
375
- this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${ageResult.reason}`);
376
- this.metrics.recordBlock(packageName, '*', ageResult.reason || 'Package too new');
377
- return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
378
- blocked: true,
379
- reason: ageResult.reason || 'Package too new',
380
- ageDays: ageResult.ageDays,
381
- plugin: {
382
- name: 'verdaccio-security-filter',
383
- version: '2.0.0',
384
- },
385
- blockedAt: new Date().toISOString(),
386
- } });
382
+ const reason = ageResult.reason || 'Package too new';
383
+ this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${reason}`);
384
+ this.metrics.recordBlock(packageName, '*', reason);
385
+ return this._blockedPackage(pkg, reason, { ageDays: ageResult.ageDays });
387
386
  }
388
387
  else if (!ageResult.allowed && ageResult.warnOnly) {
389
388
  this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
390
389
  }
391
390
  if (this.config.packageAge.minVersionAgeDays) {
392
- const filteredVersions = Object.assign({}, pkg.versions);
393
391
  const removedVersions = [];
394
- for (const version of Object.keys(filteredVersions)) {
392
+ for (const version of Object.keys(pkg.versions || {})) {
395
393
  const versionAgeResult = this.packageAgeChecker.checkVersionAge(pkg, version);
396
394
  if (!versionAgeResult.allowed && !versionAgeResult.warnOnly) {
397
395
  this.logger.warn(`[filter_metadata] VERSION AGE FILTERED: ${packageName}@${version} - ${versionAgeResult.reason}`);
398
396
  this.metrics.recordBlock(packageName, version, versionAgeResult.reason || 'Version too new');
399
- delete filteredVersions[version];
400
397
  removedVersions.push(version);
401
398
  }
402
399
  else if (!versionAgeResult.allowed && versionAgeResult.warnOnly) {
403
400
  this.logger.warn(`[filter_metadata] VERSION AGE WARNING: ${packageName}@${version} - ${versionAgeResult.reason} (warn-only mode)`);
404
401
  }
405
402
  }
406
- if (removedVersions.length > 0) {
407
- this.logger.info(`[filter_metadata] Removed ${removedVersions.length} too-new version(s) from ${packageName}: ${removedVersions.join(', ')}`);
408
- const filteredDistTags = Object.assign({}, pkg['dist-tags']);
409
- for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
410
- if (removedVersions.includes(tagVersion)) {
411
- delete filteredDistTags[tag];
412
- this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to filtered version ${tagVersion}`);
413
- }
414
- }
415
- pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
416
- }
403
+ pkg = this._removeVersions(pkg, removedVersions);
417
404
  }
418
405
  }
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])) {
406
+ if ((_f = this.config.authorFilter) === null || _f === void 0 ? void 0 : _f.enabled) {
407
+ const latestVersion = (_g = pkg['dist-tags']) === null || _g === void 0 ? void 0 : _g.latest;
408
+ if (latestVersion && ((_h = pkg.versions) === null || _h === void 0 ? void 0 : _h[latestVersion])) {
422
409
  const versionData = pkg.versions[latestVersion];
423
410
  const authorResult = this.authorChecker.checkAuthor(versionData);
424
411
  if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
425
- this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${authorResult.reason}`);
412
+ const reason = authorResult.reason || 'Author not allowed';
413
+ this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${reason}`);
426
414
  this.metrics.record({
427
415
  timestamp: new Date().toISOString(),
428
416
  event: 'author_blocked',
429
417
  packageName,
430
418
  version: latestVersion,
431
- reason: authorResult.reason || 'Author not allowed',
419
+ reason,
432
420
  metadata: {
433
421
  blockedBy: authorResult.blockedBy,
434
422
  authorInfo: authorResult.authorInfo,
435
423
  },
436
424
  });
437
- return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: {
438
- blocked: true,
439
- reason: authorResult.reason || 'Author not allowed',
440
- blockedBy: authorResult.blockedBy,
441
- authorInfo: authorResult.authorInfo,
442
- plugin: {
443
- name: 'verdaccio-security-filter',
444
- version: '2.0.0',
445
- },
446
- blockedAt: new Date().toISOString(),
447
- } });
425
+ return this._blockedPackage(pkg, reason, {
426
+ blockedBy: authorResult.blockedBy,
427
+ authorInfo: authorResult.authorInfo,
428
+ });
448
429
  }
449
430
  else if (!authorResult.allowed && this.config.authorFilter.warnOnly) {
450
431
  this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName} - ${authorResult.reason} (warn-only mode)`);
451
432
  }
452
433
  }
453
434
  }
454
- this.logger.info(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
435
+ this.logger.debug(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
455
436
  return pkg;
456
437
  }
457
438
  catch (error) {
458
439
  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';
440
+ const errorStrategy = ((_j = this.config.errorHandling) === null || _j === void 0 ? void 0 : _j.onFilterError) || 'fail-open';
460
441
  if (errorStrategy === 'fail-closed') {
461
442
  this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
462
443
  this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
463
- return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
464
- blocked: true,
465
- reason: `Error during security processing (fail-closed mode): ${error.message}`,
466
- plugin: {
467
- name: 'verdaccio-security-filter',
468
- version: '2.0.0',
469
- },
470
- blockedAt: new Date().toISOString(),
471
- } });
444
+ return this._blockedPackage(packageInfo, `Error during security processing (fail-closed mode): ${error.message}`);
472
445
  }
473
446
  this.logger.warn(`[filter_metadata] Error handling: fail-open - ALLOWING ${packageName}`);
474
447
  return packageInfo;
475
448
  }
476
449
  });
477
450
  }
451
+ _versionBlockReason(packageName, version) {
452
+ if (this.securityRules.blockedVersions.includes(`${packageName}@${version}`)) {
453
+ return 'Exact version match in blocklist';
454
+ }
455
+ const rangeRule = this._getVersionRangeRule(packageName, version);
456
+ return rangeRule ? `Version matches blocked range: ${rangeRule.range}` : null;
457
+ }
478
458
  _checkPackageBlock(packageName, version) {
479
459
  return __awaiter(this, void 0, void 0, function* () {
480
460
  if (this.config.mode === 'whitelist') {
@@ -489,15 +469,9 @@ class SecurityFilterPlugin {
489
469
  if (!this._isScopeAllowed(packageName)) {
490
470
  return { blocked: true, reason: 'Package scope not allowed' };
491
471
  }
492
- if (version) {
493
- const versionKey = `${packageName}@${version}`;
494
- if (this.securityRules.blockedVersions.includes(versionKey)) {
495
- return { blocked: true, reason: 'Exact version match in blocklist' };
496
- }
497
- const rangeRule = this._getVersionRangeRule(packageName, version);
498
- if (rangeRule && rangeRule.strategy === 'block') {
499
- return { blocked: true, reason: `Version matches blocked range: ${rangeRule.range}` };
500
- }
472
+ const versionReason = version ? this._versionBlockReason(packageName, version) : null;
473
+ if (versionReason) {
474
+ return { blocked: true, reason: versionReason };
501
475
  }
502
476
  return { blocked: false, reason: '' };
503
477
  });
@@ -508,11 +482,9 @@ class SecurityFilterPlugin {
508
482
  rules.mode = 'whitelist';
509
483
  rules.whitelisted = false;
510
484
  }
511
- if (this._isBlockedByPattern(packageName)) {
512
- rules.blockedPattern = this.securityRules.blockedPatterns.find(pattern => {
513
- const regex = new RegExp(pattern);
514
- return regex.test(packageName);
515
- });
485
+ const blockedPattern = this.blockedPatternRegexes.find(regex => regex.test(packageName));
486
+ if (blockedPattern) {
487
+ rules.blockedPattern = blockedPattern.source;
516
488
  }
517
489
  if (packageName.startsWith('@')) {
518
490
  const scope = packageName.split('/')[0];
@@ -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
  }
@@ -4,6 +4,8 @@ export declare class MetricsCollector {
4
4
  private metricsBuffer;
5
5
  private readonly bufferSize;
6
6
  private readonly maxBufferSize;
7
+ private readonly flushIntervalMs;
8
+ private flushTimer;
7
9
  constructor(config?: MetricsConfig);
8
10
  record(event: MetricsData): void;
9
11
  recordBlock(packageName: string, version: string, reason: string, metadata?: Record<string, any>): void;
@@ -41,11 +41,17 @@ class MetricsCollector {
41
41
  this.metricsBuffer = [];
42
42
  this.bufferSize = 100;
43
43
  this.maxBufferSize = 500;
44
+ this.flushIntervalMs = 10000;
45
+ this.flushTimer = null;
44
46
  this.config = {
45
47
  enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
46
48
  output: (config === null || config === void 0 ? void 0 : config.output) || 'stdout',
47
49
  filePath: (config === null || config === void 0 ? void 0 : config.filePath) || './security-metrics.json',
48
50
  };
51
+ if (this.config.enabled) {
52
+ this.flushTimer = setInterval(() => this.flush(), this.flushIntervalMs);
53
+ this.flushTimer.unref();
54
+ }
49
55
  }
50
56
  record(event) {
51
57
  if (!this.config.enabled) {
@@ -163,6 +169,10 @@ class MetricsCollector {
163
169
  return summary;
164
170
  }
165
171
  cleanup() {
172
+ if (this.flushTimer) {
173
+ clearInterval(this.flushTimer);
174
+ this.flushTimer = null;
175
+ }
166
176
  this.flush();
167
177
  }
168
178
  }
@@ -1,12 +1,14 @@
1
1
  import { WhitelistConfig } from '../types';
2
2
  export declare class WhitelistChecker {
3
3
  private config;
4
+ private regexes;
4
5
  constructor(config?: WhitelistConfig);
5
6
  isWhitelisted(packageName: string, version?: string): {
6
7
  allowed: boolean;
7
8
  reason?: string;
8
9
  };
9
10
  private matchesPattern;
11
+ private compile;
10
12
  addPackage(packageName: string, versionRange?: string): void;
11
13
  removePackage(packageName: string): void;
12
14
  addPattern(pattern: string): void;
@@ -46,6 +46,7 @@ exports.WhitelistChecker = void 0;
46
46
  const semver = __importStar(require("semver"));
47
47
  class WhitelistChecker {
48
48
  constructor(config) {
49
+ this.regexes = new Map();
49
50
  this.config = {
50
51
  packages: (config === null || config === void 0 ? void 0 : config.packages) || [],
51
52
  patterns: (config === null || config === void 0 ? void 0 : config.patterns) || [],
@@ -75,16 +76,21 @@ class WhitelistChecker {
75
76
  };
76
77
  }
77
78
  matchesPattern(packageName) {
78
- return this.config.patterns.some(pattern => {
79
+ return this.config.patterns.some(pattern => { var _a; return (_a = this.compile(pattern)) === null || _a === void 0 ? void 0 : _a.test(packageName); });
80
+ }
81
+ compile(pattern) {
82
+ let regex = this.regexes.get(pattern);
83
+ if (regex === undefined) {
79
84
  try {
80
- const regex = new RegExp(pattern);
81
- return regex.test(packageName);
85
+ regex = new RegExp(pattern);
82
86
  }
83
87
  catch (_a) {
84
88
  console.error(`[Whitelist] Invalid pattern: ${pattern}`);
85
- return false;
89
+ regex = null;
86
90
  }
87
- });
91
+ this.regexes.set(pattern, regex);
92
+ }
93
+ return regex;
88
94
  }
89
95
  addPackage(packageName, versionRange) {
90
96
  if (!this.config.packages.includes(packageName)) {
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.10",
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",