verdaccio-security-filter 1.1.0 → 1.3.0
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 +66 -5
- package/build/index.d.ts +2 -0
- package/build/index.js +51 -4
- package/build/lib/author-checker.d.ts +44 -0
- package/build/lib/author-checker.js +194 -0
- package/build/lib/metrics.js +1 -0
- package/build/types.d.ts +24 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,13 @@ This plugin implements **two independent security layers** for complete protecti
|
|
|
71
71
|
- **Minimum Package Age** - Require packages to exist for a minimum number of days
|
|
72
72
|
- **Minimum Version Age** - Require specific versions to exist for a minimum number of days
|
|
73
73
|
- **Warn-Only Mode** - Log warnings without blocking
|
|
74
|
+
- **Author Filtering** - Block packages based on author information
|
|
75
|
+
- **Block by Author Name** - Block specific author names or patterns
|
|
76
|
+
- **Block by Email** - Block specific email addresses or patterns
|
|
77
|
+
- **Block by Email Domain** - Block entire email domains (e.g., `.ru`, `yandex.ru`)
|
|
78
|
+
- **Block by Region** - Block authors from specific regions based on email domains
|
|
79
|
+
- **Maintainer & Contributor Checking** - Validates all package maintainers and contributors
|
|
80
|
+
- **Warn-Only Mode** - Log warnings without blocking
|
|
74
81
|
- **Middleware Interception** - HTTP middleware that blocks tarball downloads for blocked packages
|
|
75
82
|
- Prevents blocked packages from being installed as dependencies
|
|
76
83
|
- Intercepts requests to `/:package/-/:filename.tgz` routes
|
|
@@ -467,6 +474,20 @@ middlewares:
|
|
|
467
474
|
| `packageAge.minVersionAgeDays` | `number` | `undefined` | Minimum age for versions (days) |
|
|
468
475
|
| `packageAge.warnOnly` | `boolean` | `false` | Only warn, don't block |
|
|
469
476
|
|
|
477
|
+
### Author Filter Options
|
|
478
|
+
|
|
479
|
+
| Option | Type | Default | Description |
|
|
480
|
+
|--------|------|---------|-------------|
|
|
481
|
+
| `authorFilter.enabled` | `boolean` | `false` | Enable author filtering |
|
|
482
|
+
| `authorFilter.blockedAuthors` | `string[]` | `[]` | List of author names to block |
|
|
483
|
+
| `authorFilter.blockedAuthorPatterns` | `string[]` | `[]` | Regex patterns for author names |
|
|
484
|
+
| `authorFilter.blockedEmails` | `string[]` | `[]` | List of email addresses to block |
|
|
485
|
+
| `authorFilter.blockedEmailPatterns` | `string[]` | `[]` | Regex patterns for author emails |
|
|
486
|
+
| `authorFilter.blockedEmailDomains` | `string[]` | `[]` | Email domains to block (e.g., `.ru`, `yandex.ru`) |
|
|
487
|
+
| `authorFilter.blockedRegions` | `string[]` | `[]` | Region codes to block (`ru`, `cn`, `by`, `kp`, `ir`, `sy`, `cu`, `sd`) |
|
|
488
|
+
| `authorFilter.requireVerifiedEmail` | `boolean` | `false` | Require author email information |
|
|
489
|
+
| `authorFilter.warnOnly` | `boolean` | `false` | Only warn, don't block |
|
|
490
|
+
|
|
470
491
|
### Error Handling Options
|
|
471
492
|
|
|
472
493
|
| Option | Type | Default | Description |
|
|
@@ -533,13 +554,14 @@ npm test:watch
|
|
|
533
554
|
```
|
|
534
555
|
|
|
535
556
|
**Test Results:**
|
|
536
|
-
- ✅
|
|
557
|
+
- ✅ 108 tests passing
|
|
537
558
|
- ✅ 60%+ code coverage
|
|
538
559
|
- ✅ SecurityFilterPlugin: 21 tests
|
|
539
560
|
- ✅ PackageAgeChecker: 13 tests
|
|
540
561
|
- ✅ SecurityLogger: 16 tests
|
|
541
562
|
- ✅ LicenseChecker: 15 tests
|
|
542
563
|
- ✅ WhitelistChecker: 15 tests
|
|
564
|
+
- ✅ AuthorChecker: 28 tests
|
|
543
565
|
|
|
544
566
|
## 🔧 Development
|
|
545
567
|
|
|
@@ -567,6 +589,7 @@ When metrics are enabled, the plugin tracks:
|
|
|
567
589
|
- `cve_detected` - CVE vulnerabilities found
|
|
568
590
|
- `license_blocked` - Packages blocked by license rules
|
|
569
591
|
- `package_too_new` - Packages/versions blocked due to age restrictions
|
|
592
|
+
- `author_blocked` - Packages blocked due to author/region filtering
|
|
570
593
|
|
|
571
594
|
Example metrics output:
|
|
572
595
|
```json
|
|
@@ -656,16 +679,54 @@ packageAge:
|
|
|
656
679
|
warnOnly: false # Block, don't just warn
|
|
657
680
|
```
|
|
658
681
|
|
|
682
|
+
### 7. Block Packages by Author/Region
|
|
683
|
+
|
|
684
|
+
```yaml
|
|
685
|
+
# Block packages from specific authors, emails, or regions
|
|
686
|
+
authorFilter:
|
|
687
|
+
enabled: true
|
|
688
|
+
# Block specific author names
|
|
689
|
+
blockedAuthors:
|
|
690
|
+
- "Suspicious Author"
|
|
691
|
+
- "Known Bad Actor"
|
|
692
|
+
# Block author names matching patterns
|
|
693
|
+
blockedAuthorPatterns:
|
|
694
|
+
- "^Bot.*"
|
|
695
|
+
- ".*Spammer$"
|
|
696
|
+
# Block specific email addresses
|
|
697
|
+
blockedEmails:
|
|
698
|
+
- "malicious@example.com"
|
|
699
|
+
# Block email patterns
|
|
700
|
+
blockedEmailPatterns:
|
|
701
|
+
- ".*@temporary-mail\\.com$"
|
|
702
|
+
# Block entire email domains
|
|
703
|
+
blockedEmailDomains:
|
|
704
|
+
- ".ru" # All .ru domains
|
|
705
|
+
- "yandex.ru" # Specific domain
|
|
706
|
+
- "mail.ru"
|
|
707
|
+
# Block by region (includes multiple common domains)
|
|
708
|
+
blockedRegions:
|
|
709
|
+
- "ru" # Russia (.ru, yandex.ru, mail.ru, etc.)
|
|
710
|
+
- "cn" # China (.cn, qq.com, 163.com, etc.)
|
|
711
|
+
- "by" # Belarus
|
|
712
|
+
- "kp" # North Korea
|
|
713
|
+
# Require author information
|
|
714
|
+
requireVerifiedEmail: true
|
|
715
|
+
# Warn-only mode (don't block, just log)
|
|
716
|
+
warnOnly: false
|
|
717
|
+
```
|
|
718
|
+
|
|
659
719
|
## 🔐 Security Best Practices
|
|
660
720
|
|
|
661
721
|
1. **Enable CVE Scanning** - Automatically detect and block vulnerable packages
|
|
662
722
|
2. **Use Whitelist Mode** - For maximum security in sensitive environments
|
|
663
723
|
3. **Enforce License Compliance** - Prevent legal issues with license filtering
|
|
664
724
|
4. **Enable Package Age Filtering** - Block newly published packages to prevent supply chain attacks
|
|
665
|
-
5. **Enable
|
|
666
|
-
6. **
|
|
667
|
-
7. **
|
|
668
|
-
8. **
|
|
725
|
+
5. **Enable Author Filtering** - Block packages from untrusted authors or regions
|
|
726
|
+
6. **Enable Metrics** - Track security events for audit and compliance
|
|
727
|
+
7. **Regular Updates** - Keep the plugin and CVE database cache updated
|
|
728
|
+
8. **Test Rules** - Use dry run mode (planned) before applying strict rules
|
|
729
|
+
9. **Monitor Logs** - Review security logs regularly for suspicious activity
|
|
669
730
|
|
|
670
731
|
## 🐛 Troubleshooting
|
|
671
732
|
|
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
|
|
@@ -83,4 +84,5 @@ export { WhitelistChecker } from './lib/whitelist-checker';
|
|
|
83
84
|
export { CVEChecker } from './lib/cve-checker';
|
|
84
85
|
export { LicenseChecker } from './lib/license-checker';
|
|
85
86
|
export { PackageAgeChecker } from './lib/package-age-checker';
|
|
87
|
+
export { AuthorChecker } from './lib/author-checker';
|
|
86
88
|
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)
|
|
@@ -375,7 +384,7 @@ class SecurityFilterPlugin {
|
|
|
375
384
|
*/
|
|
376
385
|
filter_metadata(packageInfo) {
|
|
377
386
|
return __awaiter(this, void 0, void 0, function* () {
|
|
378
|
-
var _a, _b, _c, _d, _e;
|
|
387
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
379
388
|
const packageName = packageInfo.name;
|
|
380
389
|
this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
|
|
381
390
|
try {
|
|
@@ -468,12 +477,48 @@ class SecurityFilterPlugin {
|
|
|
468
477
|
this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
|
|
469
478
|
}
|
|
470
479
|
}
|
|
480
|
+
// 5. Author Filter Check
|
|
481
|
+
if ((_e = this.config.authorFilter) === null || _e === void 0 ? void 0 : _e.enabled) {
|
|
482
|
+
const latestVersion = (_f = packageInfo['dist-tags']) === null || _f === void 0 ? void 0 : _f.latest;
|
|
483
|
+
if (latestVersion && ((_g = packageInfo.versions) === null || _g === void 0 ? void 0 : _g[latestVersion])) {
|
|
484
|
+
const versionData = packageInfo.versions[latestVersion];
|
|
485
|
+
const authorResult = this.authorChecker.checkAuthor(versionData);
|
|
486
|
+
if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
|
|
487
|
+
this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${authorResult.reason}`);
|
|
488
|
+
this.metrics.record({
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
event: 'author_blocked',
|
|
491
|
+
packageName,
|
|
492
|
+
version: latestVersion,
|
|
493
|
+
reason: authorResult.reason || 'Author not allowed',
|
|
494
|
+
metadata: {
|
|
495
|
+
blockedBy: authorResult.blockedBy,
|
|
496
|
+
authorInfo: authorResult.authorInfo,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
500
|
+
blocked: true,
|
|
501
|
+
reason: authorResult.reason || 'Author not allowed',
|
|
502
|
+
blockedBy: authorResult.blockedBy,
|
|
503
|
+
authorInfo: authorResult.authorInfo,
|
|
504
|
+
plugin: {
|
|
505
|
+
name: 'verdaccio-security-filter',
|
|
506
|
+
version: '2.0.0',
|
|
507
|
+
},
|
|
508
|
+
blockedAt: new Date().toISOString(),
|
|
509
|
+
} });
|
|
510
|
+
}
|
|
511
|
+
else if (!authorResult.allowed && this.config.authorFilter.warnOnly) {
|
|
512
|
+
this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName} - ${authorResult.reason} (warn-only mode)`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
471
516
|
this.logger.info(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
|
|
472
517
|
return packageInfo;
|
|
473
518
|
}
|
|
474
519
|
catch (error) {
|
|
475
520
|
this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
|
|
476
|
-
const errorStrategy = ((
|
|
521
|
+
const errorStrategy = ((_h = this.config.errorHandling) === null || _h === void 0 ? void 0 : _h.onFilterError) || 'fail-open';
|
|
477
522
|
if (errorStrategy === 'fail-closed') {
|
|
478
523
|
this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
|
|
479
524
|
this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
|
|
@@ -580,4 +625,6 @@ var license_checker_2 = require("./lib/license-checker");
|
|
|
580
625
|
Object.defineProperty(exports, "LicenseChecker", { enumerable: true, get: function () { return license_checker_2.LicenseChecker; } });
|
|
581
626
|
var package_age_checker_2 = require("./lib/package-age-checker");
|
|
582
627
|
Object.defineProperty(exports, "PackageAgeChecker", { enumerable: true, get: function () { return package_age_checker_2.PackageAgeChecker; } });
|
|
628
|
+
var author_checker_2 = require("./lib/author-checker");
|
|
629
|
+
Object.defineProperty(exports, "AuthorChecker", { enumerable: true, get: function () { return author_checker_2.AuthorChecker; } });
|
|
583
630
|
__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;
|
package/build/lib/metrics.js
CHANGED
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