verdaccio-security-filter 1.2.0 → 1.3.1

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
@@ -2,6 +2,7 @@
2
2
 
3
3
  Advanced security filter plugin for Verdaccio with **dual-layer protection architecture** combining middleware interception and metadata filtering for comprehensive package security.
4
4
 
5
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
5
6
  [![npm version](https://img.shields.io/npm/v/verdaccio-security-filter.svg)](https://www.npmjs.com/package/verdaccio-security-filter)
6
7
  [![npm downloads](https://img.shields.io/npm/dm/verdaccio-security-filter.svg)](https://www.npmjs.com/package/verdaccio-security-filter)
7
8
  [![CI](https://github.com/ponomarenko/verdaccio-security-filter/actions/workflows/ci.yml/badge.svg)](https://github.com/ponomarenko/verdaccio-security-filter/actions/workflows/ci.yml)
@@ -71,6 +72,13 @@ This plugin implements **two independent security layers** for complete protecti
71
72
  - **Minimum Package Age** - Require packages to exist for a minimum number of days
72
73
  - **Minimum Version Age** - Require specific versions to exist for a minimum number of days
73
74
  - **Warn-Only Mode** - Log warnings without blocking
75
+ - **Author Filtering** - Block packages based on author information
76
+ - **Block by Author Name** - Block specific author names or patterns
77
+ - **Block by Email** - Block specific email addresses or patterns
78
+ - **Block by Email Domain** - Block entire email domains (e.g., `.ru`, `yandex.ru`)
79
+ - **Block by Region** - Block authors from specific regions based on email domains
80
+ - **Maintainer & Contributor Checking** - Validates all package maintainers and contributors
81
+ - **Warn-Only Mode** - Log warnings without blocking
74
82
  - **Middleware Interception** - HTTP middleware that blocks tarball downloads for blocked packages
75
83
  - Prevents blocked packages from being installed as dependencies
76
84
  - Intercepts requests to `/:package/-/:filename.tgz` routes
@@ -467,6 +475,20 @@ middlewares:
467
475
  | `packageAge.minVersionAgeDays` | `number` | `undefined` | Minimum age for versions (days) |
468
476
  | `packageAge.warnOnly` | `boolean` | `false` | Only warn, don't block |
469
477
 
478
+ ### Author Filter Options
479
+
480
+ | Option | Type | Default | Description |
481
+ |--------|------|---------|-------------|
482
+ | `authorFilter.enabled` | `boolean` | `false` | Enable author filtering |
483
+ | `authorFilter.blockedAuthors` | `string[]` | `[]` | List of author names to block |
484
+ | `authorFilter.blockedAuthorPatterns` | `string[]` | `[]` | Regex patterns for author names |
485
+ | `authorFilter.blockedEmails` | `string[]` | `[]` | List of email addresses to block |
486
+ | `authorFilter.blockedEmailPatterns` | `string[]` | `[]` | Regex patterns for author emails |
487
+ | `authorFilter.blockedEmailDomains` | `string[]` | `[]` | Email domains to block (e.g., `.ru`, `yandex.ru`) |
488
+ | `authorFilter.blockedRegions` | `string[]` | `[]` | Region codes to block (`ru`, `cn`, `by`, `kp`, `ir`, `sy`, `cu`, `sd`) |
489
+ | `authorFilter.requireVerifiedEmail` | `boolean` | `false` | Require author email information |
490
+ | `authorFilter.warnOnly` | `boolean` | `false` | Only warn, don't block |
491
+
470
492
  ### Error Handling Options
471
493
 
472
494
  | Option | Type | Default | Description |
@@ -533,13 +555,14 @@ npm test:watch
533
555
  ```
534
556
 
535
557
  **Test Results:**
536
- - ✅ 80 tests passing
558
+ - ✅ 108 tests passing
537
559
  - ✅ 60%+ code coverage
538
560
  - ✅ SecurityFilterPlugin: 21 tests
539
561
  - ✅ PackageAgeChecker: 13 tests
540
562
  - ✅ SecurityLogger: 16 tests
541
563
  - ✅ LicenseChecker: 15 tests
542
564
  - ✅ WhitelistChecker: 15 tests
565
+ - ✅ AuthorChecker: 28 tests
543
566
 
544
567
  ## 🔧 Development
545
568
 
@@ -567,6 +590,7 @@ When metrics are enabled, the plugin tracks:
567
590
  - `cve_detected` - CVE vulnerabilities found
568
591
  - `license_blocked` - Packages blocked by license rules
569
592
  - `package_too_new` - Packages/versions blocked due to age restrictions
593
+ - `author_blocked` - Packages blocked due to author/region filtering
570
594
 
571
595
  Example metrics output:
572
596
  ```json
@@ -656,16 +680,54 @@ packageAge:
656
680
  warnOnly: false # Block, don't just warn
657
681
  ```
658
682
 
683
+ ### 7. Block Packages by Author/Region
684
+
685
+ ```yaml
686
+ # Block packages from specific authors, emails, or regions
687
+ authorFilter:
688
+ enabled: true
689
+ # Block specific author names
690
+ blockedAuthors:
691
+ - "Suspicious Author"
692
+ - "Known Bad Actor"
693
+ # Block author names matching patterns
694
+ blockedAuthorPatterns:
695
+ - "^Bot.*"
696
+ - ".*Spammer$"
697
+ # Block specific email addresses
698
+ blockedEmails:
699
+ - "malicious@example.com"
700
+ # Block email patterns
701
+ blockedEmailPatterns:
702
+ - ".*@temporary-mail\\.com$"
703
+ # Block entire email domains
704
+ blockedEmailDomains:
705
+ - ".ru" # All .ru domains
706
+ - "yandex.ru" # Specific domain
707
+ - "mail.ru"
708
+ # Block by region (includes multiple common domains)
709
+ blockedRegions:
710
+ - "ru" # Russia (.ru, yandex.ru, mail.ru, etc.)
711
+ - "cn" # China (.cn, qq.com, 163.com, etc.)
712
+ - "by" # Belarus
713
+ - "kp" # North Korea
714
+ # Require author information
715
+ requireVerifiedEmail: true
716
+ # Warn-only mode (don't block, just log)
717
+ warnOnly: false
718
+ ```
719
+
659
720
  ## 🔐 Security Best Practices
660
721
 
661
722
  1. **Enable CVE Scanning** - Automatically detect and block vulnerable packages
662
723
  2. **Use Whitelist Mode** - For maximum security in sensitive environments
663
724
  3. **Enforce License Compliance** - Prevent legal issues with license filtering
664
725
  4. **Enable Package Age Filtering** - Block newly published packages to prevent supply chain attacks
665
- 5. **Enable Metrics** - Track security events for audit and compliance
666
- 6. **Regular Updates** - Keep the plugin and CVE database cache updated
667
- 7. **Test Rules** - Use dry run mode (planned) before applying strict rules
668
- 8. **Monitor Logs** - Review security logs regularly for suspicious activity
726
+ 5. **Enable Author Filtering** - Block packages from untrusted authors or regions
727
+ 6. **Enable Metrics** - Track security events for audit and compliance
728
+ 7. **Regular Updates** - Keep the plugin and CVE database cache updated
729
+ 8. **Test Rules** - Use dry run mode (planned) before applying strict rules
730
+ 9. **Monitor Logs** - Review security logs regularly for suspicious activity
669
731
 
670
732
  ## 🐛 Troubleshooting
671
733
 
package/build/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
20
20
  private readonly cveChecker;
21
21
  private readonly licenseChecker;
22
22
  private readonly packageAgeChecker;
23
+ private readonly authorChecker;
23
24
  /**
24
25
  * Creates a new SecurityFilterPlugin instance
25
26
  * @param {SecurityConfig} config - Plugin configuration
@@ -76,6 +77,11 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
76
77
  * @private
77
78
  */
78
79
  private _getAppliedRules;
80
+ /**
81
+ * Cleanup plugin resources
82
+ * Should be called when Verdaccio shuts down
83
+ */
84
+ destroy(): void;
79
85
  }
80
86
  export { SecurityLogger } from './lib/logger';
81
87
  export { MetricsCollector } from './lib/metrics';
@@ -83,4 +89,5 @@ export { WhitelistChecker } from './lib/whitelist-checker';
83
89
  export { CVEChecker } from './lib/cve-checker';
84
90
  export { LicenseChecker } from './lib/license-checker';
85
91
  export { PackageAgeChecker } from './lib/package-age-checker';
92
+ export { AuthorChecker } from './lib/author-checker';
86
93
  export * from './types';
package/build/index.js CHANGED
@@ -45,7 +45,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
45
45
  });
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.PackageAgeChecker = exports.LicenseChecker = exports.CVEChecker = exports.WhitelistChecker = exports.MetricsCollector = exports.SecurityLogger = void 0;
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
50
  const logger_1 = require("./lib/logger");
51
51
  const metrics_1 = require("./lib/metrics");
@@ -53,6 +53,7 @@ const whitelist_checker_1 = require("./lib/whitelist-checker");
53
53
  const cve_checker_1 = require("./lib/cve-checker");
54
54
  const license_checker_1 = require("./lib/license-checker");
55
55
  const package_age_checker_1 = require("./lib/package-age-checker");
56
+ const author_checker_1 = require("./lib/author-checker");
56
57
  /**
57
58
  * Security Filter Plugin for Verdaccio
58
59
  * Works with both middleware and filter interfaces for complete protection
@@ -88,6 +89,7 @@ class SecurityFilterPlugin {
88
89
  this.cveChecker = new cve_checker_1.CVEChecker(this.config.cveCheck);
89
90
  this.licenseChecker = new license_checker_1.LicenseChecker(this.config.licenses);
90
91
  this.packageAgeChecker = new package_age_checker_1.PackageAgeChecker(this.config.packageAge);
92
+ this.authorChecker = new author_checker_1.AuthorChecker(this.config.authorFilter);
91
93
  // Security rules configuration
92
94
  this.securityRules = {
93
95
  blockedVersions: this.config.blockedVersions || [],
@@ -135,6 +137,7 @@ class SecurityFilterPlugin {
135
137
  * @private
136
138
  */
137
139
  _logInitialization() {
140
+ var _a;
138
141
  const features = this._getEnabledFeatures();
139
142
  this.logger.logInit(features);
140
143
  this.logger.debug(`[Init] Config mode: ${this.config.mode || 'not set'}`);
@@ -151,13 +154,17 @@ class SecurityFilterPlugin {
151
154
  const summary = this.whitelistChecker.getSummary();
152
155
  this.logger.info(`Whitelist mode: ${summary.totalPackages} packages, ${summary.totalPatterns} patterns`);
153
156
  }
157
+ if ((_a = this.config.authorFilter) === null || _a === void 0 ? void 0 : _a.enabled) {
158
+ const summary = this.authorChecker.getSummary();
159
+ this.logger.info(`Author filtering enabled: ${summary.blockedAuthors} authors, ${summary.blockedEmails} emails, ${summary.blockedRegions} regions blocked`);
160
+ }
154
161
  }
155
162
  /**
156
163
  * Get list of enabled features
157
164
  * @private
158
165
  */
159
166
  _getEnabledFeatures() {
160
- var _a, _b, _c, _d;
167
+ var _a, _b, _c, _d, _e;
161
168
  const features = [];
162
169
  if (this.config.mode === 'whitelist')
163
170
  features.push('whitelist-mode');
@@ -169,6 +176,8 @@ class SecurityFilterPlugin {
169
176
  features.push('license-filtering');
170
177
  if ((_d = this.config.packageAge) === null || _d === void 0 ? void 0 : _d.enabled)
171
178
  features.push('package-age-verification');
179
+ if ((_e = this.config.authorFilter) === null || _e === void 0 ? void 0 : _e.enabled)
180
+ features.push('author-filtering');
172
181
  if (this.securityRules.versionRangeRules.length > 0)
173
182
  features.push('version-range-rules');
174
183
  if (this.securityRules.blockedPatterns.length > 0)
@@ -281,79 +290,33 @@ class SecurityFilterPlugin {
281
290
  // Check if package should be blocked
282
291
  const blockResult = yield this._checkPackageBlock(packageName);
283
292
  if (blockResult.blocked) {
284
- // Intercept the response
285
- const originalSend = res.send;
286
- const originalJson = res.json;
287
- // eslint-disable-next-line @typescript-eslint/no-this-alias
288
- const self = this;
289
- // Override res.json to intercept JSON responses
290
- res.json = function (body) {
291
- if (body && typeof body === 'object' && body.name === packageName) {
292
- // This is package metadata - modify it to show blocked info
293
- self.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
294
- self.logger.info(`[Middleware] Modifying response to show blocking info`);
295
- const blockedResponse = {
296
- name: packageName,
297
- versions: {},
298
- 'dist-tags': {},
299
- security: {
300
- blocked: true,
301
- reason: blockResult.reason,
302
- plugin: {
303
- name: 'verdaccio-security-filter',
304
- version: '1.0.0',
305
- mode: self.config.mode || 'blacklist',
306
- },
307
- blockedAt: new Date().toISOString(),
308
- rules: self._getAppliedRules(packageName),
309
- message: `This package has been blocked by security filter: ${blockResult.reason}`,
310
- contact: 'Please contact your registry administrator for more information',
311
- },
312
- _security: {
313
- blocked: true,
314
- reason: blockResult.reason,
315
- blockedBy: 'verdaccio-security-filter',
316
- blockedAt: new Date().toISOString(),
317
- }
318
- };
319
- return originalJson.call(this, blockedResponse);
320
- }
321
- return originalJson.call(this, body);
322
- };
323
- // Override res.send for non-JSON responses
324
- res.send = function (body) {
325
- if (typeof body === 'string') {
326
- try {
327
- const parsed = JSON.parse(body);
328
- if (parsed && parsed.name === packageName) {
329
- self.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
330
- const blockedResponse = {
331
- name: packageName,
332
- versions: {},
333
- 'dist-tags': {},
334
- security: {
335
- blocked: true,
336
- reason: blockResult.reason,
337
- plugin: {
338
- name: 'verdaccio-security-filter',
339
- version: '1.0.0',
340
- mode: self.config.mode || 'blacklist',
341
- },
342
- blockedAt: new Date().toISOString(),
343
- rules: self._getAppliedRules(packageName),
344
- message: `This package has been blocked by security filter: ${blockResult.reason}`,
345
- contact: 'Please contact your registry administrator for more information',
346
- }
347
- };
348
- return originalSend.call(this, JSON.stringify(blockedResponse));
349
- }
350
- }
351
- catch (_a) {
352
- // Not JSON, pass through
353
- }
293
+ this.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
294
+ const blockedResponse = {
295
+ name: packageName,
296
+ versions: {},
297
+ 'dist-tags': {},
298
+ security: {
299
+ blocked: true,
300
+ reason: blockResult.reason,
301
+ plugin: {
302
+ name: 'verdaccio-security-filter',
303
+ version: '1.0.0',
304
+ mode: this.config.mode || 'blacklist',
305
+ },
306
+ blockedAt: new Date().toISOString(),
307
+ rules: this._getAppliedRules(packageName),
308
+ message: `This package has been blocked by security filter: ${blockResult.reason}`,
309
+ contact: 'Please contact your registry administrator for more information',
310
+ },
311
+ _security: {
312
+ blocked: true,
313
+ reason: blockResult.reason,
314
+ blockedBy: 'verdaccio-security-filter',
315
+ blockedAt: new Date().toISOString(),
354
316
  }
355
- return originalSend.call(this, body);
356
317
  };
318
+ res.status(200).json(blockedResponse);
319
+ return;
357
320
  }
358
321
  else {
359
322
  this.logger.info(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
@@ -375,7 +338,7 @@ class SecurityFilterPlugin {
375
338
  */
376
339
  filter_metadata(packageInfo) {
377
340
  return __awaiter(this, void 0, void 0, function* () {
378
- var _a, _b, _c, _d, _e;
341
+ var _a, _b, _c, _d, _e, _f, _g, _h;
379
342
  const packageName = packageInfo.name;
380
343
  this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
381
344
  try {
@@ -397,16 +360,24 @@ class SecurityFilterPlugin {
397
360
  rules: this._getAppliedRules(packageName),
398
361
  } });
399
362
  }
400
- // 2. CVE Check - check all versions
363
+ // 2. CVE Check - check versions in controlled batches
401
364
  if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
402
365
  const versions = Object.keys(packageInfo.versions || {});
403
366
  const vulnerableVersions = [];
404
- for (const version of versions) {
405
- const cveResult = yield this.cveChecker.checkPackage(packageName, version);
406
- if (cveResult.isVulnerable) {
407
- vulnerableVersions.push(version);
408
- this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${cveResult.vulnerabilities.length} vulnerabilities`);
409
- }
367
+ const BATCH_SIZE = 10;
368
+ for (let i = 0; i < versions.length; i += BATCH_SIZE) {
369
+ const batch = versions.slice(i, i + BATCH_SIZE);
370
+ const results = yield Promise.allSettled(batch.map(version => this.cveChecker.checkPackage(packageName, version)));
371
+ results.forEach((result, index) => {
372
+ if (result.status === 'fulfilled' && result.value.isVulnerable) {
373
+ const version = batch[index];
374
+ vulnerableVersions.push(version);
375
+ this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${result.value.vulnerabilities.length} vulnerabilities`);
376
+ }
377
+ else if (result.status === 'rejected') {
378
+ this.logger.error(`[filter_metadata] CVE check failed for ${packageName}@${batch[index]}: ${result.reason}`);
379
+ }
380
+ });
410
381
  }
411
382
  // If configured to auto-block and vulnerabilities found
412
383
  if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
@@ -468,12 +439,48 @@ class SecurityFilterPlugin {
468
439
  this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
469
440
  }
470
441
  }
442
+ // 5. Author Filter Check
443
+ if ((_e = this.config.authorFilter) === null || _e === void 0 ? void 0 : _e.enabled) {
444
+ const latestVersion = (_f = packageInfo['dist-tags']) === null || _f === void 0 ? void 0 : _f.latest;
445
+ if (latestVersion && ((_g = packageInfo.versions) === null || _g === void 0 ? void 0 : _g[latestVersion])) {
446
+ const versionData = packageInfo.versions[latestVersion];
447
+ const authorResult = this.authorChecker.checkAuthor(versionData);
448
+ if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
449
+ this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${authorResult.reason}`);
450
+ this.metrics.record({
451
+ timestamp: new Date().toISOString(),
452
+ event: 'author_blocked',
453
+ packageName,
454
+ version: latestVersion,
455
+ reason: authorResult.reason || 'Author not allowed',
456
+ metadata: {
457
+ blockedBy: authorResult.blockedBy,
458
+ authorInfo: authorResult.authorInfo,
459
+ },
460
+ });
461
+ return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
462
+ blocked: true,
463
+ reason: authorResult.reason || 'Author not allowed',
464
+ blockedBy: authorResult.blockedBy,
465
+ authorInfo: authorResult.authorInfo,
466
+ plugin: {
467
+ name: 'verdaccio-security-filter',
468
+ version: '2.0.0',
469
+ },
470
+ blockedAt: new Date().toISOString(),
471
+ } });
472
+ }
473
+ else if (!authorResult.allowed && this.config.authorFilter.warnOnly) {
474
+ this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName} - ${authorResult.reason} (warn-only mode)`);
475
+ }
476
+ }
477
+ }
471
478
  this.logger.info(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
472
479
  return packageInfo;
473
480
  }
474
481
  catch (error) {
475
482
  this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
476
- const errorStrategy = ((_e = this.config.errorHandling) === null || _e === void 0 ? void 0 : _e.onFilterError) || 'fail-open';
483
+ const errorStrategy = ((_h = this.config.errorHandling) === null || _h === void 0 ? void 0 : _h.onFilterError) || 'fail-open';
477
484
  if (errorStrategy === 'fail-closed') {
478
485
  this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
479
486
  this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
@@ -565,6 +572,21 @@ class SecurityFilterPlugin {
565
572
  }
566
573
  return rules;
567
574
  }
575
+ /**
576
+ * Cleanup plugin resources
577
+ * Should be called when Verdaccio shuts down
578
+ */
579
+ destroy() {
580
+ this.logger.info('[Cleanup] Shutting down security filter plugin');
581
+ try {
582
+ this.cveChecker.destroy();
583
+ this.metrics.cleanup();
584
+ this.logger.info('[Cleanup] Plugin resources cleaned up successfully');
585
+ }
586
+ catch (error) {
587
+ this.logger.error(`[Cleanup] Error during cleanup: ${error.message}`);
588
+ }
589
+ }
568
590
  }
569
591
  exports.default = SecurityFilterPlugin;
570
592
  // Export utility classes for testing
@@ -580,4 +602,6 @@ var license_checker_2 = require("./lib/license-checker");
580
602
  Object.defineProperty(exports, "LicenseChecker", { enumerable: true, get: function () { return license_checker_2.LicenseChecker; } });
581
603
  var package_age_checker_2 = require("./lib/package-age-checker");
582
604
  Object.defineProperty(exports, "PackageAgeChecker", { enumerable: true, get: function () { return package_age_checker_2.PackageAgeChecker; } });
605
+ var author_checker_2 = require("./lib/author-checker");
606
+ Object.defineProperty(exports, "AuthorChecker", { enumerable: true, get: function () { return author_checker_2.AuthorChecker; } });
583
607
  __exportStar(require("./types"), exports);
@@ -0,0 +1,44 @@
1
+ import { Version } from '@verdaccio/types';
2
+ import { AuthorFilterConfig, AuthorCheckResult } from '../types';
3
+ /**
4
+ * AuthorChecker validates package authors and maintainers
5
+ * against configured security policies.
6
+ */
7
+ export declare class AuthorChecker {
8
+ private readonly config?;
9
+ private readonly blockedAuthors;
10
+ private readonly blockedAuthorPatterns;
11
+ private readonly blockedEmails;
12
+ private readonly blockedEmailPatterns;
13
+ private readonly blockedEmailDomains;
14
+ private readonly regionDomainMap;
15
+ constructor(config?: AuthorFilterConfig);
16
+ /**
17
+ * Check if package author/maintainer is allowed
18
+ */
19
+ checkAuthor(versionData: Version): AuthorCheckResult;
20
+ /**
21
+ * Extract author information from package version data
22
+ */
23
+ private _extractAuthors;
24
+ /**
25
+ * Normalize author data to AuthorInfo format
26
+ */
27
+ private _normalizeAuthor;
28
+ /**
29
+ * Check a single author against all blocking rules
30
+ */
31
+ private _checkSingleAuthor;
32
+ /**
33
+ * Get summary of configuration
34
+ */
35
+ getSummary(): {
36
+ enabled: boolean;
37
+ blockedAuthors: number;
38
+ blockedAuthorPatterns: number;
39
+ blockedEmails: number;
40
+ blockedEmailPatterns: number;
41
+ blockedEmailDomains: number;
42
+ blockedRegions: number;
43
+ };
44
+ }
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthorChecker = void 0;
4
+ /**
5
+ * Region to TLD mapping for email domain filtering
6
+ */
7
+ const REGION_DOMAINS = {
8
+ 'ru': ['.ru', 'yandex.ru', 'mail.ru', 'rambler.ru', 'ya.ru', 'bk.ru', 'list.ru'],
9
+ 'cn': ['.cn', 'qq.com', '163.com', '126.com', 'sina.com', 'sohu.com'],
10
+ 'by': ['.by', 'tut.by', 'mail.by'],
11
+ 'kp': ['.kp'], // North Korea
12
+ 'ir': ['.ir'], // Iran
13
+ 'sy': ['.sy'], // Syria
14
+ 'cu': ['.cu'], // Cuba
15
+ 'sd': ['.sd'], // Sudan
16
+ };
17
+ /**
18
+ * AuthorChecker validates package authors and maintainers
19
+ * against configured security policies.
20
+ */
21
+ class AuthorChecker {
22
+ constructor(config) {
23
+ this.config = config;
24
+ this.blockedAuthors = new Set(((config === null || config === void 0 ? void 0 : config.blockedAuthors) || []).map(a => a.toLowerCase().trim()));
25
+ this.blockedAuthorPatterns = ((config === null || config === void 0 ? void 0 : config.blockedAuthorPatterns) || [])
26
+ .map(pattern => new RegExp(pattern, 'i'));
27
+ this.blockedEmails = new Set(((config === null || config === void 0 ? void 0 : config.blockedEmails) || []).map(e => e.toLowerCase().trim()));
28
+ this.blockedEmailPatterns = ((config === null || config === void 0 ? void 0 : config.blockedEmailPatterns) || [])
29
+ .map(pattern => new RegExp(pattern, 'i'));
30
+ this.blockedEmailDomains = new Set(((config === null || config === void 0 ? void 0 : config.blockedEmailDomains) || []).map(d => d.toLowerCase().trim()));
31
+ this.regionDomainMap = new Map();
32
+ ((config === null || config === void 0 ? void 0 : config.blockedRegions) || []).forEach(region => {
33
+ const domains = REGION_DOMAINS[region.toLowerCase()] || [];
34
+ this.regionDomainMap.set(region.toLowerCase(), new Set(domains));
35
+ });
36
+ }
37
+ /**
38
+ * Check if package author/maintainer is allowed
39
+ */
40
+ checkAuthor(versionData) {
41
+ var _a;
42
+ if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.enabled)) {
43
+ return { allowed: true };
44
+ }
45
+ const authors = this._extractAuthors(versionData);
46
+ if (authors.length === 0) {
47
+ if (this.config.requireVerifiedEmail) {
48
+ return {
49
+ allowed: false,
50
+ reason: 'No author information available',
51
+ blockedBy: 'email',
52
+ };
53
+ }
54
+ return { allowed: true };
55
+ }
56
+ for (const author of authors) {
57
+ const result = this._checkSingleAuthor(author);
58
+ if (!result.allowed) {
59
+ return result;
60
+ }
61
+ }
62
+ return { allowed: true };
63
+ }
64
+ /**
65
+ * Extract author information from package version data
66
+ */
67
+ _extractAuthors(versionData) {
68
+ const authors = [];
69
+ if (versionData.author) {
70
+ authors.push(this._normalizeAuthor(versionData.author));
71
+ }
72
+ if (Array.isArray(versionData.maintainers)) {
73
+ versionData.maintainers.forEach(maintainer => {
74
+ authors.push(this._normalizeAuthor(maintainer));
75
+ });
76
+ }
77
+ if (Array.isArray(versionData.contributors)) {
78
+ versionData.contributors.forEach(contributor => {
79
+ authors.push(this._normalizeAuthor(contributor));
80
+ });
81
+ }
82
+ return authors;
83
+ }
84
+ /**
85
+ * Normalize author data to AuthorInfo format
86
+ */
87
+ _normalizeAuthor(author) {
88
+ if (typeof author === 'string') {
89
+ const emailMatch = author.match(/<([^>]+)>/);
90
+ const nameMatch = author.match(/^([^<]+)/);
91
+ return {
92
+ name: nameMatch ? nameMatch[1].trim() : undefined,
93
+ email: emailMatch ? emailMatch[1].trim() : undefined,
94
+ };
95
+ }
96
+ return {
97
+ name: author.name,
98
+ email: author.email,
99
+ url: author.url,
100
+ };
101
+ }
102
+ /**
103
+ * Check a single author against all blocking rules
104
+ */
105
+ _checkSingleAuthor(author) {
106
+ // Check blocked author names
107
+ if (author.name) {
108
+ const normalizedName = author.name.toLowerCase().trim();
109
+ if (this.blockedAuthors.has(normalizedName)) {
110
+ return {
111
+ allowed: false,
112
+ reason: `Author name "${author.name}" is blocked`,
113
+ blockedBy: 'name',
114
+ authorInfo: author,
115
+ };
116
+ }
117
+ for (const pattern of this.blockedAuthorPatterns) {
118
+ if (pattern.test(author.name)) {
119
+ return {
120
+ allowed: false,
121
+ reason: `Author name "${author.name}" matches blocked pattern`,
122
+ blockedBy: 'name',
123
+ authorInfo: author,
124
+ };
125
+ }
126
+ }
127
+ }
128
+ // Check blocked emails
129
+ if (author.email) {
130
+ const normalizedEmail = author.email.toLowerCase().trim();
131
+ if (this.blockedEmails.has(normalizedEmail)) {
132
+ return {
133
+ allowed: false,
134
+ reason: `Author email "${author.email}" is blocked`,
135
+ blockedBy: 'email',
136
+ authorInfo: author,
137
+ };
138
+ }
139
+ for (const pattern of this.blockedEmailPatterns) {
140
+ if (pattern.test(author.email)) {
141
+ return {
142
+ allowed: false,
143
+ reason: `Author email "${author.email}" matches blocked pattern`,
144
+ blockedBy: 'email',
145
+ authorInfo: author,
146
+ };
147
+ }
148
+ }
149
+ // Check blocked email domains
150
+ for (const domain of this.blockedEmailDomains) {
151
+ if (normalizedEmail.endsWith(domain) || normalizedEmail.includes(`@${domain}`)) {
152
+ return {
153
+ allowed: false,
154
+ reason: `Author email domain "${domain}" is blocked`,
155
+ blockedBy: 'domain',
156
+ authorInfo: author,
157
+ };
158
+ }
159
+ }
160
+ // Check blocked regions
161
+ for (const [region, domains] of this.regionDomainMap.entries()) {
162
+ for (const domain of domains) {
163
+ if (normalizedEmail.endsWith(domain) ||
164
+ normalizedEmail.endsWith(`@${domain}`) ||
165
+ normalizedEmail.includes(`@${domain}`)) {
166
+ return {
167
+ allowed: false,
168
+ reason: `Author email from blocked region "${region.toUpperCase()}"`,
169
+ blockedBy: 'region',
170
+ authorInfo: author,
171
+ };
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return { allowed: true };
177
+ }
178
+ /**
179
+ * Get summary of configuration
180
+ */
181
+ getSummary() {
182
+ var _a;
183
+ return {
184
+ enabled: ((_a = this.config) === null || _a === void 0 ? void 0 : _a.enabled) || false,
185
+ blockedAuthors: this.blockedAuthors.size,
186
+ blockedAuthorPatterns: this.blockedAuthorPatterns.length,
187
+ blockedEmails: this.blockedEmails.size,
188
+ blockedEmailPatterns: this.blockedEmailPatterns.length,
189
+ blockedEmailDomains: this.blockedEmailDomains.size,
190
+ blockedRegions: this.regionDomainMap.size,
191
+ };
192
+ }
193
+ }
194
+ exports.AuthorChecker = AuthorChecker;
@@ -6,6 +6,9 @@ export declare class CVEChecker {
6
6
  private config;
7
7
  private cache;
8
8
  private lastUpdate;
9
+ private readonly MAX_CACHE_SIZE;
10
+ private saveTimer;
11
+ private readonly SAVE_DEBOUNCE_MS;
9
12
  constructor(config?: CVECheckConfig);
10
13
  /**
11
14
  * Check if a package version has known vulnerabilities
@@ -37,11 +40,19 @@ export declare class CVEChecker {
37
40
  */
38
41
  private loadCache;
39
42
  /**
40
- * Save cache to disk
43
+ * Debounced save cache to reduce I/O operations
44
+ */
45
+ private debouncedSaveCache;
46
+ /**
47
+ * Save cache to disk immediately
41
48
  */
42
49
  private saveCache;
43
50
  /**
44
51
  * Clear cache
45
52
  */
46
53
  clearCache(): void;
54
+ /**
55
+ * Cleanup resources
56
+ */
57
+ destroy(): void;
47
58
  }
@@ -45,13 +45,16 @@ 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 lru_cache_1 = require("./lru-cache");
48
49
  /**
49
50
  * CVE Checker that integrates with vulnerability databases
50
51
  */
51
52
  class CVEChecker {
52
53
  constructor(config) {
53
- this.cache = new Map();
54
54
  this.lastUpdate = 0;
55
+ this.MAX_CACHE_SIZE = 1000;
56
+ this.saveTimer = null;
57
+ this.SAVE_DEBOUNCE_MS = 5000;
55
58
  this.config = {
56
59
  enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
57
60
  databases: (config === null || config === void 0 ? void 0 : config.databases) || ['osv'],
@@ -60,6 +63,7 @@ class CVEChecker {
60
63
  updateInterval: (config === null || config === void 0 ? void 0 : config.updateInterval) || 24,
61
64
  cacheDir: (config === null || config === void 0 ? void 0 : config.cacheDir) || './.security-cache',
62
65
  };
66
+ this.cache = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
63
67
  if (this.config.enabled) {
64
68
  this.loadCache();
65
69
  }
@@ -96,7 +100,7 @@ class CVEChecker {
96
100
  };
97
101
  // Cache result
98
102
  this.cache.set(cacheKey, result);
99
- this.saveCache();
103
+ this.debouncedSaveCache();
100
104
  return result;
101
105
  });
102
106
  }
@@ -243,7 +247,11 @@ class CVEChecker {
243
247
  if (fs.existsSync(cacheFile)) {
244
248
  const data = fs.readFileSync(cacheFile, 'utf8');
245
249
  const parsed = JSON.parse(data);
246
- this.cache = new Map(Object.entries(parsed.cache));
250
+ const entries = Object.entries(parsed.cache);
251
+ const sortedEntries = entries.slice(-this.MAX_CACHE_SIZE);
252
+ sortedEntries.forEach(([key, value]) => {
253
+ this.cache.set(key, value);
254
+ });
247
255
  this.lastUpdate = parsed.lastUpdate || 0;
248
256
  }
249
257
  }
@@ -253,7 +261,19 @@ class CVEChecker {
253
261
  }
254
262
  }
255
263
  /**
256
- * Save cache to disk
264
+ * Debounced save cache to reduce I/O operations
265
+ */
266
+ debouncedSaveCache() {
267
+ if (this.saveTimer) {
268
+ clearTimeout(this.saveTimer);
269
+ }
270
+ this.saveTimer = setTimeout(() => {
271
+ this.saveCache();
272
+ this.saveTimer = null;
273
+ }, this.SAVE_DEBOUNCE_MS);
274
+ }
275
+ /**
276
+ * Save cache to disk immediately
257
277
  */
258
278
  saveCache() {
259
279
  try {
@@ -263,7 +283,7 @@ class CVEChecker {
263
283
  fs.mkdirSync(dir, { recursive: true });
264
284
  }
265
285
  const data = {
266
- cache: Object.fromEntries(this.cache),
286
+ cache: Object.fromEntries(this.cache.entries()),
267
287
  lastUpdate: Date.now(),
268
288
  };
269
289
  fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
@@ -282,5 +302,16 @@ class CVEChecker {
282
302
  this.lastUpdate = 0;
283
303
  this.saveCache();
284
304
  }
305
+ /**
306
+ * Cleanup resources
307
+ */
308
+ destroy() {
309
+ if (this.saveTimer) {
310
+ clearTimeout(this.saveTimer);
311
+ this.saveTimer = null;
312
+ }
313
+ this.saveCache();
314
+ this.cache.clear();
315
+ }
285
316
  }
286
317
  exports.CVEChecker = CVEChecker;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Simple LRU (Least Recently Used) Cache implementation with size limits
3
+ */
4
+ export declare class LRUCache<K, V> {
5
+ private cache;
6
+ private readonly maxSize;
7
+ constructor(maxSize?: number);
8
+ get(key: K): V | undefined;
9
+ set(key: K, value: V): void;
10
+ has(key: K): boolean;
11
+ delete(key: K): boolean;
12
+ clear(): void;
13
+ get size(): number;
14
+ entries(): IterableIterator<[K, V]>;
15
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LRUCache = void 0;
4
+ /**
5
+ * Simple LRU (Least Recently Used) Cache implementation with size limits
6
+ */
7
+ class LRUCache {
8
+ constructor(maxSize = 1000) {
9
+ this.cache = new Map();
10
+ this.maxSize = maxSize;
11
+ }
12
+ get(key) {
13
+ if (!this.cache.has(key)) {
14
+ return undefined;
15
+ }
16
+ const value = this.cache.get(key);
17
+ this.cache.delete(key);
18
+ this.cache.set(key, value);
19
+ return value;
20
+ }
21
+ set(key, value) {
22
+ if (this.cache.has(key)) {
23
+ this.cache.delete(key);
24
+ }
25
+ else if (this.cache.size >= this.maxSize) {
26
+ const firstKey = this.cache.keys().next().value;
27
+ if (firstKey !== undefined) {
28
+ this.cache.delete(firstKey);
29
+ }
30
+ }
31
+ this.cache.set(key, value);
32
+ }
33
+ has(key) {
34
+ return this.cache.has(key);
35
+ }
36
+ delete(key) {
37
+ return this.cache.delete(key);
38
+ }
39
+ clear() {
40
+ this.cache.clear();
41
+ }
42
+ get size() {
43
+ return this.cache.size;
44
+ }
45
+ entries() {
46
+ return this.cache.entries();
47
+ }
48
+ }
49
+ exports.LRUCache = LRUCache;
@@ -6,6 +6,7 @@ export declare class MetricsCollector {
6
6
  private config;
7
7
  private metricsBuffer;
8
8
  private readonly bufferSize;
9
+ private readonly maxBufferSize;
9
10
  constructor(config?: MetricsConfig);
10
11
  /**
11
12
  * Record a security event
@@ -43,6 +43,7 @@ class MetricsCollector {
43
43
  constructor(config) {
44
44
  this.metricsBuffer = [];
45
45
  this.bufferSize = 100;
46
+ this.maxBufferSize = 500;
46
47
  this.config = {
47
48
  enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
48
49
  output: (config === null || config === void 0 ? void 0 : config.output) || 'stdout',
@@ -56,6 +57,9 @@ class MetricsCollector {
56
57
  if (!this.config.enabled) {
57
58
  return;
58
59
  }
60
+ if (this.metricsBuffer.length >= this.maxBufferSize) {
61
+ this.flush();
62
+ }
59
63
  this.metricsBuffer.push(event);
60
64
  if (this.metricsBuffer.length >= this.bufferSize) {
61
65
  this.flush();
@@ -186,6 +190,7 @@ class MetricsCollector {
186
190
  cve_detected: 0,
187
191
  license_blocked: 0,
188
192
  package_too_new: 0,
193
+ author_blocked: 0,
189
194
  };
190
195
  this.metricsBuffer.forEach(metric => {
191
196
  summary[metric.event] = (summary[metric.event] || 0) + 1;
package/build/types.d.ts CHANGED
@@ -45,6 +45,17 @@ export interface PackageAgeConfig {
45
45
  minVersionAgeDays?: number;
46
46
  warnOnly?: boolean;
47
47
  }
48
+ export interface AuthorFilterConfig {
49
+ enabled: boolean;
50
+ blockedAuthors?: string[];
51
+ blockedAuthorPatterns?: string[];
52
+ blockedEmails?: string[];
53
+ blockedEmailPatterns?: string[];
54
+ blockedEmailDomains?: string[];
55
+ blockedRegions?: string[];
56
+ requireVerifiedEmail?: boolean;
57
+ warnOnly?: boolean;
58
+ }
48
59
  export interface SecurityConfig extends Config {
49
60
  blockedVersions?: string[];
50
61
  blockedPatterns?: string[];
@@ -61,6 +72,7 @@ export interface SecurityConfig extends Config {
61
72
  logger?: LoggerConfig;
62
73
  metrics?: MetricsConfig;
63
74
  packageAge?: PackageAgeConfig;
75
+ authorFilter?: AuthorFilterConfig;
64
76
  errorHandling?: {
65
77
  onFilterError?: 'fail-open' | 'fail-closed';
66
78
  onCveCheckError?: 'fail-open' | 'fail-closed';
@@ -111,9 +123,20 @@ export interface CVECheckResult {
111
123
  }
112
124
  export interface MetricsData {
113
125
  timestamp: string;
114
- event: 'block' | 'fallback' | 'publish_rejected' | 'cve_detected' | 'license_blocked' | 'package_too_new';
126
+ event: 'block' | 'fallback' | 'publish_rejected' | 'cve_detected' | 'license_blocked' | 'package_too_new' | 'author_blocked';
115
127
  packageName: string;
116
128
  version?: string;
117
129
  reason: string;
118
130
  metadata?: Record<string, any>;
119
131
  }
132
+ export interface AuthorInfo {
133
+ name?: string;
134
+ email?: string;
135
+ url?: string;
136
+ }
137
+ export interface AuthorCheckResult {
138
+ allowed: boolean;
139
+ reason?: string;
140
+ blockedBy?: 'name' | 'email' | 'domain' | 'region';
141
+ authorInfo?: AuthorInfo;
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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",