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 +67 -5
- package/build/index.d.ts +7 -0
- package/build/index.js +106 -82
- package/build/lib/author-checker.d.ts +44 -0
- package/build/lib/author-checker.js +194 -0
- package/build/lib/cve-checker.d.ts +12 -1
- package/build/lib/cve-checker.js +36 -5
- package/build/lib/lru-cache.d.ts +15 -0
- package/build/lib/lru-cache.js +49 -0
- package/build/lib/metrics.d.ts +1 -0
- package/build/lib/metrics.js +5 -0
- package/build/types.d.ts +24 -1
- package/package.json +1 -1
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
|
+
[](https://stand-with-ukraine.pp.ua)
|
|
5
6
|
[](https://www.npmjs.com/package/verdaccio-security-filter)
|
|
6
7
|
[](https://www.npmjs.com/package/verdaccio-security-filter)
|
|
7
8
|
[](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
|
-
- ✅
|
|
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
|
|
666
|
-
6. **
|
|
667
|
-
7. **
|
|
668
|
-
8. **
|
|
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
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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 = ((
|
|
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
|
-
*
|
|
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
|
}
|
package/build/lib/cve-checker.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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;
|
package/build/lib/metrics.d.ts
CHANGED
package/build/lib/metrics.js
CHANGED
|
@@ -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