verdaccio-security-filter 1.3.9 → 1.3.11
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 +20 -39
- package/build/index.d.ts +4 -0
- package/build/index.js +95 -134
- package/build/lib/metrics.d.ts +2 -2
- package/build/lib/metrics.js +10 -20
- package/build/lib/whitelist-checker.d.ts +2 -5
- package/build/lib/whitelist-checker.js +11 -27
- package/build/types.d.ts +2 -27
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -36,17 +36,15 @@ This plugin implements **two independent security layers** for complete protecti
|
|
|
36
36
|
|
|
37
37
|
#### 🔒 Version Management
|
|
38
38
|
- **Version Range Blocking** - Block vulnerable versions using semver ranges
|
|
39
|
-
-
|
|
40
|
-
- **Fallback Strategy**: Transparently redirect to safe versions
|
|
39
|
+
- Matching versions are hidden from metadata and their tarballs return 403, so npm resolves to the highest allowed version
|
|
41
40
|
- **Exact Version Blocking** - Block specific package@version combinations
|
|
42
41
|
- **Semver Support** - Full semver syntax (`^`, `~`, `>`, `>=`, `<`, `<=`, `x`, `*`)
|
|
43
42
|
|
|
44
43
|
#### 🛡️ CVE & Vulnerability Scanning
|
|
45
44
|
- **OSV Database Integration** - Automatic vulnerability scanning via [OSV API](https://osv.dev/)
|
|
46
45
|
- **Severity Filtering** - Filter by severity: `low`, `medium`, `high`, `critical`
|
|
47
|
-
- **Auto-Block Vulnerable
|
|
48
|
-
- **Caching System** -
|
|
49
|
-
- **Multiple Database Support** - Ready for OSV, Snyk, GitHub Advisory
|
|
46
|
+
- **Auto-Block Vulnerable Versions** - Remove versions with known CVEs from metadata
|
|
47
|
+
- **Caching System** - One OSV lookup per package, cached per package with a configurable refresh interval
|
|
50
48
|
|
|
51
49
|
#### ⚖️ License Compliance
|
|
52
50
|
- **License Filtering** - Enforce license policies with allowed/blocked lists
|
|
@@ -62,7 +60,7 @@ This plugin implements **two independent security layers** for complete protecti
|
|
|
62
60
|
|
|
63
61
|
#### 📊 Monitoring & Observability
|
|
64
62
|
- **Enhanced Logging** - Configurable log levels (`debug`, `info`, `warn`, `error`)
|
|
65
|
-
- **Metrics Collection** - Track security events (blocks
|
|
63
|
+
- **Metrics Collection** - Track security events (blocks with their reason, author blocks)
|
|
66
64
|
- **Security Audit Trail** - Detailed logging of all security decisions
|
|
67
65
|
- **Customizable Output** - Log to stdout or file in JSON format
|
|
68
66
|
|
|
@@ -83,9 +81,7 @@ This plugin implements **two independent security layers** for complete protecti
|
|
|
83
81
|
- Prevents blocked packages from being installed as dependencies
|
|
84
82
|
- Intercepts requests to `/:package/-/:filename.tgz` routes
|
|
85
83
|
- Returns 403 Forbidden with detailed error messages
|
|
86
|
-
- **Size Limits** - Enforce minimum and maximum package sizes
|
|
87
84
|
- **Metadata Validation** - Verify package integrity and detect dangerous characters
|
|
88
|
-
- **Checksum Enforcement** - Ensure package integrity
|
|
89
85
|
|
|
90
86
|
#### ⚙️ Reliability & Error Handling
|
|
91
87
|
- **Fail-Safe/Fail-Closed Configuration** - Choose error handling strategy
|
|
@@ -274,7 +270,6 @@ filters:
|
|
|
274
270
|
enabled: true
|
|
275
271
|
databases:
|
|
276
272
|
- osv
|
|
277
|
-
- github
|
|
278
273
|
severity:
|
|
279
274
|
- high
|
|
280
275
|
- critical
|
|
@@ -304,8 +299,7 @@ filters:
|
|
|
304
299
|
versionRangeRules:
|
|
305
300
|
- package: lodash
|
|
306
301
|
range: "<4.17.21"
|
|
307
|
-
strategy:
|
|
308
|
-
fallbackVersion: "4.17.21"
|
|
302
|
+
strategy: block
|
|
309
303
|
reason: "CVE-2021-23337: Command injection"
|
|
310
304
|
|
|
311
305
|
- package: minimist
|
|
@@ -379,7 +373,7 @@ filters:
|
|
|
379
373
|
|
|
380
374
|
cveCheck:
|
|
381
375
|
enabled: true
|
|
382
|
-
databases: [osv
|
|
376
|
+
databases: [osv]
|
|
383
377
|
severity: [low, medium, high, critical] # Block ALL
|
|
384
378
|
autoBlock: true
|
|
385
379
|
updateInterval: 6 # Check every 6 hours
|
|
@@ -389,9 +383,6 @@ filters:
|
|
|
389
383
|
blocked: [GPL-3.0, AGPL-3.0, LGPL-3.0]
|
|
390
384
|
requireLicense: true
|
|
391
385
|
|
|
392
|
-
minPackageSize: 10000 # 10KB minimum
|
|
393
|
-
maxPackageSize: 10485760 # 10MB maximum
|
|
394
|
-
|
|
395
386
|
logger:
|
|
396
387
|
level: debug # Log everything
|
|
397
388
|
enabled: true
|
|
@@ -419,23 +410,22 @@ middlewares:
|
|
|
419
410
|
| `blockedPatterns` | `string[]` | `[]` | Regex patterns for package names |
|
|
420
411
|
| `allowedScopes` | `string[]` | `[]` | Allowed package scopes |
|
|
421
412
|
| `blockedScopes` | `string[]` | `[]` | Blocked package scopes |
|
|
422
|
-
| `minPackageSize` | `number` | `0` | Minimum package size in bytes |
|
|
423
|
-
| `maxPackageSize` | `number` | `104857600` | Maximum package size (100MB) |
|
|
424
|
-
| `enforceChecksum` | `boolean` | `true` | Enforce checksum validation |
|
|
425
413
|
|
|
426
414
|
### CVE Check Options
|
|
427
415
|
|
|
428
416
|
| Option | Type | Default | Description |
|
|
429
417
|
|--------|------|---------|-------------|
|
|
430
418
|
| `cveCheck.enabled` | `boolean` | `false` | Enable CVE scanning |
|
|
431
|
-
| `cveCheck.databases` | `string[]` | `['osv']` |
|
|
419
|
+
| `cveCheck.databases` | `string[]` | `['osv']` | Vulnerability databases. Only `osv` is implemented; it already includes GitHub Security Advisories |
|
|
432
420
|
| `cveCheck.severity` | `string[]` | `['high', 'critical']` | Severity levels to check |
|
|
433
421
|
| `cveCheck.autoBlock` | `boolean` | `false` | Remove vulnerable versions from metadata (whole package blocked if every version is vulnerable) |
|
|
434
|
-
| `cveCheck.updateInterval` | `number` | `24` |
|
|
422
|
+
| `cveCheck.updateInterval` | `number` | `24` | How long a package's vulnerability data is cached (hours) |
|
|
435
423
|
| `cveCheck.cacheDir` | `string` | `./.security-cache` | Cache directory path |
|
|
436
424
|
|
|
437
425
|
> **Note on `autoBlock` and dist-tags:** only the vulnerable versions are removed; unaffected versions stay installable. Any dist-tag (including `latest`) that points to a removed version is dropped and **not** re-pointed to another version. If `latest` is vulnerable, `npm install <pkg>` without a version or range will fail - install an explicit safe version or range instead (e.g. `npm install <pkg>@^1.2.0`).
|
|
438
426
|
|
|
427
|
+
> **When OSV is unreachable:** a lookup gets one retry with a 5 second timeout each. After a failure the same package is not looked up again for 60 seconds. Previously cached data, even if older than `updateInterval`, keeps being used. With no cached data, `errorHandling.onCveCheckError` decides: `fail-open` serves the package unchecked, `fail-closed` blocks it.
|
|
428
|
+
|
|
439
429
|
### License Options
|
|
440
430
|
|
|
441
431
|
| Option | Type | Default | Description |
|
|
@@ -496,8 +486,7 @@ middlewares:
|
|
|
496
486
|
| Option | Type | Default | Description |
|
|
497
487
|
|--------|------|---------|-------------|
|
|
498
488
|
| `errorHandling.onFilterError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when filter errors occur |
|
|
499
|
-
| `errorHandling.onCveCheckError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when
|
|
500
|
-
| `errorHandling.onLicenseCheckError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when license check fails |
|
|
489
|
+
| `errorHandling.onCveCheckError` | `'fail-open' \| 'fail-closed'` | `'fail-open'` | Strategy when the vulnerability lookup fails and no cached data exists |
|
|
501
490
|
|
|
502
491
|
**Error Handling Strategies:**
|
|
503
492
|
- **fail-open** (Default): Allow packages through on errors (prioritizes availability)
|
|
@@ -509,19 +498,16 @@ middlewares:
|
|
|
509
498
|
errorHandling:
|
|
510
499
|
onFilterError: fail-closed
|
|
511
500
|
onCveCheckError: fail-closed
|
|
512
|
-
onLicenseCheckError: fail-closed
|
|
513
501
|
|
|
514
502
|
# Development: Availability-first approach
|
|
515
503
|
errorHandling:
|
|
516
504
|
onFilterError: fail-open
|
|
517
505
|
onCveCheckError: fail-open
|
|
518
|
-
onLicenseCheckError: fail-open
|
|
519
506
|
|
|
520
|
-
# Balanced:
|
|
507
|
+
# Balanced: block only when vulnerability data is unavailable
|
|
521
508
|
errorHandling:
|
|
522
509
|
onFilterError: fail-open
|
|
523
|
-
onCveCheckError: fail-
|
|
524
|
-
onLicenseCheckError: fail-closed
|
|
510
|
+
onCveCheckError: fail-closed
|
|
525
511
|
```
|
|
526
512
|
|
|
527
513
|
### Version Range Rules
|
|
@@ -530,8 +516,7 @@ errorHandling:
|
|
|
530
516
|
{
|
|
531
517
|
package: string; // Package name
|
|
532
518
|
range: string; // Semver range
|
|
533
|
-
strategy: 'block'
|
|
534
|
-
fallbackVersion?: string; // Required for fallback strategy
|
|
519
|
+
strategy: 'block'; // 'fallback' is deprecated and behaves like 'block'
|
|
535
520
|
reason?: string; // Explanation for blocking
|
|
536
521
|
}
|
|
537
522
|
```
|
|
@@ -586,12 +571,7 @@ npm run build -- --watch
|
|
|
586
571
|
|
|
587
572
|
When metrics are enabled, the plugin tracks:
|
|
588
573
|
|
|
589
|
-
- `block` - Packages blocked by
|
|
590
|
-
- `fallback` - Versions redirected to safe alternatives
|
|
591
|
-
- `publish_rejected` - Publish attempts rejected
|
|
592
|
-
- `cve_detected` - CVE vulnerabilities found
|
|
593
|
-
- `license_blocked` - Packages blocked by license rules
|
|
594
|
-
- `package_too_new` - Packages/versions blocked due to age restrictions
|
|
574
|
+
- `block` - Packages or versions blocked by rules, CVE, license or age checks; the `reason` field says which
|
|
595
575
|
- `author_blocked` - Packages blocked due to author/region filtering
|
|
596
576
|
|
|
597
577
|
Example metrics output:
|
|
@@ -621,17 +601,18 @@ versionRangeRules:
|
|
|
621
601
|
reason: "CVE-2021-23337: Command injection vulnerability"
|
|
622
602
|
```
|
|
623
603
|
|
|
624
|
-
### 2.
|
|
604
|
+
### 2. Steer Installs to a Patched Version
|
|
625
605
|
|
|
626
606
|
```yaml
|
|
627
607
|
versionRangeRules:
|
|
628
608
|
- package: axios
|
|
629
609
|
range: "0.21.1"
|
|
630
|
-
strategy:
|
|
631
|
-
fallbackVersion: "0.21.4"
|
|
610
|
+
strategy: block
|
|
632
611
|
reason: "SSRF vulnerability fix"
|
|
633
612
|
```
|
|
634
613
|
|
|
614
|
+
The blocked version disappears from the package metadata, so a range such as `axios@^0.21.0` resolves to the newest allowed version. A lockfile pinned to the blocked version fails with 403 until it is updated.
|
|
615
|
+
|
|
635
616
|
### 3. Enforce Corporate License Policy
|
|
636
617
|
|
|
637
618
|
```yaml
|
|
@@ -665,7 +646,7 @@ whitelist:
|
|
|
665
646
|
```yaml
|
|
666
647
|
cveCheck:
|
|
667
648
|
enabled: true
|
|
668
|
-
databases: [osv
|
|
649
|
+
databases: [osv]
|
|
669
650
|
severity: [high, critical]
|
|
670
651
|
autoBlock: true
|
|
671
652
|
updateInterval: 12
|
package/build/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
|
|
|
6
6
|
logger: SecurityLogger;
|
|
7
7
|
config: SecurityConfig;
|
|
8
8
|
private readonly securityRules;
|
|
9
|
+
private readonly blockedPatternRegexes;
|
|
9
10
|
private readonly metrics;
|
|
10
11
|
private readonly whitelistChecker;
|
|
11
12
|
private readonly cveChecker;
|
|
@@ -20,7 +21,10 @@ export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityC
|
|
|
20
21
|
private _isScopeAllowed;
|
|
21
22
|
private _getVersionRangeRule;
|
|
22
23
|
register_middlewares(app: Application, _auth: IBasicAuth<SecurityConfig>, _storage: IStorageManager<SecurityConfig>): void;
|
|
24
|
+
private _blockedPackage;
|
|
25
|
+
private _removeVersions;
|
|
23
26
|
filter_metadata(packageInfo: Package): Promise<Package>;
|
|
27
|
+
private _versionBlockReason;
|
|
24
28
|
private _checkPackageBlock;
|
|
25
29
|
private _getAppliedRules;
|
|
26
30
|
destroy(): void;
|
package/build/index.js
CHANGED
|
@@ -47,6 +47,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.AuthorChecker = exports.PackageAgeChecker = exports.LicenseChecker = exports.CVEChecker = exports.WhitelistChecker = exports.MetricsCollector = exports.SecurityLogger = void 0;
|
|
49
49
|
const semver = __importStar(require("semver"));
|
|
50
|
+
const fs_1 = require("fs");
|
|
51
|
+
const path_1 = require("path");
|
|
50
52
|
const logger_1 = require("./lib/logger");
|
|
51
53
|
const metrics_1 = require("./lib/metrics");
|
|
52
54
|
const whitelist_checker_1 = require("./lib/whitelist-checker");
|
|
@@ -54,6 +56,7 @@ const cve_checker_1 = require("./lib/cve-checker");
|
|
|
54
56
|
const license_checker_1 = require("./lib/license-checker");
|
|
55
57
|
const package_age_checker_1 = require("./lib/package-age-checker");
|
|
56
58
|
const author_checker_1 = require("./lib/author-checker");
|
|
59
|
+
const PLUGIN_VERSION = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
57
60
|
class SecurityFilterPlugin {
|
|
58
61
|
constructor(config, options) {
|
|
59
62
|
this.config = config || {};
|
|
@@ -61,7 +64,6 @@ class SecurityFilterPlugin {
|
|
|
61
64
|
this.config.errorHandling = {
|
|
62
65
|
onFilterError: 'fail-open',
|
|
63
66
|
onCveCheckError: 'fail-open',
|
|
64
|
-
onLicenseCheckError: 'fail-open',
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
this.logger = new logger_1.SecurityLogger(options.logger, this.config.logger);
|
|
@@ -74,13 +76,19 @@ class SecurityFilterPlugin {
|
|
|
74
76
|
this.securityRules = {
|
|
75
77
|
blockedVersions: this.config.blockedVersions || [],
|
|
76
78
|
blockedPatterns: this.config.blockedPatterns || [],
|
|
77
|
-
minPackageSize: this.config.minPackageSize || 0,
|
|
78
|
-
maxPackageSize: this.config.maxPackageSize || 100 * 1024 * 1024,
|
|
79
79
|
allowedScopes: this.config.allowedScopes || [],
|
|
80
80
|
blockedScopes: this.config.blockedScopes || [],
|
|
81
|
-
enforceChecksum: this.config.enforceChecksum !== false,
|
|
82
81
|
versionRangeRules: this._parseVersionRangeRules(this.config.versionRangeRules || [])
|
|
83
82
|
};
|
|
83
|
+
this.blockedPatternRegexes = this.securityRules.blockedPatterns.flatMap(pattern => {
|
|
84
|
+
try {
|
|
85
|
+
return [new RegExp(pattern)];
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.logger.warn(`Invalid blocked pattern "${pattern}", skipping: ${error.message}`);
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
84
92
|
this._logInitialization();
|
|
85
93
|
}
|
|
86
94
|
_parseVersionRangeRules(rules) {
|
|
@@ -89,9 +97,8 @@ class SecurityFilterPlugin {
|
|
|
89
97
|
this.logger.warn(`Invalid version range rule at index ${index}, skipping`);
|
|
90
98
|
return null;
|
|
91
99
|
}
|
|
92
|
-
if (rule.strategy === 'fallback'
|
|
93
|
-
this.logger.warn(`
|
|
94
|
-
return null;
|
|
100
|
+
if (rule.strategy === 'fallback') {
|
|
101
|
+
this.logger.warn(`Strategy "fallback" for ${rule.package} is deprecated and works like "block": matching versions are hidden, so npm resolves to the highest allowed one`);
|
|
95
102
|
}
|
|
96
103
|
try {
|
|
97
104
|
const validRange = semver.validRange(rule.range);
|
|
@@ -156,10 +163,7 @@ class SecurityFilterPlugin {
|
|
|
156
163
|
return features;
|
|
157
164
|
}
|
|
158
165
|
_isBlockedByPattern(packageName) {
|
|
159
|
-
return this.
|
|
160
|
-
const regex = new RegExp(pattern);
|
|
161
|
-
return regex.test(packageName);
|
|
162
|
-
});
|
|
166
|
+
return this.blockedPatternRegexes.some(regex => regex.test(packageName));
|
|
163
167
|
}
|
|
164
168
|
_isScopeAllowed(packageName) {
|
|
165
169
|
if (!packageName.startsWith('@')) {
|
|
@@ -212,9 +216,11 @@ class SecurityFilterPlugin {
|
|
|
212
216
|
const tarballFile = urlMatch[3];
|
|
213
217
|
let version;
|
|
214
218
|
if (tarballFile) {
|
|
215
|
-
const
|
|
216
|
-
version =
|
|
217
|
-
|
|
219
|
+
const filePrefix = `${urlMatch[2]}-`;
|
|
220
|
+
version = tarballFile.startsWith(filePrefix)
|
|
221
|
+
? tarballFile.slice(filePrefix.length, -'.tgz'.length)
|
|
222
|
+
: undefined;
|
|
223
|
+
this.logger.debug(`[Middleware] TARBALL REQUEST: ${packageName}@${version || 'unknown'}`);
|
|
218
224
|
const blockResult = yield this._checkPackageBlock(packageName, version);
|
|
219
225
|
if (blockResult.blocked) {
|
|
220
226
|
this.logger.warn(`[Middleware] [X] BLOCKED TARBALL: ${packageName}@${version || '*'} - ${blockResult.reason}`);
|
|
@@ -228,10 +234,10 @@ class SecurityFilterPlugin {
|
|
|
228
234
|
});
|
|
229
235
|
return;
|
|
230
236
|
}
|
|
231
|
-
this.logger.
|
|
237
|
+
this.logger.debug(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
|
|
232
238
|
}
|
|
233
239
|
else {
|
|
234
|
-
this.logger.
|
|
240
|
+
this.logger.debug(`[Middleware] METADATA REQUEST: ${packageName} - will intercept response`);
|
|
235
241
|
const blockResult = yield this._checkPackageBlock(packageName);
|
|
236
242
|
if (blockResult.blocked) {
|
|
237
243
|
this.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
|
|
@@ -244,7 +250,7 @@ class SecurityFilterPlugin {
|
|
|
244
250
|
reason: blockResult.reason,
|
|
245
251
|
plugin: {
|
|
246
252
|
name: 'verdaccio-security-filter',
|
|
247
|
-
version:
|
|
253
|
+
version: PLUGIN_VERSION,
|
|
248
254
|
mode: this.config.mode || 'blacklist',
|
|
249
255
|
},
|
|
250
256
|
blockedAt: new Date().toISOString(),
|
|
@@ -263,7 +269,7 @@ class SecurityFilterPlugin {
|
|
|
263
269
|
return;
|
|
264
270
|
}
|
|
265
271
|
else {
|
|
266
|
-
this.logger.
|
|
272
|
+
this.logger.debug(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
next();
|
|
@@ -275,28 +281,52 @@ class SecurityFilterPlugin {
|
|
|
275
281
|
}));
|
|
276
282
|
this.logger.info('[Middleware] Security filter middleware registered successfully');
|
|
277
283
|
}
|
|
284
|
+
_blockedPackage(pkg, reason, extra = {}) {
|
|
285
|
+
return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: Object.assign(Object.assign({ blocked: true, reason }, extra), { plugin: {
|
|
286
|
+
name: 'verdaccio-security-filter',
|
|
287
|
+
version: PLUGIN_VERSION,
|
|
288
|
+
mode: this.config.mode || 'blacklist',
|
|
289
|
+
}, blockedAt: new Date().toISOString() }) });
|
|
290
|
+
}
|
|
291
|
+
_removeVersions(pkg, removed) {
|
|
292
|
+
if (removed.length === 0) {
|
|
293
|
+
return pkg;
|
|
294
|
+
}
|
|
295
|
+
const gone = new Set(removed);
|
|
296
|
+
const versions = Object.assign({}, pkg.versions);
|
|
297
|
+
for (const version of gone) {
|
|
298
|
+
delete versions[version];
|
|
299
|
+
}
|
|
300
|
+
const distTags = Object.assign({}, pkg['dist-tags']);
|
|
301
|
+
for (const [tag, tagVersion] of Object.entries(distTags)) {
|
|
302
|
+
if (gone.has(tagVersion)) {
|
|
303
|
+
delete distTags[tag];
|
|
304
|
+
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to filtered version ${tagVersion}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return Object.assign(Object.assign({}, pkg), { versions, 'dist-tags': distTags });
|
|
308
|
+
}
|
|
278
309
|
filter_metadata(packageInfo) {
|
|
279
310
|
return __awaiter(this, void 0, void 0, function* () {
|
|
280
311
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
281
312
|
let pkg = packageInfo;
|
|
282
313
|
const packageName = pkg.name;
|
|
283
|
-
this.logger.
|
|
314
|
+
this.logger.debug(`[filter_metadata] --> Processing: ${packageName}`);
|
|
284
315
|
try {
|
|
285
316
|
const blockResult = yield this._checkPackageBlock(packageName);
|
|
286
317
|
if (blockResult.blocked) {
|
|
287
318
|
this.logger.warn(`[filter_metadata] BLOCKED: ${packageName}@* - ${blockResult.reason}`);
|
|
288
319
|
this.metrics.recordBlock(packageName, '*', blockResult.reason);
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
} });
|
|
320
|
+
return this._blockedPackage(pkg, blockResult.reason, { rules: this._getAppliedRules(packageName) });
|
|
321
|
+
}
|
|
322
|
+
const whitelistMode = this.config.mode === 'whitelist';
|
|
323
|
+
if (whitelistMode || this.securityRules.blockedVersions.length > 0 || this.securityRules.versionRangeRules.length > 0) {
|
|
324
|
+
const ruleBlocked = Object.keys(pkg.versions || {}).filter(version => this._versionBlockReason(packageName, version) !== null ||
|
|
325
|
+
(whitelistMode && !this.whitelistChecker.isWhitelisted(packageName, version).allowed));
|
|
326
|
+
if (ruleBlocked.length > 0) {
|
|
327
|
+
this.logger.warn(`[filter_metadata] RULE FILTERED: ${packageName}: ${ruleBlocked.join(', ')}`);
|
|
328
|
+
pkg = this._removeVersions(pkg, ruleBlocked);
|
|
329
|
+
}
|
|
300
330
|
}
|
|
301
331
|
if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
|
|
302
332
|
const versions = Object.keys(pkg.versions || {});
|
|
@@ -310,15 +340,7 @@ class SecurityFilterPlugin {
|
|
|
310
340
|
if (((_b = this.config.errorHandling) === null || _b === void 0 ? void 0 : _b.onCveCheckError) === 'fail-closed') {
|
|
311
341
|
const reason = `CVE check failed (fail-closed mode): ${error.message}`;
|
|
312
342
|
this.metrics.recordBlock(packageName, '*', reason);
|
|
313
|
-
return
|
|
314
|
-
blocked: true,
|
|
315
|
-
reason,
|
|
316
|
-
plugin: {
|
|
317
|
-
name: 'verdaccio-security-filter',
|
|
318
|
-
version: '2.0.0',
|
|
319
|
-
},
|
|
320
|
-
blockedAt: new Date().toISOString(),
|
|
321
|
-
} });
|
|
343
|
+
return this._blockedPackage(pkg, reason);
|
|
322
344
|
}
|
|
323
345
|
}
|
|
324
346
|
if (vulnerableVersions.length > 0 && !this.config.cveCheck.autoBlock) {
|
|
@@ -329,31 +351,13 @@ class SecurityFilterPlugin {
|
|
|
329
351
|
if (vulnerableVersions.length === versions.length) {
|
|
330
352
|
this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
|
|
331
353
|
this.metrics.recordBlock(packageName, '*', reason);
|
|
332
|
-
return
|
|
333
|
-
blocked: true,
|
|
334
|
-
reason,
|
|
335
|
-
vulnerableVersions,
|
|
336
|
-
plugin: {
|
|
337
|
-
name: 'verdaccio-security-filter',
|
|
338
|
-
version: '2.0.0',
|
|
339
|
-
},
|
|
340
|
-
blockedAt: new Date().toISOString(),
|
|
341
|
-
} });
|
|
354
|
+
return this._blockedPackage(pkg, reason, { vulnerableVersions });
|
|
342
355
|
}
|
|
343
356
|
this.logger.warn(`[filter_metadata] CVE FILTERED: ${packageName} - ${reason}: ${vulnerableVersions.join(', ')}`);
|
|
344
|
-
const filteredVersions = Object.assign({}, pkg.versions);
|
|
345
357
|
for (const version of vulnerableVersions) {
|
|
346
|
-
delete filteredVersions[version];
|
|
347
358
|
this.metrics.recordBlock(packageName, version, reason);
|
|
348
359
|
}
|
|
349
|
-
|
|
350
|
-
for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
|
|
351
|
-
if (vulnerableVersions.includes(tagVersion)) {
|
|
352
|
-
delete filteredDistTags[tag];
|
|
353
|
-
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to vulnerable version ${tagVersion}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
|
|
360
|
+
pkg = this._removeVersions(pkg, vulnerableVersions);
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
363
|
if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
|
|
@@ -362,66 +366,38 @@ class SecurityFilterPlugin {
|
|
|
362
366
|
const versionData = pkg.versions[latestVersion];
|
|
363
367
|
const licenseResult = this.licenseChecker.checkLicense(versionData);
|
|
364
368
|
if (!licenseResult.allowed) {
|
|
365
|
-
|
|
366
|
-
this.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
reason: licenseResult.reason || 'License not allowed',
|
|
370
|
-
license: licenseResult.license,
|
|
371
|
-
plugin: {
|
|
372
|
-
name: 'verdaccio-security-filter',
|
|
373
|
-
version: '2.0.0',
|
|
374
|
-
},
|
|
375
|
-
blockedAt: new Date().toISOString(),
|
|
376
|
-
} });
|
|
369
|
+
const reason = licenseResult.reason || 'License not allowed';
|
|
370
|
+
this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${reason}`);
|
|
371
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
372
|
+
return this._blockedPackage(pkg, reason, { license: licenseResult.license });
|
|
377
373
|
}
|
|
378
374
|
}
|
|
379
375
|
}
|
|
380
376
|
if ((_e = this.config.packageAge) === null || _e === void 0 ? void 0 : _e.enabled) {
|
|
381
377
|
const ageResult = this.packageAgeChecker.checkPackageAge(pkg);
|
|
382
378
|
if (!ageResult.allowed && !ageResult.warnOnly) {
|
|
383
|
-
|
|
384
|
-
this.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
reason: ageResult.reason || 'Package too new',
|
|
388
|
-
ageDays: ageResult.ageDays,
|
|
389
|
-
plugin: {
|
|
390
|
-
name: 'verdaccio-security-filter',
|
|
391
|
-
version: '2.0.0',
|
|
392
|
-
},
|
|
393
|
-
blockedAt: new Date().toISOString(),
|
|
394
|
-
} });
|
|
379
|
+
const reason = ageResult.reason || 'Package too new';
|
|
380
|
+
this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${reason}`);
|
|
381
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
382
|
+
return this._blockedPackage(pkg, reason, { ageDays: ageResult.ageDays });
|
|
395
383
|
}
|
|
396
384
|
else if (!ageResult.allowed && ageResult.warnOnly) {
|
|
397
385
|
this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
|
|
398
386
|
}
|
|
399
387
|
if (this.config.packageAge.minVersionAgeDays) {
|
|
400
|
-
const filteredVersions = Object.assign({}, pkg.versions);
|
|
401
388
|
const removedVersions = [];
|
|
402
|
-
for (const version of Object.keys(
|
|
389
|
+
for (const version of Object.keys(pkg.versions || {})) {
|
|
403
390
|
const versionAgeResult = this.packageAgeChecker.checkVersionAge(pkg, version);
|
|
404
391
|
if (!versionAgeResult.allowed && !versionAgeResult.warnOnly) {
|
|
405
392
|
this.logger.warn(`[filter_metadata] VERSION AGE FILTERED: ${packageName}@${version} - ${versionAgeResult.reason}`);
|
|
406
393
|
this.metrics.recordBlock(packageName, version, versionAgeResult.reason || 'Version too new');
|
|
407
|
-
delete filteredVersions[version];
|
|
408
394
|
removedVersions.push(version);
|
|
409
395
|
}
|
|
410
396
|
else if (!versionAgeResult.allowed && versionAgeResult.warnOnly) {
|
|
411
397
|
this.logger.warn(`[filter_metadata] VERSION AGE WARNING: ${packageName}@${version} - ${versionAgeResult.reason} (warn-only mode)`);
|
|
412
398
|
}
|
|
413
399
|
}
|
|
414
|
-
|
|
415
|
-
this.logger.info(`[filter_metadata] Removed ${removedVersions.length} too-new version(s) from ${packageName}: ${removedVersions.join(', ')}`);
|
|
416
|
-
const filteredDistTags = Object.assign({}, pkg['dist-tags']);
|
|
417
|
-
for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
|
|
418
|
-
if (removedVersions.includes(tagVersion)) {
|
|
419
|
-
delete filteredDistTags[tag];
|
|
420
|
-
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to filtered version ${tagVersion}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
|
|
424
|
-
}
|
|
400
|
+
pkg = this._removeVersions(pkg, removedVersions);
|
|
425
401
|
}
|
|
426
402
|
}
|
|
427
403
|
if ((_f = this.config.authorFilter) === null || _f === void 0 ? void 0 : _f.enabled) {
|
|
@@ -430,36 +406,30 @@ class SecurityFilterPlugin {
|
|
|
430
406
|
const versionData = pkg.versions[latestVersion];
|
|
431
407
|
const authorResult = this.authorChecker.checkAuthor(versionData);
|
|
432
408
|
if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
|
|
433
|
-
|
|
409
|
+
const reason = authorResult.reason || 'Author not allowed';
|
|
410
|
+
this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${reason}`);
|
|
434
411
|
this.metrics.record({
|
|
435
412
|
timestamp: new Date().toISOString(),
|
|
436
413
|
event: 'author_blocked',
|
|
437
414
|
packageName,
|
|
438
415
|
version: latestVersion,
|
|
439
|
-
reason
|
|
416
|
+
reason,
|
|
440
417
|
metadata: {
|
|
441
418
|
blockedBy: authorResult.blockedBy,
|
|
442
419
|
authorInfo: authorResult.authorInfo,
|
|
443
420
|
},
|
|
444
421
|
});
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
authorInfo: authorResult.authorInfo,
|
|
450
|
-
plugin: {
|
|
451
|
-
name: 'verdaccio-security-filter',
|
|
452
|
-
version: '2.0.0',
|
|
453
|
-
},
|
|
454
|
-
blockedAt: new Date().toISOString(),
|
|
455
|
-
} });
|
|
422
|
+
return this._blockedPackage(pkg, reason, {
|
|
423
|
+
blockedBy: authorResult.blockedBy,
|
|
424
|
+
authorInfo: authorResult.authorInfo,
|
|
425
|
+
});
|
|
456
426
|
}
|
|
457
427
|
else if (!authorResult.allowed && this.config.authorFilter.warnOnly) {
|
|
458
428
|
this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName} - ${authorResult.reason} (warn-only mode)`);
|
|
459
429
|
}
|
|
460
430
|
}
|
|
461
431
|
}
|
|
462
|
-
this.logger.
|
|
432
|
+
this.logger.debug(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
|
|
463
433
|
return pkg;
|
|
464
434
|
}
|
|
465
435
|
catch (error) {
|
|
@@ -468,21 +438,20 @@ class SecurityFilterPlugin {
|
|
|
468
438
|
if (errorStrategy === 'fail-closed') {
|
|
469
439
|
this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
|
|
470
440
|
this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
|
|
471
|
-
return
|
|
472
|
-
blocked: true,
|
|
473
|
-
reason: `Error during security processing (fail-closed mode): ${error.message}`,
|
|
474
|
-
plugin: {
|
|
475
|
-
name: 'verdaccio-security-filter',
|
|
476
|
-
version: '2.0.0',
|
|
477
|
-
},
|
|
478
|
-
blockedAt: new Date().toISOString(),
|
|
479
|
-
} });
|
|
441
|
+
return this._blockedPackage(packageInfo, `Error during security processing (fail-closed mode): ${error.message}`);
|
|
480
442
|
}
|
|
481
443
|
this.logger.warn(`[filter_metadata] Error handling: fail-open - ALLOWING ${packageName}`);
|
|
482
444
|
return packageInfo;
|
|
483
445
|
}
|
|
484
446
|
});
|
|
485
447
|
}
|
|
448
|
+
_versionBlockReason(packageName, version) {
|
|
449
|
+
if (this.securityRules.blockedVersions.includes(`${packageName}@${version}`)) {
|
|
450
|
+
return 'Exact version match in blocklist';
|
|
451
|
+
}
|
|
452
|
+
const rangeRule = this._getVersionRangeRule(packageName, version);
|
|
453
|
+
return rangeRule ? `Version matches blocked range: ${rangeRule.range}` : null;
|
|
454
|
+
}
|
|
486
455
|
_checkPackageBlock(packageName, version) {
|
|
487
456
|
return __awaiter(this, void 0, void 0, function* () {
|
|
488
457
|
if (this.config.mode === 'whitelist') {
|
|
@@ -497,15 +466,9 @@ class SecurityFilterPlugin {
|
|
|
497
466
|
if (!this._isScopeAllowed(packageName)) {
|
|
498
467
|
return { blocked: true, reason: 'Package scope not allowed' };
|
|
499
468
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return { blocked: true, reason: 'Exact version match in blocklist' };
|
|
504
|
-
}
|
|
505
|
-
const rangeRule = this._getVersionRangeRule(packageName, version);
|
|
506
|
-
if (rangeRule && rangeRule.strategy === 'block') {
|
|
507
|
-
return { blocked: true, reason: `Version matches blocked range: ${rangeRule.range}` };
|
|
508
|
-
}
|
|
469
|
+
const versionReason = version ? this._versionBlockReason(packageName, version) : null;
|
|
470
|
+
if (versionReason) {
|
|
471
|
+
return { blocked: true, reason: versionReason };
|
|
509
472
|
}
|
|
510
473
|
return { blocked: false, reason: '' };
|
|
511
474
|
});
|
|
@@ -516,11 +479,9 @@ class SecurityFilterPlugin {
|
|
|
516
479
|
rules.mode = 'whitelist';
|
|
517
480
|
rules.whitelisted = false;
|
|
518
481
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return regex.test(packageName);
|
|
523
|
-
});
|
|
482
|
+
const blockedPattern = this.blockedPatternRegexes.find(regex => regex.test(packageName));
|
|
483
|
+
if (blockedPattern) {
|
|
484
|
+
rules.blockedPattern = blockedPattern.source;
|
|
524
485
|
}
|
|
525
486
|
if (packageName.startsWith('@')) {
|
|
526
487
|
const scope = packageName.split('/')[0];
|
package/build/lib/metrics.d.ts
CHANGED
|
@@ -4,12 +4,12 @@ export declare class MetricsCollector {
|
|
|
4
4
|
private metricsBuffer;
|
|
5
5
|
private readonly bufferSize;
|
|
6
6
|
private readonly maxBufferSize;
|
|
7
|
+
private readonly flushIntervalMs;
|
|
8
|
+
private flushTimer;
|
|
7
9
|
constructor(config?: MetricsConfig);
|
|
8
10
|
record(event: MetricsData): void;
|
|
9
11
|
recordBlock(packageName: string, version: string, reason: string, metadata?: Record<string, any>): void;
|
|
10
|
-
recordFallback(packageName: string, fromVersion: string, toVersion: string, reason: string): void;
|
|
11
12
|
recordPublishRejected(packageName: string, version: string, reason: string): void;
|
|
12
|
-
recordCVE(packageName: string, version: string, cveId: string, severity: string): void;
|
|
13
13
|
recordLicenseBlock(packageName: string, version: string, license: string, reason: string): void;
|
|
14
14
|
recordPackageTooNew(packageName: string, version: string, reason: string, metadata?: Record<string, any>): void;
|
|
15
15
|
flush(): void;
|
package/build/lib/metrics.js
CHANGED
|
@@ -41,11 +41,17 @@ class MetricsCollector {
|
|
|
41
41
|
this.metricsBuffer = [];
|
|
42
42
|
this.bufferSize = 100;
|
|
43
43
|
this.maxBufferSize = 500;
|
|
44
|
+
this.flushIntervalMs = 10000;
|
|
45
|
+
this.flushTimer = null;
|
|
44
46
|
this.config = {
|
|
45
47
|
enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
|
|
46
48
|
output: (config === null || config === void 0 ? void 0 : config.output) || 'stdout',
|
|
47
49
|
filePath: (config === null || config === void 0 ? void 0 : config.filePath) || './security-metrics.json',
|
|
48
50
|
};
|
|
51
|
+
if (this.config.enabled) {
|
|
52
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushIntervalMs);
|
|
53
|
+
this.flushTimer.unref();
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
record(event) {
|
|
51
57
|
if (!this.config.enabled) {
|
|
@@ -69,16 +75,6 @@ class MetricsCollector {
|
|
|
69
75
|
metadata,
|
|
70
76
|
});
|
|
71
77
|
}
|
|
72
|
-
recordFallback(packageName, fromVersion, toVersion, reason) {
|
|
73
|
-
this.record({
|
|
74
|
-
timestamp: new Date().toISOString(),
|
|
75
|
-
event: 'fallback',
|
|
76
|
-
packageName,
|
|
77
|
-
version: fromVersion,
|
|
78
|
-
reason,
|
|
79
|
-
metadata: { toVersion },
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
78
|
recordPublishRejected(packageName, version, reason) {
|
|
83
79
|
this.record({
|
|
84
80
|
timestamp: new Date().toISOString(),
|
|
@@ -88,16 +84,6 @@ class MetricsCollector {
|
|
|
88
84
|
reason,
|
|
89
85
|
});
|
|
90
86
|
}
|
|
91
|
-
recordCVE(packageName, version, cveId, severity) {
|
|
92
|
-
this.record({
|
|
93
|
-
timestamp: new Date().toISOString(),
|
|
94
|
-
event: 'cve_detected',
|
|
95
|
-
packageName,
|
|
96
|
-
version,
|
|
97
|
-
reason: `CVE ${cveId} (${severity})`,
|
|
98
|
-
metadata: { cveId, severity },
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
87
|
recordLicenseBlock(packageName, version, license, reason) {
|
|
102
88
|
this.record({
|
|
103
89
|
timestamp: new Date().toISOString(),
|
|
@@ -163,6 +149,10 @@ class MetricsCollector {
|
|
|
163
149
|
return summary;
|
|
164
150
|
}
|
|
165
151
|
cleanup() {
|
|
152
|
+
if (this.flushTimer) {
|
|
153
|
+
clearInterval(this.flushTimer);
|
|
154
|
+
this.flushTimer = null;
|
|
155
|
+
}
|
|
166
156
|
this.flush();
|
|
167
157
|
}
|
|
168
158
|
}
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { WhitelistConfig } from '../types';
|
|
2
2
|
export declare class WhitelistChecker {
|
|
3
3
|
private config;
|
|
4
|
+
private regexes;
|
|
4
5
|
constructor(config?: WhitelistConfig);
|
|
5
6
|
isWhitelisted(packageName: string, version?: string): {
|
|
6
7
|
allowed: boolean;
|
|
7
8
|
reason?: string;
|
|
8
9
|
};
|
|
9
10
|
private matchesPattern;
|
|
11
|
+
private compile;
|
|
10
12
|
addPackage(packageName: string, versionRange?: string): void;
|
|
11
13
|
removePackage(packageName: string): void;
|
|
12
14
|
addPattern(pattern: string): void;
|
|
13
|
-
meetsAutoApproveCriteria(_packageName: string): Promise<{
|
|
14
|
-
approved: boolean;
|
|
15
|
-
reason?: string;
|
|
16
|
-
}>;
|
|
17
15
|
getSummary(): {
|
|
18
16
|
totalPackages: number;
|
|
19
17
|
totalPatterns: number;
|
|
20
|
-
hasAutoApprove: boolean;
|
|
21
18
|
};
|
|
22
19
|
}
|
|
@@ -32,25 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
36
|
exports.WhitelistChecker = void 0;
|
|
46
37
|
const semver = __importStar(require("semver"));
|
|
47
38
|
class WhitelistChecker {
|
|
48
39
|
constructor(config) {
|
|
40
|
+
this.regexes = new Map();
|
|
49
41
|
this.config = {
|
|
50
42
|
packages: (config === null || config === void 0 ? void 0 : config.packages) || [],
|
|
51
43
|
patterns: (config === null || config === void 0 ? void 0 : config.patterns) || [],
|
|
52
44
|
versions: (config === null || config === void 0 ? void 0 : config.versions) || {},
|
|
53
|
-
autoApprove: config === null || config === void 0 ? void 0 : config.autoApprove,
|
|
54
45
|
};
|
|
55
46
|
}
|
|
56
47
|
isWhitelisted(packageName, version) {
|
|
@@ -75,16 +66,21 @@ class WhitelistChecker {
|
|
|
75
66
|
};
|
|
76
67
|
}
|
|
77
68
|
matchesPattern(packageName) {
|
|
78
|
-
return this.config.patterns.some(pattern => {
|
|
69
|
+
return this.config.patterns.some(pattern => { var _a; return (_a = this.compile(pattern)) === null || _a === void 0 ? void 0 : _a.test(packageName); });
|
|
70
|
+
}
|
|
71
|
+
compile(pattern) {
|
|
72
|
+
let regex = this.regexes.get(pattern);
|
|
73
|
+
if (regex === undefined) {
|
|
79
74
|
try {
|
|
80
|
-
|
|
81
|
-
return regex.test(packageName);
|
|
75
|
+
regex = new RegExp(pattern);
|
|
82
76
|
}
|
|
83
77
|
catch (_a) {
|
|
84
78
|
console.error(`[Whitelist] Invalid pattern: ${pattern}`);
|
|
85
|
-
|
|
79
|
+
regex = null;
|
|
86
80
|
}
|
|
87
|
-
|
|
81
|
+
this.regexes.set(pattern, regex);
|
|
82
|
+
}
|
|
83
|
+
return regex;
|
|
88
84
|
}
|
|
89
85
|
addPackage(packageName, versionRange) {
|
|
90
86
|
if (!this.config.packages.includes(packageName)) {
|
|
@@ -109,22 +105,10 @@ class WhitelistChecker {
|
|
|
109
105
|
this.config.patterns.push(pattern);
|
|
110
106
|
}
|
|
111
107
|
}
|
|
112
|
-
meetsAutoApproveCriteria(_packageName) {
|
|
113
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
-
if (!this.config.autoApprove) {
|
|
115
|
-
return { approved: false, reason: 'Auto-approve not configured' };
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
approved: false,
|
|
119
|
-
reason: 'Auto-approve requires npm registry API integration (not implemented)',
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
108
|
getSummary() {
|
|
124
109
|
return {
|
|
125
110
|
totalPackages: this.config.packages.length,
|
|
126
111
|
totalPatterns: this.config.patterns.length,
|
|
127
|
-
hasAutoApprove: !!this.config.autoApprove,
|
|
128
112
|
};
|
|
129
113
|
}
|
|
130
114
|
}
|
package/build/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Config
|
|
1
|
+
import { Config } from '@verdaccio/types';
|
|
2
2
|
export interface VersionRangeRule {
|
|
3
3
|
package: string;
|
|
4
4
|
range: string;
|
|
@@ -8,7 +8,7 @@ export interface VersionRangeRule {
|
|
|
8
8
|
}
|
|
9
9
|
export interface CVECheckConfig {
|
|
10
10
|
enabled: boolean;
|
|
11
|
-
databases:
|
|
11
|
+
databases: 'osv'[];
|
|
12
12
|
severity: ('low' | 'medium' | 'high' | 'critical')[];
|
|
13
13
|
autoBlock: boolean;
|
|
14
14
|
updateInterval?: number;
|
|
@@ -18,11 +18,6 @@ export interface WhitelistConfig {
|
|
|
18
18
|
packages: string[];
|
|
19
19
|
patterns: string[];
|
|
20
20
|
versions?: Record<string, string>;
|
|
21
|
-
autoApprove?: {
|
|
22
|
-
minDownloads?: number;
|
|
23
|
-
minStars?: number;
|
|
24
|
-
verifiedPublisher?: boolean;
|
|
25
|
-
};
|
|
26
21
|
}
|
|
27
22
|
export interface LicenseConfig {
|
|
28
23
|
allowed: string[];
|
|
@@ -59,11 +54,8 @@ export interface AuthorFilterConfig {
|
|
|
59
54
|
export interface SecurityConfig extends Config {
|
|
60
55
|
blockedVersions?: string[];
|
|
61
56
|
blockedPatterns?: string[];
|
|
62
|
-
minPackageSize?: number;
|
|
63
|
-
maxPackageSize?: number;
|
|
64
57
|
allowedScopes?: string[];
|
|
65
58
|
blockedScopes?: string[];
|
|
66
|
-
enforceChecksum?: boolean;
|
|
67
59
|
versionRangeRules?: VersionRangeRule[];
|
|
68
60
|
mode?: 'blacklist' | 'whitelist';
|
|
69
61
|
whitelist?: WhitelistConfig;
|
|
@@ -76,32 +68,15 @@ export interface SecurityConfig extends Config {
|
|
|
76
68
|
errorHandling?: {
|
|
77
69
|
onFilterError?: 'fail-open' | 'fail-closed';
|
|
78
70
|
onCveCheckError?: 'fail-open' | 'fail-closed';
|
|
79
|
-
onLicenseCheckError?: 'fail-open' | 'fail-closed';
|
|
80
71
|
};
|
|
81
72
|
}
|
|
82
73
|
export interface SecurityRules {
|
|
83
74
|
blockedVersions: string[];
|
|
84
75
|
blockedPatterns: string[];
|
|
85
|
-
minPackageSize: number;
|
|
86
|
-
maxPackageSize: number;
|
|
87
76
|
allowedScopes: string[];
|
|
88
77
|
blockedScopes: string[];
|
|
89
|
-
enforceChecksum: boolean;
|
|
90
78
|
versionRangeRules: VersionRangeRule[];
|
|
91
79
|
}
|
|
92
|
-
export interface VersionData extends Version {
|
|
93
|
-
_fallback?: boolean;
|
|
94
|
-
_fallbackFrom?: string;
|
|
95
|
-
_fallbackReason?: string;
|
|
96
|
-
}
|
|
97
|
-
export interface FilterResult {
|
|
98
|
-
versions: Record<string, VersionData>;
|
|
99
|
-
blockedVersions: string[];
|
|
100
|
-
fallbacksApplied: Array<{
|
|
101
|
-
original: string;
|
|
102
|
-
fallback: string;
|
|
103
|
-
}>;
|
|
104
|
-
}
|
|
105
80
|
export interface CVEVulnerability {
|
|
106
81
|
id: string;
|
|
107
82
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verdaccio-security-filter",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.11",
|
|
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",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test": "jest",
|
|
29
29
|
"test:watch": "jest --watch",
|
|
30
30
|
"test:coverage": "jest --coverage",
|
|
31
|
+
"test:e2e": "npm run build && jest --testMatch \"**/*.e2e.ts\" --coverage=false",
|
|
31
32
|
"debug:verdaccio": "node --inspect-brk $(which verdaccio)",
|
|
32
33
|
"debug:build": "npm run build && npm link",
|
|
33
34
|
"prepack": "npm run build",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
},
|
|
56
57
|
"overrides": {
|
|
57
58
|
"qs": "6.16.0",
|
|
58
|
-
"@cypress/request": "
|
|
59
|
+
"@cypress/request": "3.0.10"
|
|
59
60
|
},
|
|
60
61
|
"author": {
|
|
61
62
|
"name": "Vitaliy Ponomarenko",
|