verdaccio-security-filter 0.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +719 -4
- package/build/index.d.ts +86 -0
- package/build/index.js +583 -0
- package/build/lib/cve-checker.d.ts +47 -0
- package/build/lib/cve-checker.js +286 -0
- package/build/lib/license-checker.d.ts +37 -0
- package/build/lib/license-checker.js +124 -0
- package/build/lib/logger.d.ts +42 -0
- package/build/lib/logger.js +95 -0
- package/build/lib/metrics.d.ts +54 -0
- package/build/lib/metrics.js +202 -0
- package/build/lib/package-age-checker.d.ts +49 -0
- package/build/lib/package-age-checker.js +138 -0
- package/build/lib/whitelist-checker.d.ts +48 -0
- package/build/lib/whitelist-checker.js +168 -0
- package/build/types.d.ts +119 -0
- package/build/types.js +2 -0
- package/package.json +61 -6
- package/index.js +0 -3
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { IPluginMiddleware, IPluginStorageFilter, PluginOptions, IBasicAuth, IStorageManager, Package } from '@verdaccio/types';
|
|
2
|
+
import { Application } from 'express';
|
|
3
|
+
import { SecurityConfig } from './types';
|
|
4
|
+
import { SecurityLogger } from './lib/logger';
|
|
5
|
+
/**
|
|
6
|
+
* Security Filter Plugin for Verdaccio
|
|
7
|
+
* Works with both middleware and filter interfaces for complete protection
|
|
8
|
+
* Compatible with Verdaccio 6.x and 7.x
|
|
9
|
+
*
|
|
10
|
+
* Two-layer protection:
|
|
11
|
+
* - Layer 1 (filter_metadata): CVE/License/Age checks when metadata is available
|
|
12
|
+
* - Layer 2 (middleware): Whitelist/Pattern/Scope checks + tarball blocking
|
|
13
|
+
*/
|
|
14
|
+
export default class SecurityFilterPlugin implements IPluginMiddleware<SecurityConfig>, IPluginStorageFilter<SecurityConfig> {
|
|
15
|
+
logger: SecurityLogger;
|
|
16
|
+
config: SecurityConfig;
|
|
17
|
+
private readonly securityRules;
|
|
18
|
+
private readonly metrics;
|
|
19
|
+
private readonly whitelistChecker;
|
|
20
|
+
private readonly cveChecker;
|
|
21
|
+
private readonly licenseChecker;
|
|
22
|
+
private readonly packageAgeChecker;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new SecurityFilterPlugin instance
|
|
25
|
+
* @param {SecurityConfig} config - Plugin configuration
|
|
26
|
+
* @param {PluginOptions} options - Verdaccio plugin options
|
|
27
|
+
*/
|
|
28
|
+
constructor(config: SecurityConfig, options: PluginOptions<SecurityConfig>);
|
|
29
|
+
/**
|
|
30
|
+
* Parse and validate version range rules
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
private _parseVersionRangeRules;
|
|
34
|
+
/**
|
|
35
|
+
* Log initialization details
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
private _logInitialization;
|
|
39
|
+
/**
|
|
40
|
+
* Get list of enabled features
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
private _getEnabledFeatures;
|
|
44
|
+
/**
|
|
45
|
+
* Check if package name matches blocked patterns
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
private _isBlockedByPattern;
|
|
49
|
+
/**
|
|
50
|
+
* Check if package scope is allowed
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
private _isScopeAllowed;
|
|
54
|
+
/**
|
|
55
|
+
* Get version range rule for a specific package and version
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
private _getVersionRangeRule;
|
|
59
|
+
/**
|
|
60
|
+
* Register Express middleware to intercept package requests
|
|
61
|
+
* Intercepts both metadata and tarball requests
|
|
62
|
+
*/
|
|
63
|
+
register_middlewares(app: Application, _auth: IBasicAuth<SecurityConfig>, _storage: IStorageManager<SecurityConfig>): void;
|
|
64
|
+
/**
|
|
65
|
+
* Filter package metadata before returning to client
|
|
66
|
+
* This provides CVE, License, and Age checking when metadata is available
|
|
67
|
+
*/
|
|
68
|
+
filter_metadata(packageInfo: Package): Promise<Package>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if package should be blocked (used by middleware)
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
private _checkPackageBlock;
|
|
74
|
+
/**
|
|
75
|
+
* Get applied security rules for a package
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
private _getAppliedRules;
|
|
79
|
+
}
|
|
80
|
+
export { SecurityLogger } from './lib/logger';
|
|
81
|
+
export { MetricsCollector } from './lib/metrics';
|
|
82
|
+
export { WhitelistChecker } from './lib/whitelist-checker';
|
|
83
|
+
export { CVEChecker } from './lib/cve-checker';
|
|
84
|
+
export { LicenseChecker } from './lib/license-checker';
|
|
85
|
+
export { PackageAgeChecker } from './lib/package-age-checker';
|
|
86
|
+
export * from './types';
|
package/build/index.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
36
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
|
+
};
|
|
38
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
39
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
40
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
41
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
42
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
43
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
44
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.PackageAgeChecker = exports.LicenseChecker = exports.CVEChecker = exports.WhitelistChecker = exports.MetricsCollector = exports.SecurityLogger = void 0;
|
|
49
|
+
const semver = __importStar(require("semver"));
|
|
50
|
+
const logger_1 = require("./lib/logger");
|
|
51
|
+
const metrics_1 = require("./lib/metrics");
|
|
52
|
+
const whitelist_checker_1 = require("./lib/whitelist-checker");
|
|
53
|
+
const cve_checker_1 = require("./lib/cve-checker");
|
|
54
|
+
const license_checker_1 = require("./lib/license-checker");
|
|
55
|
+
const package_age_checker_1 = require("./lib/package-age-checker");
|
|
56
|
+
/**
|
|
57
|
+
* Security Filter Plugin for Verdaccio
|
|
58
|
+
* Works with both middleware and filter interfaces for complete protection
|
|
59
|
+
* Compatible with Verdaccio 6.x and 7.x
|
|
60
|
+
*
|
|
61
|
+
* Two-layer protection:
|
|
62
|
+
* - Layer 1 (filter_metadata): CVE/License/Age checks when metadata is available
|
|
63
|
+
* - Layer 2 (middleware): Whitelist/Pattern/Scope checks + tarball blocking
|
|
64
|
+
*/
|
|
65
|
+
class SecurityFilterPlugin {
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new SecurityFilterPlugin instance
|
|
68
|
+
* @param {SecurityConfig} config - Plugin configuration
|
|
69
|
+
* @param {PluginOptions} options - Verdaccio plugin options
|
|
70
|
+
*/
|
|
71
|
+
constructor(config, options) {
|
|
72
|
+
this.config = config || {};
|
|
73
|
+
// Set default error handling strategy if not provided
|
|
74
|
+
if (!this.config.errorHandling) {
|
|
75
|
+
this.config.errorHandling = {
|
|
76
|
+
onFilterError: 'fail-open',
|
|
77
|
+
onCveCheckError: 'fail-open',
|
|
78
|
+
onLicenseCheckError: 'fail-open',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Initialize enhanced logger
|
|
82
|
+
this.logger = new logger_1.SecurityLogger(options.logger, this.config.logger);
|
|
83
|
+
// Initialize metrics collector
|
|
84
|
+
this.metrics = new metrics_1.MetricsCollector(this.config.metrics);
|
|
85
|
+
// Initialize whitelist checker
|
|
86
|
+
this.whitelistChecker = new whitelist_checker_1.WhitelistChecker(this.config.whitelist);
|
|
87
|
+
// Initialize security checkers
|
|
88
|
+
this.cveChecker = new cve_checker_1.CVEChecker(this.config.cveCheck);
|
|
89
|
+
this.licenseChecker = new license_checker_1.LicenseChecker(this.config.licenses);
|
|
90
|
+
this.packageAgeChecker = new package_age_checker_1.PackageAgeChecker(this.config.packageAge);
|
|
91
|
+
// Security rules configuration
|
|
92
|
+
this.securityRules = {
|
|
93
|
+
blockedVersions: this.config.blockedVersions || [],
|
|
94
|
+
blockedPatterns: this.config.blockedPatterns || [],
|
|
95
|
+
minPackageSize: this.config.minPackageSize || 0,
|
|
96
|
+
maxPackageSize: this.config.maxPackageSize || 100 * 1024 * 1024, // 100MB
|
|
97
|
+
allowedScopes: this.config.allowedScopes || [],
|
|
98
|
+
blockedScopes: this.config.blockedScopes || [],
|
|
99
|
+
enforceChecksum: this.config.enforceChecksum !== false,
|
|
100
|
+
versionRangeRules: this._parseVersionRangeRules(this.config.versionRangeRules || [])
|
|
101
|
+
};
|
|
102
|
+
this._logInitialization();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse and validate version range rules
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_parseVersionRangeRules(rules) {
|
|
109
|
+
return rules.map((rule, index) => {
|
|
110
|
+
if (!rule.package || !rule.range || !rule.strategy) {
|
|
111
|
+
this.logger.warn(`Invalid version range rule at index ${index}, skipping`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (rule.strategy === 'fallback' && !rule.fallbackVersion) {
|
|
115
|
+
this.logger.warn(`Fallback strategy requires fallbackVersion for ${rule.package}, skipping`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Validate semver range
|
|
119
|
+
try {
|
|
120
|
+
const validRange = semver.validRange(rule.range);
|
|
121
|
+
if (!validRange) {
|
|
122
|
+
this.logger.warn(`Invalid semver range "${rule.range}" for ${rule.package}, skipping`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.logger.warn(`Error parsing range "${rule.range}" for ${rule.package}: ${error.message}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return rule;
|
|
131
|
+
}).filter((rule) => rule !== null);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Log initialization details
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_logInitialization() {
|
|
138
|
+
const features = this._getEnabledFeatures();
|
|
139
|
+
this.logger.logInit(features);
|
|
140
|
+
this.logger.debug(`[Init] Config mode: ${this.config.mode || 'not set'}`);
|
|
141
|
+
this.logger.debug(`[Init] Blocked versions: ${this.securityRules.blockedVersions.length}`);
|
|
142
|
+
this.logger.debug(`[Init] Blocked patterns: ${this.securityRules.blockedPatterns.length}`);
|
|
143
|
+
if (this.securityRules.versionRangeRules.length > 0) {
|
|
144
|
+
this.logger.info('Version range rules configured:');
|
|
145
|
+
this.securityRules.versionRangeRules.forEach(rule => {
|
|
146
|
+
const fallback = rule.strategy === 'fallback' ? ` -> ${rule.fallbackVersion}` : '';
|
|
147
|
+
this.logger.debug(` ${rule.package} ${rule.range} [${rule.strategy}${fallback}]`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (this.config.mode === 'whitelist') {
|
|
151
|
+
const summary = this.whitelistChecker.getSummary();
|
|
152
|
+
this.logger.info(`Whitelist mode: ${summary.totalPackages} packages, ${summary.totalPatterns} patterns`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get list of enabled features
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
_getEnabledFeatures() {
|
|
160
|
+
var _a, _b, _c, _d;
|
|
161
|
+
const features = [];
|
|
162
|
+
if (this.config.mode === 'whitelist')
|
|
163
|
+
features.push('whitelist-mode');
|
|
164
|
+
if ((_a = this.config.metrics) === null || _a === void 0 ? void 0 : _a.enabled)
|
|
165
|
+
features.push('metrics');
|
|
166
|
+
if ((_b = this.config.cveCheck) === null || _b === void 0 ? void 0 : _b.enabled)
|
|
167
|
+
features.push('cve-checking');
|
|
168
|
+
if (((_c = this.config.licenses) === null || _c === void 0 ? void 0 : _c.allowed) && this.config.licenses.allowed.length > 0)
|
|
169
|
+
features.push('license-filtering');
|
|
170
|
+
if ((_d = this.config.packageAge) === null || _d === void 0 ? void 0 : _d.enabled)
|
|
171
|
+
features.push('package-age-verification');
|
|
172
|
+
if (this.securityRules.versionRangeRules.length > 0)
|
|
173
|
+
features.push('version-range-rules');
|
|
174
|
+
if (this.securityRules.blockedPatterns.length > 0)
|
|
175
|
+
features.push('pattern-blocking');
|
|
176
|
+
if (this.securityRules.blockedVersions.length > 0)
|
|
177
|
+
features.push('version-blocking');
|
|
178
|
+
if (this.securityRules.blockedScopes.length > 0)
|
|
179
|
+
features.push('scope-blocking');
|
|
180
|
+
return features;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if package name matches blocked patterns
|
|
184
|
+
* @private
|
|
185
|
+
*/
|
|
186
|
+
_isBlockedByPattern(packageName) {
|
|
187
|
+
return this.securityRules.blockedPatterns.some(pattern => {
|
|
188
|
+
const regex = new RegExp(pattern);
|
|
189
|
+
return regex.test(packageName);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if package scope is allowed
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_isScopeAllowed(packageName) {
|
|
197
|
+
// If not a scoped package, check if allowedScopes is empty
|
|
198
|
+
if (!packageName.startsWith('@')) {
|
|
199
|
+
return this.securityRules.allowedScopes.length === 0;
|
|
200
|
+
}
|
|
201
|
+
const scope = packageName.split('/')[0];
|
|
202
|
+
// Check blacklist
|
|
203
|
+
if (this.securityRules.blockedScopes.includes(scope)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
// Check whitelist (if exists)
|
|
207
|
+
if (this.securityRules.allowedScopes.length > 0) {
|
|
208
|
+
return this.securityRules.allowedScopes.includes(scope);
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get version range rule for a specific package and version
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
_getVersionRangeRule(packageName, version) {
|
|
217
|
+
return this.securityRules.versionRangeRules.find(rule => {
|
|
218
|
+
if (rule.package !== packageName) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
return semver.satisfies(version, rule.range);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
this.logger.warn(`Error checking version ${version} against range ${rule.range}: ${error.message}`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}) || null;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Register Express middleware to intercept package requests
|
|
232
|
+
* Intercepts both metadata and tarball requests
|
|
233
|
+
*/
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
235
|
+
register_middlewares(app, _auth, _storage) {
|
|
236
|
+
this.logger.info('[Middleware] Registering security filter middleware');
|
|
237
|
+
this.logger.info(`[Middleware] Config mode: ${this.config.mode || 'NOT SET'}`);
|
|
238
|
+
// Middleware to intercept all package requests
|
|
239
|
+
app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
|
240
|
+
try {
|
|
241
|
+
// Extract package name and version from URL
|
|
242
|
+
// Tarball URLs: /:package/-/:filename.tgz
|
|
243
|
+
// Scoped tarball URLs: /@scope/:package/-/:filename.tgz
|
|
244
|
+
// Metadata URLs: /:package or /@scope/:package
|
|
245
|
+
const urlMatch = req.url.match(/^\/(@[^/]+\/)?([^/]+)(?:\/-\/([^/]+\.tgz))?/);
|
|
246
|
+
if (!urlMatch) {
|
|
247
|
+
// Not a package-related request, pass through
|
|
248
|
+
this.logger.debug(`[Middleware] Non-package request: ${req.method} ${req.url}`);
|
|
249
|
+
return next();
|
|
250
|
+
}
|
|
251
|
+
const scope = urlMatch[1] || '';
|
|
252
|
+
const packageName = scope + urlMatch[2];
|
|
253
|
+
const tarballFile = urlMatch[3];
|
|
254
|
+
// Extract version from tarball filename if present
|
|
255
|
+
let version;
|
|
256
|
+
if (tarballFile) {
|
|
257
|
+
// This is a tarball download request - block it immediately
|
|
258
|
+
const versionMatch = tarballFile.match(/-([\d.]+(?:-[a-zA-Z0-9.]+)?)\.tgz$/);
|
|
259
|
+
version = versionMatch ? versionMatch[1] : undefined;
|
|
260
|
+
this.logger.info(`[Middleware] TARBALL REQUEST: ${packageName}@${version || 'unknown'}`);
|
|
261
|
+
// Apply all security checks for tarball downloads
|
|
262
|
+
const blockResult = yield this._checkPackageBlock(packageName, version);
|
|
263
|
+
if (blockResult.blocked) {
|
|
264
|
+
this.logger.warn(`[Middleware] [X] BLOCKED TARBALL: ${packageName}@${version || '*'} - ${blockResult.reason}`);
|
|
265
|
+
this.metrics.recordBlock(packageName, version || '*', blockResult.reason);
|
|
266
|
+
// Return 403 Forbidden with detailed error
|
|
267
|
+
res.status(403).json({
|
|
268
|
+
error: 'Package blocked by security filter',
|
|
269
|
+
package: packageName,
|
|
270
|
+
version: version || 'all versions',
|
|
271
|
+
reason: blockResult.reason,
|
|
272
|
+
timestamp: new Date().toISOString()
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.logger.info(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// This is a metadata request - intercept response and modify it
|
|
280
|
+
this.logger.info(`[Middleware] METADATA REQUEST: ${packageName} - will intercept response`);
|
|
281
|
+
// Check if package should be blocked
|
|
282
|
+
const blockResult = yield this._checkPackageBlock(packageName);
|
|
283
|
+
if (blockResult.blocked) {
|
|
284
|
+
// Intercept the response
|
|
285
|
+
const originalSend = res.send;
|
|
286
|
+
const originalJson = res.json;
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
288
|
+
const self = this;
|
|
289
|
+
// Override res.json to intercept JSON responses
|
|
290
|
+
res.json = function (body) {
|
|
291
|
+
if (body && typeof body === 'object' && body.name === packageName) {
|
|
292
|
+
// This is package metadata - modify it to show blocked info
|
|
293
|
+
self.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
|
|
294
|
+
self.logger.info(`[Middleware] Modifying response to show blocking info`);
|
|
295
|
+
const blockedResponse = {
|
|
296
|
+
name: packageName,
|
|
297
|
+
versions: {},
|
|
298
|
+
'dist-tags': {},
|
|
299
|
+
security: {
|
|
300
|
+
blocked: true,
|
|
301
|
+
reason: blockResult.reason,
|
|
302
|
+
plugin: {
|
|
303
|
+
name: 'verdaccio-security-filter',
|
|
304
|
+
version: '1.0.0',
|
|
305
|
+
mode: self.config.mode || 'blacklist',
|
|
306
|
+
},
|
|
307
|
+
blockedAt: new Date().toISOString(),
|
|
308
|
+
rules: self._getAppliedRules(packageName),
|
|
309
|
+
message: `This package has been blocked by security filter: ${blockResult.reason}`,
|
|
310
|
+
contact: 'Please contact your registry administrator for more information',
|
|
311
|
+
},
|
|
312
|
+
_security: {
|
|
313
|
+
blocked: true,
|
|
314
|
+
reason: blockResult.reason,
|
|
315
|
+
blockedBy: 'verdaccio-security-filter',
|
|
316
|
+
blockedAt: new Date().toISOString(),
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
return originalJson.call(this, blockedResponse);
|
|
320
|
+
}
|
|
321
|
+
return originalJson.call(this, body);
|
|
322
|
+
};
|
|
323
|
+
// Override res.send for non-JSON responses
|
|
324
|
+
res.send = function (body) {
|
|
325
|
+
if (typeof body === 'string') {
|
|
326
|
+
try {
|
|
327
|
+
const parsed = JSON.parse(body);
|
|
328
|
+
if (parsed && parsed.name === packageName) {
|
|
329
|
+
self.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
|
|
330
|
+
const blockedResponse = {
|
|
331
|
+
name: packageName,
|
|
332
|
+
versions: {},
|
|
333
|
+
'dist-tags': {},
|
|
334
|
+
security: {
|
|
335
|
+
blocked: true,
|
|
336
|
+
reason: blockResult.reason,
|
|
337
|
+
plugin: {
|
|
338
|
+
name: 'verdaccio-security-filter',
|
|
339
|
+
version: '1.0.0',
|
|
340
|
+
mode: self.config.mode || 'blacklist',
|
|
341
|
+
},
|
|
342
|
+
blockedAt: new Date().toISOString(),
|
|
343
|
+
rules: self._getAppliedRules(packageName),
|
|
344
|
+
message: `This package has been blocked by security filter: ${blockResult.reason}`,
|
|
345
|
+
contact: 'Please contact your registry administrator for more information',
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
return originalSend.call(this, JSON.stringify(blockedResponse));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (_a) {
|
|
352
|
+
// Not JSON, pass through
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return originalSend.call(this, body);
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
this.logger.info(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Continue to next middleware
|
|
363
|
+
next();
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
this.logger.error(`[Middleware] Error processing request: ${error.message}`);
|
|
367
|
+
next(error);
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
370
|
+
this.logger.info('[Middleware] Security filter middleware registered successfully');
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Filter package metadata before returning to client
|
|
374
|
+
* This provides CVE, License, and Age checking when metadata is available
|
|
375
|
+
*/
|
|
376
|
+
filter_metadata(packageInfo) {
|
|
377
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
378
|
+
var _a, _b, _c, _d, _e;
|
|
379
|
+
const packageName = packageInfo.name;
|
|
380
|
+
this.logger.info(`[filter_metadata] --> Processing: ${packageName}`);
|
|
381
|
+
try {
|
|
382
|
+
// 1. Whitelist/blacklist check (basic filtering)
|
|
383
|
+
const blockResult = yield this._checkPackageBlock(packageName);
|
|
384
|
+
if (blockResult.blocked) {
|
|
385
|
+
this.logger.warn(`[filter_metadata] BLOCKED: ${packageName}@* - ${blockResult.reason}`);
|
|
386
|
+
this.metrics.recordBlock(packageName, '*', blockResult.reason);
|
|
387
|
+
// Return empty versions with security field
|
|
388
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
389
|
+
blocked: true,
|
|
390
|
+
reason: blockResult.reason,
|
|
391
|
+
plugin: {
|
|
392
|
+
name: 'verdaccio-security-filter',
|
|
393
|
+
version: '2.0.0',
|
|
394
|
+
mode: this.config.mode || 'blacklist',
|
|
395
|
+
},
|
|
396
|
+
blockedAt: new Date().toISOString(),
|
|
397
|
+
rules: this._getAppliedRules(packageName),
|
|
398
|
+
} });
|
|
399
|
+
}
|
|
400
|
+
// 2. CVE Check - check all versions
|
|
401
|
+
if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
|
|
402
|
+
const versions = Object.keys(packageInfo.versions || {});
|
|
403
|
+
const vulnerableVersions = [];
|
|
404
|
+
for (const version of versions) {
|
|
405
|
+
const cveResult = yield this.cveChecker.checkPackage(packageName, version);
|
|
406
|
+
if (cveResult.isVulnerable) {
|
|
407
|
+
vulnerableVersions.push(version);
|
|
408
|
+
this.logger.warn(`[filter_metadata] CVE found in ${packageName}@${version}: ${cveResult.vulnerabilities.length} vulnerabilities`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// If configured to auto-block and vulnerabilities found
|
|
412
|
+
if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
|
|
413
|
+
const reason = `Package has ${vulnerableVersions.length} vulnerable version(s)`;
|
|
414
|
+
this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
|
|
415
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
416
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
417
|
+
blocked: true,
|
|
418
|
+
reason,
|
|
419
|
+
vulnerableVersions,
|
|
420
|
+
plugin: {
|
|
421
|
+
name: 'verdaccio-security-filter',
|
|
422
|
+
version: '2.0.0',
|
|
423
|
+
},
|
|
424
|
+
blockedAt: new Date().toISOString(),
|
|
425
|
+
} });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// 3. License Check - check latest version
|
|
429
|
+
if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
|
|
430
|
+
const latestVersion = (_b = packageInfo['dist-tags']) === null || _b === void 0 ? void 0 : _b.latest;
|
|
431
|
+
if (latestVersion && ((_c = packageInfo.versions) === null || _c === void 0 ? void 0 : _c[latestVersion])) {
|
|
432
|
+
const versionData = packageInfo.versions[latestVersion];
|
|
433
|
+
const licenseResult = this.licenseChecker.checkLicense(versionData);
|
|
434
|
+
if (!licenseResult.allowed) {
|
|
435
|
+
this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${licenseResult.reason}`);
|
|
436
|
+
this.metrics.recordBlock(packageName, '*', licenseResult.reason || 'License not allowed');
|
|
437
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
438
|
+
blocked: true,
|
|
439
|
+
reason: licenseResult.reason || 'License not allowed',
|
|
440
|
+
license: licenseResult.license,
|
|
441
|
+
plugin: {
|
|
442
|
+
name: 'verdaccio-security-filter',
|
|
443
|
+
version: '2.0.0',
|
|
444
|
+
},
|
|
445
|
+
blockedAt: new Date().toISOString(),
|
|
446
|
+
} });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// 4. Package Age Check
|
|
451
|
+
if ((_d = this.config.packageAge) === null || _d === void 0 ? void 0 : _d.enabled) {
|
|
452
|
+
const ageResult = this.packageAgeChecker.checkPackageAge(packageInfo);
|
|
453
|
+
if (!ageResult.allowed && !ageResult.warnOnly) {
|
|
454
|
+
this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${ageResult.reason}`);
|
|
455
|
+
this.metrics.recordBlock(packageName, '*', ageResult.reason || 'Package too new');
|
|
456
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
457
|
+
blocked: true,
|
|
458
|
+
reason: ageResult.reason || 'Package too new',
|
|
459
|
+
ageDays: ageResult.ageDays,
|
|
460
|
+
plugin: {
|
|
461
|
+
name: 'verdaccio-security-filter',
|
|
462
|
+
version: '2.0.0',
|
|
463
|
+
},
|
|
464
|
+
blockedAt: new Date().toISOString(),
|
|
465
|
+
} });
|
|
466
|
+
}
|
|
467
|
+
else if (!ageResult.allowed && ageResult.warnOnly) {
|
|
468
|
+
this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
this.logger.info(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
|
|
472
|
+
return packageInfo;
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
|
|
476
|
+
const errorStrategy = ((_e = this.config.errorHandling) === null || _e === void 0 ? void 0 : _e.onFilterError) || 'fail-open';
|
|
477
|
+
if (errorStrategy === 'fail-closed') {
|
|
478
|
+
this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
|
|
479
|
+
this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
|
|
480
|
+
return Object.assign(Object.assign({}, packageInfo), { versions: {}, 'dist-tags': {}, security: {
|
|
481
|
+
blocked: true,
|
|
482
|
+
reason: `Error during security processing (fail-closed mode): ${error.message}`,
|
|
483
|
+
plugin: {
|
|
484
|
+
name: 'verdaccio-security-filter',
|
|
485
|
+
version: '2.0.0',
|
|
486
|
+
},
|
|
487
|
+
blockedAt: new Date().toISOString(),
|
|
488
|
+
} });
|
|
489
|
+
}
|
|
490
|
+
this.logger.warn(`[filter_metadata] Error handling: fail-open - ALLOWING ${packageName}`);
|
|
491
|
+
return packageInfo;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Check if package should be blocked (used by middleware)
|
|
497
|
+
* @private
|
|
498
|
+
*/
|
|
499
|
+
_checkPackageBlock(packageName, version) {
|
|
500
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
501
|
+
// 1. Whitelist mode
|
|
502
|
+
if (this.config.mode === 'whitelist') {
|
|
503
|
+
const whitelistCheck = this.whitelistChecker.isWhitelisted(packageName, version);
|
|
504
|
+
if (!whitelistCheck.allowed) {
|
|
505
|
+
return { blocked: true, reason: whitelistCheck.reason || 'Package is not in whitelist' };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// 2. Blocked patterns
|
|
509
|
+
if (this._isBlockedByPattern(packageName)) {
|
|
510
|
+
return { blocked: true, reason: 'Package name matches blocked pattern' };
|
|
511
|
+
}
|
|
512
|
+
// 3. Scopes
|
|
513
|
+
if (!this._isScopeAllowed(packageName)) {
|
|
514
|
+
return { blocked: true, reason: 'Package scope not allowed' };
|
|
515
|
+
}
|
|
516
|
+
// 4. Exact version blocking
|
|
517
|
+
if (version) {
|
|
518
|
+
const versionKey = `${packageName}@${version}`;
|
|
519
|
+
if (this.securityRules.blockedVersions.includes(versionKey)) {
|
|
520
|
+
return { blocked: true, reason: 'Exact version match in blocklist' };
|
|
521
|
+
}
|
|
522
|
+
// 5. Version range rules
|
|
523
|
+
const rangeRule = this._getVersionRangeRule(packageName, version);
|
|
524
|
+
if (rangeRule && rangeRule.strategy === 'block') {
|
|
525
|
+
return { blocked: true, reason: `Version matches blocked range: ${rangeRule.range}` };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { blocked: false, reason: '' };
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get applied security rules for a package
|
|
533
|
+
* @private
|
|
534
|
+
*/
|
|
535
|
+
_getAppliedRules(packageName) {
|
|
536
|
+
const rules = {};
|
|
537
|
+
// Check which rules apply to this package
|
|
538
|
+
if (this.config.mode === 'whitelist') {
|
|
539
|
+
rules.mode = 'whitelist';
|
|
540
|
+
rules.whitelisted = false;
|
|
541
|
+
}
|
|
542
|
+
if (this._isBlockedByPattern(packageName)) {
|
|
543
|
+
rules.blockedPattern = this.securityRules.blockedPatterns.find(pattern => {
|
|
544
|
+
const regex = new RegExp(pattern);
|
|
545
|
+
return regex.test(packageName);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (packageName.startsWith('@')) {
|
|
549
|
+
const scope = packageName.split('/')[0];
|
|
550
|
+
if (this.securityRules.blockedScopes.includes(scope)) {
|
|
551
|
+
rules.blockedScope = scope;
|
|
552
|
+
}
|
|
553
|
+
if (this.securityRules.allowedScopes.length > 0 && !this.securityRules.allowedScopes.includes(scope)) {
|
|
554
|
+
rules.scopeNotInAllowedList = scope;
|
|
555
|
+
rules.allowedScopes = this.securityRules.allowedScopes;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const versionRules = this.securityRules.versionRangeRules.filter(rule => rule.package === packageName);
|
|
559
|
+
if (versionRules.length > 0) {
|
|
560
|
+
rules.versionRangeRules = versionRules.map(rule => ({
|
|
561
|
+
range: rule.range,
|
|
562
|
+
strategy: rule.strategy,
|
|
563
|
+
reason: rule.reason,
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
return rules;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
exports.default = SecurityFilterPlugin;
|
|
570
|
+
// Export utility classes for testing
|
|
571
|
+
var logger_2 = require("./lib/logger");
|
|
572
|
+
Object.defineProperty(exports, "SecurityLogger", { enumerable: true, get: function () { return logger_2.SecurityLogger; } });
|
|
573
|
+
var metrics_2 = require("./lib/metrics");
|
|
574
|
+
Object.defineProperty(exports, "MetricsCollector", { enumerable: true, get: function () { return metrics_2.MetricsCollector; } });
|
|
575
|
+
var whitelist_checker_2 = require("./lib/whitelist-checker");
|
|
576
|
+
Object.defineProperty(exports, "WhitelistChecker", { enumerable: true, get: function () { return whitelist_checker_2.WhitelistChecker; } });
|
|
577
|
+
var cve_checker_2 = require("./lib/cve-checker");
|
|
578
|
+
Object.defineProperty(exports, "CVEChecker", { enumerable: true, get: function () { return cve_checker_2.CVEChecker; } });
|
|
579
|
+
var license_checker_2 = require("./lib/license-checker");
|
|
580
|
+
Object.defineProperty(exports, "LicenseChecker", { enumerable: true, get: function () { return license_checker_2.LicenseChecker; } });
|
|
581
|
+
var package_age_checker_2 = require("./lib/package-age-checker");
|
|
582
|
+
Object.defineProperty(exports, "PackageAgeChecker", { enumerable: true, get: function () { return package_age_checker_2.PackageAgeChecker; } });
|
|
583
|
+
__exportStar(require("./types"), exports);
|