verdaccio-security-filter 1.3.8 → 1.3.10
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/build/index.d.ts +4 -0
- package/build/index.js +116 -144
- package/build/lib/cve-checker.d.ts +11 -4
- package/build/lib/cve-checker.js +202 -124
- package/build/lib/metrics.d.ts +2 -0
- package/build/lib/metrics.js +10 -0
- package/build/lib/whitelist-checker.d.ts +2 -0
- package/build/lib/whitelist-checker.js +11 -5
- package/package.json +1 -1
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 || {};
|
|
@@ -81,6 +84,15 @@ class SecurityFilterPlugin {
|
|
|
81
84
|
enforceChecksum: this.config.enforceChecksum !== false,
|
|
82
85
|
versionRangeRules: this._parseVersionRangeRules(this.config.versionRangeRules || [])
|
|
83
86
|
};
|
|
87
|
+
this.blockedPatternRegexes = this.securityRules.blockedPatterns.flatMap(pattern => {
|
|
88
|
+
try {
|
|
89
|
+
return [new RegExp(pattern)];
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.logger.warn(`Invalid blocked pattern "${pattern}", skipping: ${error.message}`);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
});
|
|
84
96
|
this._logInitialization();
|
|
85
97
|
}
|
|
86
98
|
_parseVersionRangeRules(rules) {
|
|
@@ -89,9 +101,8 @@ class SecurityFilterPlugin {
|
|
|
89
101
|
this.logger.warn(`Invalid version range rule at index ${index}, skipping`);
|
|
90
102
|
return null;
|
|
91
103
|
}
|
|
92
|
-
if (rule.strategy === 'fallback'
|
|
93
|
-
this.logger.warn(`
|
|
94
|
-
return null;
|
|
104
|
+
if (rule.strategy === 'fallback') {
|
|
105
|
+
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
106
|
}
|
|
96
107
|
try {
|
|
97
108
|
const validRange = semver.validRange(rule.range);
|
|
@@ -156,10 +167,7 @@ class SecurityFilterPlugin {
|
|
|
156
167
|
return features;
|
|
157
168
|
}
|
|
158
169
|
_isBlockedByPattern(packageName) {
|
|
159
|
-
return this.
|
|
160
|
-
const regex = new RegExp(pattern);
|
|
161
|
-
return regex.test(packageName);
|
|
162
|
-
});
|
|
170
|
+
return this.blockedPatternRegexes.some(regex => regex.test(packageName));
|
|
163
171
|
}
|
|
164
172
|
_isScopeAllowed(packageName) {
|
|
165
173
|
if (!packageName.startsWith('@')) {
|
|
@@ -212,9 +220,11 @@ class SecurityFilterPlugin {
|
|
|
212
220
|
const tarballFile = urlMatch[3];
|
|
213
221
|
let version;
|
|
214
222
|
if (tarballFile) {
|
|
215
|
-
const
|
|
216
|
-
version =
|
|
217
|
-
|
|
223
|
+
const filePrefix = `${urlMatch[2]}-`;
|
|
224
|
+
version = tarballFile.startsWith(filePrefix)
|
|
225
|
+
? tarballFile.slice(filePrefix.length, -'.tgz'.length)
|
|
226
|
+
: undefined;
|
|
227
|
+
this.logger.debug(`[Middleware] TARBALL REQUEST: ${packageName}@${version || 'unknown'}`);
|
|
218
228
|
const blockResult = yield this._checkPackageBlock(packageName, version);
|
|
219
229
|
if (blockResult.blocked) {
|
|
220
230
|
this.logger.warn(`[Middleware] [X] BLOCKED TARBALL: ${packageName}@${version || '*'} - ${blockResult.reason}`);
|
|
@@ -228,10 +238,10 @@ class SecurityFilterPlugin {
|
|
|
228
238
|
});
|
|
229
239
|
return;
|
|
230
240
|
}
|
|
231
|
-
this.logger.
|
|
241
|
+
this.logger.debug(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
|
|
232
242
|
}
|
|
233
243
|
else {
|
|
234
|
-
this.logger.
|
|
244
|
+
this.logger.debug(`[Middleware] METADATA REQUEST: ${packageName} - will intercept response`);
|
|
235
245
|
const blockResult = yield this._checkPackageBlock(packageName);
|
|
236
246
|
if (blockResult.blocked) {
|
|
237
247
|
this.logger.warn(`[Middleware] [X] BLOCKED METADATA: ${packageName} - ${blockResult.reason}`);
|
|
@@ -244,7 +254,7 @@ class SecurityFilterPlugin {
|
|
|
244
254
|
reason: blockResult.reason,
|
|
245
255
|
plugin: {
|
|
246
256
|
name: 'verdaccio-security-filter',
|
|
247
|
-
version:
|
|
257
|
+
version: PLUGIN_VERSION,
|
|
248
258
|
mode: this.config.mode || 'blacklist',
|
|
249
259
|
},
|
|
250
260
|
blockedAt: new Date().toISOString(),
|
|
@@ -263,7 +273,7 @@ class SecurityFilterPlugin {
|
|
|
263
273
|
return;
|
|
264
274
|
}
|
|
265
275
|
else {
|
|
266
|
-
this.logger.
|
|
276
|
+
this.logger.debug(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
next();
|
|
@@ -275,206 +285,176 @@ class SecurityFilterPlugin {
|
|
|
275
285
|
}));
|
|
276
286
|
this.logger.info('[Middleware] Security filter middleware registered successfully');
|
|
277
287
|
}
|
|
288
|
+
_blockedPackage(pkg, reason, extra = {}) {
|
|
289
|
+
return Object.assign(Object.assign({}, pkg), { versions: {}, 'dist-tags': {}, security: Object.assign(Object.assign({ blocked: true, reason }, extra), { plugin: {
|
|
290
|
+
name: 'verdaccio-security-filter',
|
|
291
|
+
version: PLUGIN_VERSION,
|
|
292
|
+
mode: this.config.mode || 'blacklist',
|
|
293
|
+
}, blockedAt: new Date().toISOString() }) });
|
|
294
|
+
}
|
|
295
|
+
_removeVersions(pkg, removed) {
|
|
296
|
+
if (removed.length === 0) {
|
|
297
|
+
return pkg;
|
|
298
|
+
}
|
|
299
|
+
const gone = new Set(removed);
|
|
300
|
+
const versions = Object.assign({}, pkg.versions);
|
|
301
|
+
for (const version of gone) {
|
|
302
|
+
delete versions[version];
|
|
303
|
+
}
|
|
304
|
+
const distTags = Object.assign({}, pkg['dist-tags']);
|
|
305
|
+
for (const [tag, tagVersion] of Object.entries(distTags)) {
|
|
306
|
+
if (gone.has(tagVersion)) {
|
|
307
|
+
delete distTags[tag];
|
|
308
|
+
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to filtered version ${tagVersion}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return Object.assign(Object.assign({}, pkg), { versions, 'dist-tags': distTags });
|
|
312
|
+
}
|
|
278
313
|
filter_metadata(packageInfo) {
|
|
279
314
|
return __awaiter(this, void 0, void 0, function* () {
|
|
280
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
315
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
281
316
|
let pkg = packageInfo;
|
|
282
317
|
const packageName = pkg.name;
|
|
283
|
-
this.logger.
|
|
318
|
+
this.logger.debug(`[filter_metadata] --> Processing: ${packageName}`);
|
|
284
319
|
try {
|
|
285
320
|
const blockResult = yield this._checkPackageBlock(packageName);
|
|
286
321
|
if (blockResult.blocked) {
|
|
287
322
|
this.logger.warn(`[filter_metadata] BLOCKED: ${packageName}@* - ${blockResult.reason}`);
|
|
288
323
|
this.metrics.recordBlock(packageName, '*', blockResult.reason);
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
rules: this._getAppliedRules(packageName),
|
|
299
|
-
} });
|
|
324
|
+
return this._blockedPackage(pkg, blockResult.reason, { rules: this._getAppliedRules(packageName) });
|
|
325
|
+
}
|
|
326
|
+
if (this.securityRules.blockedVersions.length > 0 || this.securityRules.versionRangeRules.length > 0) {
|
|
327
|
+
const ruleBlocked = Object.keys(pkg.versions || {})
|
|
328
|
+
.filter(version => this._versionBlockReason(packageName, version) !== null);
|
|
329
|
+
if (ruleBlocked.length > 0) {
|
|
330
|
+
this.logger.warn(`[filter_metadata] RULE FILTERED: ${packageName}: ${ruleBlocked.join(', ')}`);
|
|
331
|
+
pkg = this._removeVersions(pkg, ruleBlocked);
|
|
332
|
+
}
|
|
300
333
|
}
|
|
301
334
|
if ((_a = this.config.cveCheck) === null || _a === void 0 ? void 0 : _a.enabled) {
|
|
302
335
|
const versions = Object.keys(pkg.versions || {});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
});
|
|
336
|
+
let vulnerableVersions = [];
|
|
337
|
+
try {
|
|
338
|
+
const results = yield this.cveChecker.checkVersions(packageName, versions);
|
|
339
|
+
vulnerableVersions = results.filter(result => result.isVulnerable).map(result => result.version);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
this.logger.error(`[filter_metadata] CVE check failed for ${packageName}: ${error.message}`);
|
|
343
|
+
if (((_b = this.config.errorHandling) === null || _b === void 0 ? void 0 : _b.onCveCheckError) === 'fail-closed') {
|
|
344
|
+
const reason = `CVE check failed (fail-closed mode): ${error.message}`;
|
|
345
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
346
|
+
return this._blockedPackage(pkg, reason);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (vulnerableVersions.length > 0 && !this.config.cveCheck.autoBlock) {
|
|
350
|
+
this.logger.warn(`[filter_metadata] CVE found in ${packageName}: ${vulnerableVersions.length} vulnerable version(s), autoBlock is off`);
|
|
318
351
|
}
|
|
319
352
|
if (this.config.cveCheck.autoBlock && vulnerableVersions.length > 0) {
|
|
320
353
|
const reason = `Package has ${vulnerableVersions.length} vulnerable version(s)`;
|
|
321
354
|
if (vulnerableVersions.length === versions.length) {
|
|
322
355
|
this.logger.warn(`[filter_metadata] CVE BLOCKED: ${packageName} - ${reason}`);
|
|
323
356
|
this.metrics.recordBlock(packageName, '*', reason);
|
|
324
|
-
return
|
|
325
|
-
blocked: true,
|
|
326
|
-
reason,
|
|
327
|
-
vulnerableVersions,
|
|
328
|
-
plugin: {
|
|
329
|
-
name: 'verdaccio-security-filter',
|
|
330
|
-
version: '2.0.0',
|
|
331
|
-
},
|
|
332
|
-
blockedAt: new Date().toISOString(),
|
|
333
|
-
} });
|
|
357
|
+
return this._blockedPackage(pkg, reason, { vulnerableVersions });
|
|
334
358
|
}
|
|
335
359
|
this.logger.warn(`[filter_metadata] CVE FILTERED: ${packageName} - ${reason}: ${vulnerableVersions.join(', ')}`);
|
|
336
|
-
const filteredVersions = Object.assign({}, pkg.versions);
|
|
337
360
|
for (const version of vulnerableVersions) {
|
|
338
|
-
delete filteredVersions[version];
|
|
339
361
|
this.metrics.recordBlock(packageName, version, reason);
|
|
340
362
|
}
|
|
341
|
-
|
|
342
|
-
for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
|
|
343
|
-
if (vulnerableVersions.includes(tagVersion)) {
|
|
344
|
-
delete filteredDistTags[tag];
|
|
345
|
-
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to vulnerable version ${tagVersion}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
|
|
363
|
+
pkg = this._removeVersions(pkg, vulnerableVersions);
|
|
349
364
|
}
|
|
350
365
|
}
|
|
351
366
|
if (this.config.licenses && this.config.licenses.allowed && this.config.licenses.allowed.length > 0) {
|
|
352
|
-
const latestVersion = (
|
|
353
|
-
if (latestVersion && ((
|
|
367
|
+
const latestVersion = (_c = pkg['dist-tags']) === null || _c === void 0 ? void 0 : _c.latest;
|
|
368
|
+
if (latestVersion && ((_d = pkg.versions) === null || _d === void 0 ? void 0 : _d[latestVersion])) {
|
|
354
369
|
const versionData = pkg.versions[latestVersion];
|
|
355
370
|
const licenseResult = this.licenseChecker.checkLicense(versionData);
|
|
356
371
|
if (!licenseResult.allowed) {
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
reason: licenseResult.reason || 'License not allowed',
|
|
362
|
-
license: licenseResult.license,
|
|
363
|
-
plugin: {
|
|
364
|
-
name: 'verdaccio-security-filter',
|
|
365
|
-
version: '2.0.0',
|
|
366
|
-
},
|
|
367
|
-
blockedAt: new Date().toISOString(),
|
|
368
|
-
} });
|
|
372
|
+
const reason = licenseResult.reason || 'License not allowed';
|
|
373
|
+
this.logger.warn(`[filter_metadata] LICENSE BLOCKED: ${packageName} - ${reason}`);
|
|
374
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
375
|
+
return this._blockedPackage(pkg, reason, { license: licenseResult.license });
|
|
369
376
|
}
|
|
370
377
|
}
|
|
371
378
|
}
|
|
372
|
-
if ((
|
|
379
|
+
if ((_e = this.config.packageAge) === null || _e === void 0 ? void 0 : _e.enabled) {
|
|
373
380
|
const ageResult = this.packageAgeChecker.checkPackageAge(pkg);
|
|
374
381
|
if (!ageResult.allowed && !ageResult.warnOnly) {
|
|
375
|
-
|
|
376
|
-
this.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
reason: ageResult.reason || 'Package too new',
|
|
380
|
-
ageDays: ageResult.ageDays,
|
|
381
|
-
plugin: {
|
|
382
|
-
name: 'verdaccio-security-filter',
|
|
383
|
-
version: '2.0.0',
|
|
384
|
-
},
|
|
385
|
-
blockedAt: new Date().toISOString(),
|
|
386
|
-
} });
|
|
382
|
+
const reason = ageResult.reason || 'Package too new';
|
|
383
|
+
this.logger.warn(`[filter_metadata] AGE BLOCKED: ${packageName} - ${reason}`);
|
|
384
|
+
this.metrics.recordBlock(packageName, '*', reason);
|
|
385
|
+
return this._blockedPackage(pkg, reason, { ageDays: ageResult.ageDays });
|
|
387
386
|
}
|
|
388
387
|
else if (!ageResult.allowed && ageResult.warnOnly) {
|
|
389
388
|
this.logger.warn(`[filter_metadata] AGE WARNING: ${packageName} - ${ageResult.reason} (warn-only mode)`);
|
|
390
389
|
}
|
|
391
390
|
if (this.config.packageAge.minVersionAgeDays) {
|
|
392
|
-
const filteredVersions = Object.assign({}, pkg.versions);
|
|
393
391
|
const removedVersions = [];
|
|
394
|
-
for (const version of Object.keys(
|
|
392
|
+
for (const version of Object.keys(pkg.versions || {})) {
|
|
395
393
|
const versionAgeResult = this.packageAgeChecker.checkVersionAge(pkg, version);
|
|
396
394
|
if (!versionAgeResult.allowed && !versionAgeResult.warnOnly) {
|
|
397
395
|
this.logger.warn(`[filter_metadata] VERSION AGE FILTERED: ${packageName}@${version} - ${versionAgeResult.reason}`);
|
|
398
396
|
this.metrics.recordBlock(packageName, version, versionAgeResult.reason || 'Version too new');
|
|
399
|
-
delete filteredVersions[version];
|
|
400
397
|
removedVersions.push(version);
|
|
401
398
|
}
|
|
402
399
|
else if (!versionAgeResult.allowed && versionAgeResult.warnOnly) {
|
|
403
400
|
this.logger.warn(`[filter_metadata] VERSION AGE WARNING: ${packageName}@${version} - ${versionAgeResult.reason} (warn-only mode)`);
|
|
404
401
|
}
|
|
405
402
|
}
|
|
406
|
-
|
|
407
|
-
this.logger.info(`[filter_metadata] Removed ${removedVersions.length} too-new version(s) from ${packageName}: ${removedVersions.join(', ')}`);
|
|
408
|
-
const filteredDistTags = Object.assign({}, pkg['dist-tags']);
|
|
409
|
-
for (const [tag, tagVersion] of Object.entries(filteredDistTags)) {
|
|
410
|
-
if (removedVersions.includes(tagVersion)) {
|
|
411
|
-
delete filteredDistTags[tag];
|
|
412
|
-
this.logger.debug(`[filter_metadata] Removed dist-tag "${tag}" pointing to filtered version ${tagVersion}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
pkg = Object.assign(Object.assign({}, pkg), { versions: filteredVersions, 'dist-tags': filteredDistTags });
|
|
416
|
-
}
|
|
403
|
+
pkg = this._removeVersions(pkg, removedVersions);
|
|
417
404
|
}
|
|
418
405
|
}
|
|
419
|
-
if ((
|
|
420
|
-
const latestVersion = (
|
|
421
|
-
if (latestVersion && ((
|
|
406
|
+
if ((_f = this.config.authorFilter) === null || _f === void 0 ? void 0 : _f.enabled) {
|
|
407
|
+
const latestVersion = (_g = pkg['dist-tags']) === null || _g === void 0 ? void 0 : _g.latest;
|
|
408
|
+
if (latestVersion && ((_h = pkg.versions) === null || _h === void 0 ? void 0 : _h[latestVersion])) {
|
|
422
409
|
const versionData = pkg.versions[latestVersion];
|
|
423
410
|
const authorResult = this.authorChecker.checkAuthor(versionData);
|
|
424
411
|
if (!authorResult.allowed && !this.config.authorFilter.warnOnly) {
|
|
425
|
-
|
|
412
|
+
const reason = authorResult.reason || 'Author not allowed';
|
|
413
|
+
this.logger.warn(`[filter_metadata] AUTHOR BLOCKED: ${packageName} - ${reason}`);
|
|
426
414
|
this.metrics.record({
|
|
427
415
|
timestamp: new Date().toISOString(),
|
|
428
416
|
event: 'author_blocked',
|
|
429
417
|
packageName,
|
|
430
418
|
version: latestVersion,
|
|
431
|
-
reason
|
|
419
|
+
reason,
|
|
432
420
|
metadata: {
|
|
433
421
|
blockedBy: authorResult.blockedBy,
|
|
434
422
|
authorInfo: authorResult.authorInfo,
|
|
435
423
|
},
|
|
436
424
|
});
|
|
437
|
-
return
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
authorInfo: authorResult.authorInfo,
|
|
442
|
-
plugin: {
|
|
443
|
-
name: 'verdaccio-security-filter',
|
|
444
|
-
version: '2.0.0',
|
|
445
|
-
},
|
|
446
|
-
blockedAt: new Date().toISOString(),
|
|
447
|
-
} });
|
|
425
|
+
return this._blockedPackage(pkg, reason, {
|
|
426
|
+
blockedBy: authorResult.blockedBy,
|
|
427
|
+
authorInfo: authorResult.authorInfo,
|
|
428
|
+
});
|
|
448
429
|
}
|
|
449
430
|
else if (!authorResult.allowed && this.config.authorFilter.warnOnly) {
|
|
450
431
|
this.logger.warn(`[filter_metadata] AUTHOR WARNING: ${packageName} - ${authorResult.reason} (warn-only mode)`);
|
|
451
432
|
}
|
|
452
433
|
}
|
|
453
434
|
}
|
|
454
|
-
this.logger.
|
|
435
|
+
this.logger.debug(`[filter_metadata] [OK] ALLOWED: ${packageName}`);
|
|
455
436
|
return pkg;
|
|
456
437
|
}
|
|
457
438
|
catch (error) {
|
|
458
439
|
this.logger.error(`[filter_metadata] Error processing ${packageName}: ${error.message}`);
|
|
459
|
-
const errorStrategy = ((
|
|
440
|
+
const errorStrategy = ((_j = this.config.errorHandling) === null || _j === void 0 ? void 0 : _j.onFilterError) || 'fail-open';
|
|
460
441
|
if (errorStrategy === 'fail-closed') {
|
|
461
442
|
this.logger.warn(`[filter_metadata] Error handling: fail-closed - BLOCKING ${packageName}`);
|
|
462
443
|
this.metrics.recordBlock(packageName, '*', `Error during processing: ${error.message}`);
|
|
463
|
-
return
|
|
464
|
-
blocked: true,
|
|
465
|
-
reason: `Error during security processing (fail-closed mode): ${error.message}`,
|
|
466
|
-
plugin: {
|
|
467
|
-
name: 'verdaccio-security-filter',
|
|
468
|
-
version: '2.0.0',
|
|
469
|
-
},
|
|
470
|
-
blockedAt: new Date().toISOString(),
|
|
471
|
-
} });
|
|
444
|
+
return this._blockedPackage(packageInfo, `Error during security processing (fail-closed mode): ${error.message}`);
|
|
472
445
|
}
|
|
473
446
|
this.logger.warn(`[filter_metadata] Error handling: fail-open - ALLOWING ${packageName}`);
|
|
474
447
|
return packageInfo;
|
|
475
448
|
}
|
|
476
449
|
});
|
|
477
450
|
}
|
|
451
|
+
_versionBlockReason(packageName, version) {
|
|
452
|
+
if (this.securityRules.blockedVersions.includes(`${packageName}@${version}`)) {
|
|
453
|
+
return 'Exact version match in blocklist';
|
|
454
|
+
}
|
|
455
|
+
const rangeRule = this._getVersionRangeRule(packageName, version);
|
|
456
|
+
return rangeRule ? `Version matches blocked range: ${rangeRule.range}` : null;
|
|
457
|
+
}
|
|
478
458
|
_checkPackageBlock(packageName, version) {
|
|
479
459
|
return __awaiter(this, void 0, void 0, function* () {
|
|
480
460
|
if (this.config.mode === 'whitelist') {
|
|
@@ -489,15 +469,9 @@ class SecurityFilterPlugin {
|
|
|
489
469
|
if (!this._isScopeAllowed(packageName)) {
|
|
490
470
|
return { blocked: true, reason: 'Package scope not allowed' };
|
|
491
471
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return { blocked: true, reason: 'Exact version match in blocklist' };
|
|
496
|
-
}
|
|
497
|
-
const rangeRule = this._getVersionRangeRule(packageName, version);
|
|
498
|
-
if (rangeRule && rangeRule.strategy === 'block') {
|
|
499
|
-
return { blocked: true, reason: `Version matches blocked range: ${rangeRule.range}` };
|
|
500
|
-
}
|
|
472
|
+
const versionReason = version ? this._versionBlockReason(packageName, version) : null;
|
|
473
|
+
if (versionReason) {
|
|
474
|
+
return { blocked: true, reason: versionReason };
|
|
501
475
|
}
|
|
502
476
|
return { blocked: false, reason: '' };
|
|
503
477
|
});
|
|
@@ -508,11 +482,9 @@ class SecurityFilterPlugin {
|
|
|
508
482
|
rules.mode = 'whitelist';
|
|
509
483
|
rules.whitelisted = false;
|
|
510
484
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return regex.test(packageName);
|
|
515
|
-
});
|
|
485
|
+
const blockedPattern = this.blockedPatternRegexes.find(regex => regex.test(packageName));
|
|
486
|
+
if (blockedPattern) {
|
|
487
|
+
rules.blockedPattern = blockedPattern.source;
|
|
516
488
|
}
|
|
517
489
|
if (packageName.startsWith('@')) {
|
|
518
490
|
const scope = packageName.split('/')[0];
|
|
@@ -2,22 +2,29 @@ import { CVECheckConfig, CVECheckResult } from '../types';
|
|
|
2
2
|
export declare class CVEChecker {
|
|
3
3
|
private config;
|
|
4
4
|
private cache;
|
|
5
|
-
private
|
|
5
|
+
private failedUntil;
|
|
6
|
+
private inFlight;
|
|
7
|
+
private matches;
|
|
6
8
|
private readonly MAX_CACHE_SIZE;
|
|
7
9
|
private saveTimer;
|
|
8
10
|
private readonly SAVE_DEBOUNCE_MS;
|
|
11
|
+
private dirty;
|
|
9
12
|
constructor(config?: CVECheckConfig);
|
|
10
13
|
checkPackage(packageName: string, version: string): Promise<CVECheckResult>;
|
|
11
|
-
|
|
14
|
+
checkVersions(packageName: string, versions: string[]): Promise<CVECheckResult[]>;
|
|
15
|
+
private getPackageEntry;
|
|
16
|
+
private refresh;
|
|
12
17
|
private fetchFromOSV;
|
|
13
|
-
private
|
|
18
|
+
private queryOSV;
|
|
19
|
+
private toCachedVulnerability;
|
|
14
20
|
private mapOSVSeverity;
|
|
15
21
|
private mapDatabaseSpecificSeverity;
|
|
16
22
|
private scoreToSeverity;
|
|
17
23
|
private computeCVSSv3BaseScore;
|
|
18
|
-
private
|
|
24
|
+
private get cacheFile();
|
|
19
25
|
private loadCache;
|
|
20
26
|
private debouncedSaveCache;
|
|
27
|
+
private serializeCache;
|
|
21
28
|
private saveCache;
|
|
22
29
|
clearCache(): void;
|
|
23
30
|
destroy(): void;
|
package/build/lib/cve-checker.js
CHANGED
|
@@ -45,6 +45,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
45
45
|
exports.CVEChecker = void 0;
|
|
46
46
|
const fs = __importStar(require("fs"));
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
|
+
const semver = __importStar(require("semver"));
|
|
48
49
|
const lru_cache_1 = require("./lru-cache");
|
|
49
50
|
const CVSS_V3_WEIGHTS = {
|
|
50
51
|
AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
|
|
@@ -61,12 +62,56 @@ function roundUpCvssScore(value) {
|
|
|
61
62
|
}
|
|
62
63
|
return (Math.floor(intValue / 10000) + 1) / 10;
|
|
63
64
|
}
|
|
65
|
+
const OSV_QUERY_URL = 'https://api.osv.dev/v1/query';
|
|
66
|
+
const OSV_TIMEOUT_MS = 5000;
|
|
67
|
+
const OSV_ATTEMPTS = 2;
|
|
68
|
+
const OSV_RETRY_DELAY_MS = 500;
|
|
69
|
+
const FAILURE_TTL_MS = 60000;
|
|
70
|
+
function compareVersions(a, b) {
|
|
71
|
+
try {
|
|
72
|
+
return semver.compare(a, b);
|
|
73
|
+
}
|
|
74
|
+
catch (_a) {
|
|
75
|
+
return NaN;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isInRange(events, version) {
|
|
79
|
+
let affected = false;
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
if (event.introduced !== undefined) {
|
|
82
|
+
if (event.introduced === '0' || compareVersions(version, event.introduced) >= 0)
|
|
83
|
+
affected = true;
|
|
84
|
+
}
|
|
85
|
+
else if (event.fixed !== undefined) {
|
|
86
|
+
if (compareVersions(version, event.fixed) >= 0)
|
|
87
|
+
affected = false;
|
|
88
|
+
}
|
|
89
|
+
else if (event.last_affected !== undefined) {
|
|
90
|
+
if (compareVersions(version, event.last_affected) > 0)
|
|
91
|
+
affected = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return affected;
|
|
95
|
+
}
|
|
96
|
+
function isAffected(vuln, version, parsed) {
|
|
97
|
+
const informative = vuln.affected.filter(a => a.versions || a.ranges.length > 0);
|
|
98
|
+
if (informative.length === 0) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return informative.some(a => {
|
|
102
|
+
var _a;
|
|
103
|
+
return ((_a = a.versions) === null || _a === void 0 ? void 0 : _a.includes(version)) ||
|
|
104
|
+
(parsed !== null && a.ranges.some(events => isInRange(events, parsed)));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
64
107
|
class CVEChecker {
|
|
65
108
|
constructor(config) {
|
|
66
|
-
this.
|
|
109
|
+
this.inFlight = new Map();
|
|
110
|
+
this.matches = new WeakMap();
|
|
67
111
|
this.MAX_CACHE_SIZE = 1000;
|
|
68
112
|
this.saveTimer = null;
|
|
69
113
|
this.SAVE_DEBOUNCE_MS = 5000;
|
|
114
|
+
this.dirty = false;
|
|
70
115
|
this.config = {
|
|
71
116
|
enabled: (config === null || config === void 0 ? void 0 : config.enabled) || false,
|
|
72
117
|
databases: (config === null || config === void 0 ? void 0 : config.databases) || ['osv'],
|
|
@@ -76,131 +121,156 @@ class CVEChecker {
|
|
|
76
121
|
cacheDir: (config === null || config === void 0 ? void 0 : config.cacheDir) || './.security-cache',
|
|
77
122
|
};
|
|
78
123
|
this.cache = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
|
|
124
|
+
this.failedUntil = new lru_cache_1.LRUCache(this.MAX_CACHE_SIZE);
|
|
79
125
|
if (this.config.enabled) {
|
|
80
126
|
this.loadCache();
|
|
81
127
|
}
|
|
82
128
|
}
|
|
83
129
|
checkPackage(packageName, version) {
|
|
84
130
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
-
|
|
86
|
-
|
|
131
|
+
return (yield this.checkVersions(packageName, [version]))[0];
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
checkVersions(packageName, versions) {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
const checkedAt = new Date().toISOString();
|
|
137
|
+
if (!this.config.enabled || !this.config.databases.includes('osv')) {
|
|
138
|
+
return versions.map(version => ({
|
|
87
139
|
package: packageName,
|
|
88
140
|
version,
|
|
89
141
|
vulnerabilities: [],
|
|
90
142
|
isVulnerable: false,
|
|
91
|
-
checkedAt
|
|
92
|
-
};
|
|
143
|
+
checkedAt,
|
|
144
|
+
}));
|
|
93
145
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
146
|
+
const entry = yield this.getPackageEntry(packageName);
|
|
147
|
+
let memo = this.matches.get(entry);
|
|
148
|
+
if (!memo) {
|
|
149
|
+
memo = new Map();
|
|
150
|
+
this.matches.set(entry, memo);
|
|
97
151
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
152
|
+
const relevant = entry.vulns.filter(vuln => this.config.severity.includes(vuln.severity));
|
|
153
|
+
return versions.map(version => {
|
|
154
|
+
let vulnerabilities = memo.get(version);
|
|
155
|
+
if (!vulnerabilities) {
|
|
156
|
+
const parsed = semver.parse(version);
|
|
157
|
+
vulnerabilities = relevant.filter(vuln => isAffected(vuln, version, parsed));
|
|
158
|
+
memo.set(version, vulnerabilities);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
package: packageName,
|
|
162
|
+
version,
|
|
163
|
+
vulnerabilities,
|
|
164
|
+
isVulnerable: vulnerabilities.length > 0,
|
|
165
|
+
checkedAt,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
110
168
|
});
|
|
111
169
|
}
|
|
112
|
-
|
|
170
|
+
getPackageEntry(packageName) {
|
|
171
|
+
const cached = this.cache.get(packageName);
|
|
172
|
+
const ttlMs = this.config.updateInterval * 60 * 60 * 1000;
|
|
173
|
+
if (cached && Date.now() - cached.checkedAt <= ttlMs) {
|
|
174
|
+
return Promise.resolve(cached);
|
|
175
|
+
}
|
|
176
|
+
let pending = this.inFlight.get(packageName);
|
|
177
|
+
if (!pending) {
|
|
178
|
+
pending = this.refresh(packageName, cached).finally(() => this.inFlight.delete(packageName));
|
|
179
|
+
this.inFlight.set(packageName, pending);
|
|
180
|
+
}
|
|
181
|
+
return pending;
|
|
182
|
+
}
|
|
183
|
+
refresh(packageName, stale) {
|
|
113
184
|
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
var _a;
|
|
186
|
+
let failure;
|
|
187
|
+
if (((_a = this.failedUntil.get(packageName)) !== null && _a !== void 0 ? _a : 0) > Date.now()) {
|
|
188
|
+
failure = new Error('OSV lookup failed recently, not retrying yet');
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
116
191
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
break;
|
|
123
|
-
case 'github':
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
192
|
+
const entry = { checkedAt: Date.now(), vulns: yield this.fetchFromOSV(packageName) };
|
|
193
|
+
this.cache.set(packageName, entry);
|
|
194
|
+
this.failedUntil.delete(packageName);
|
|
195
|
+
this.debouncedSaveCache();
|
|
196
|
+
return entry;
|
|
126
197
|
}
|
|
127
198
|
catch (error) {
|
|
128
|
-
|
|
129
|
-
|
|
199
|
+
failure = error;
|
|
200
|
+
this.failedUntil.set(packageName, Date.now() + FAILURE_TTL_MS);
|
|
130
201
|
}
|
|
131
202
|
}
|
|
132
|
-
|
|
203
|
+
if (stale) {
|
|
204
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
205
|
+
console.warn(`[CVE Checker] Using stale data for ${packageName}: ${message}`);
|
|
206
|
+
return stale;
|
|
207
|
+
}
|
|
208
|
+
throw failure;
|
|
133
209
|
});
|
|
134
210
|
}
|
|
135
|
-
fetchFromOSV(packageName
|
|
211
|
+
fetchFromOSV(packageName) {
|
|
136
212
|
return __awaiter(this, void 0, void 0, function* () {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
213
|
+
const vulns = [];
|
|
214
|
+
let pageToken;
|
|
215
|
+
do {
|
|
216
|
+
const data = yield this.queryOSV({
|
|
217
|
+
package: { name: packageName, ecosystem: 'npm' },
|
|
218
|
+
page_token: pageToken,
|
|
219
|
+
});
|
|
220
|
+
for (const vuln of data.vulns || []) {
|
|
221
|
+
vulns.push(this.toCachedVulnerability(vuln, packageName));
|
|
222
|
+
}
|
|
223
|
+
pageToken = data.next_page_token;
|
|
224
|
+
} while (pageToken);
|
|
225
|
+
return vulns;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
queryOSV(body) {
|
|
229
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
230
|
+
for (let attempt = 1;; attempt++) {
|
|
140
231
|
try {
|
|
141
|
-
const
|
|
142
|
-
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
143
|
-
const response = yield fetch('https://api.osv.dev/v1/query', {
|
|
232
|
+
const response = yield fetch(OSV_QUERY_URL, {
|
|
144
233
|
method: 'POST',
|
|
145
|
-
headers: {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
body: JSON.stringify({
|
|
149
|
-
package: {
|
|
150
|
-
name: packageName,
|
|
151
|
-
ecosystem: 'npm',
|
|
152
|
-
},
|
|
153
|
-
version: version,
|
|
154
|
-
}),
|
|
155
|
-
signal: controller.signal,
|
|
234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
signal: AbortSignal.timeout(OSV_TIMEOUT_MS),
|
|
156
237
|
});
|
|
157
|
-
clearTimeout(timeoutId);
|
|
158
238
|
if (!response.ok) {
|
|
159
|
-
if (response.status === 429 && attempt < maxRetries) {
|
|
160
|
-
const waitTime = retryDelay * Math.pow(2, attempt - 1);
|
|
161
|
-
console.warn(`[CVE Checker] Rate limited, retrying in ${waitTime}ms (attempt ${attempt}/${maxRetries})`);
|
|
162
|
-
yield this.sleep(waitTime);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
239
|
throw new Error(`OSV API returned ${response.status}`);
|
|
166
240
|
}
|
|
167
|
-
|
|
168
|
-
if (!data.vulns || data.vulns.length === 0) {
|
|
169
|
-
return [];
|
|
170
|
-
}
|
|
171
|
-
return data.vulns.map((vuln) => {
|
|
172
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
173
|
-
return ({
|
|
174
|
-
id: vuln.id,
|
|
175
|
-
severity: this.mapOSVSeverity(vuln),
|
|
176
|
-
summary: vuln.summary || vuln.details || 'No description available',
|
|
177
|
-
affectedVersions: ((_a = vuln.affected) === null || _a === void 0 ? void 0 : _a.map((a) => { var _a; return (_a = a.ranges) === null || _a === void 0 ? void 0 : _a.map((r) => { var _a; return (_a = r.events) === null || _a === void 0 ? void 0 : _a.map((e) => e.introduced || e.fixed).filter(Boolean); }).flat(); }).flat()) || [],
|
|
178
|
-
fixedVersion: (_g = (_f = (_e = (_d = (_c = (_b = vuln.affected) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.ranges) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.events) === null || _f === void 0 ? void 0 : _f.find((e) => e.fixed)) === null || _g === void 0 ? void 0 : _g.fixed,
|
|
179
|
-
publishedDate: vuln.published || new Date().toISOString(),
|
|
180
|
-
source: 'osv',
|
|
181
|
-
});
|
|
182
|
-
});
|
|
241
|
+
return yield response.json();
|
|
183
242
|
}
|
|
184
243
|
catch (error) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
console.error(`[CVE Checker] OSV API error after ${maxRetries} attempts:`, message);
|
|
188
|
-
return [];
|
|
244
|
+
if (attempt >= OSV_ATTEMPTS) {
|
|
245
|
+
throw error;
|
|
189
246
|
}
|
|
190
|
-
|
|
191
|
-
console.warn(`[CVE Checker] Request timeout, retrying (attempt ${attempt}/${maxRetries})`);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
console.warn(`[CVE Checker] Request failed, retrying (attempt ${attempt}/${maxRetries}):`, message);
|
|
195
|
-
}
|
|
196
|
-
yield this.sleep(retryDelay * attempt);
|
|
247
|
+
yield new Promise(resolve => setTimeout(resolve, OSV_RETRY_DELAY_MS));
|
|
197
248
|
}
|
|
198
249
|
}
|
|
199
|
-
return [];
|
|
200
250
|
});
|
|
201
251
|
}
|
|
202
|
-
|
|
203
|
-
|
|
252
|
+
toCachedVulnerability(vuln, packageName) {
|
|
253
|
+
var _a;
|
|
254
|
+
const affected = (vuln.affected || [])
|
|
255
|
+
.filter(a => { var _a; return !((_a = a.package) === null || _a === void 0 ? void 0 : _a.name) || a.package.name === packageName; })
|
|
256
|
+
.map(a => {
|
|
257
|
+
var _a;
|
|
258
|
+
return ({
|
|
259
|
+
versions: ((_a = a.versions) === null || _a === void 0 ? void 0 : _a.length) ? a.versions : undefined,
|
|
260
|
+
ranges: (a.ranges || []).filter(r => r.type !== 'GIT').map(r => r.events || []),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
const events = affected.flatMap(a => a.ranges.flat());
|
|
264
|
+
return {
|
|
265
|
+
id: vuln.id || 'unknown',
|
|
266
|
+
severity: this.mapOSVSeverity(vuln),
|
|
267
|
+
summary: vuln.summary || 'No description available',
|
|
268
|
+
affectedVersions: events.map(e => e.introduced || e.fixed).filter((v) => !!v),
|
|
269
|
+
fixedVersion: (_a = events.find(e => e.fixed)) === null || _a === void 0 ? void 0 : _a.fixed,
|
|
270
|
+
publishedDate: vuln.published || new Date().toISOString(),
|
|
271
|
+
source: 'osv',
|
|
272
|
+
affected,
|
|
273
|
+
};
|
|
204
274
|
}
|
|
205
275
|
mapOSVSeverity(vuln) {
|
|
206
276
|
var _a, _b, _c, _d;
|
|
@@ -279,23 +349,21 @@ class CVEChecker {
|
|
|
279
349
|
? Math.ceil(cappedScore * 10) / 10
|
|
280
350
|
: roundUpCvssScore(cappedScore);
|
|
281
351
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const updateIntervalMs = this.config.updateInterval * 60 * 60 * 1000;
|
|
285
|
-
return now - this.lastUpdate > updateIntervalMs;
|
|
352
|
+
get cacheFile() {
|
|
353
|
+
return path.join(this.config.cacheDir, 'cve-cache.json');
|
|
286
354
|
}
|
|
287
355
|
loadCache() {
|
|
288
356
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
357
|
+
if (!fs.existsSync(this.cacheFile)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const parsed = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'));
|
|
361
|
+
for (const [key, value] of Object.entries((parsed === null || parsed === void 0 ? void 0 : parsed.cache) || {}).slice(-this.MAX_CACHE_SIZE)) {
|
|
362
|
+
const entry = value;
|
|
363
|
+
if (typeof (entry === null || entry === void 0 ? void 0 : entry.checkedAt) === 'number' && Array.isArray(entry.vulns)
|
|
364
|
+
&& entry.vulns.every(vuln => Array.isArray(vuln === null || vuln === void 0 ? void 0 : vuln.affected))) {
|
|
365
|
+
this.cache.set(key, entry);
|
|
366
|
+
}
|
|
299
367
|
}
|
|
300
368
|
}
|
|
301
369
|
catch (error) {
|
|
@@ -304,44 +372,54 @@ class CVEChecker {
|
|
|
304
372
|
}
|
|
305
373
|
}
|
|
306
374
|
debouncedSaveCache() {
|
|
375
|
+
this.dirty = true;
|
|
307
376
|
if (this.saveTimer) {
|
|
308
377
|
clearTimeout(this.saveTimer);
|
|
309
378
|
}
|
|
310
379
|
this.saveTimer = setTimeout(() => {
|
|
311
|
-
this.saveCache();
|
|
312
380
|
this.saveTimer = null;
|
|
381
|
+
void this.saveCache();
|
|
313
382
|
}, this.SAVE_DEBOUNCE_MS);
|
|
383
|
+
this.saveTimer.unref();
|
|
384
|
+
}
|
|
385
|
+
serializeCache() {
|
|
386
|
+
return JSON.stringify({ cache: Object.fromEntries(this.cache.entries()) });
|
|
314
387
|
}
|
|
315
388
|
saveCache() {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
fs.
|
|
389
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
390
|
+
try {
|
|
391
|
+
this.dirty = false;
|
|
392
|
+
const tmpFile = `${this.cacheFile}.${process.pid}.tmp`;
|
|
393
|
+
yield fs.promises.mkdir(path.dirname(this.cacheFile), { recursive: true });
|
|
394
|
+
yield fs.promises.writeFile(tmpFile, this.serializeCache(), 'utf8');
|
|
395
|
+
yield fs.promises.rename(tmpFile, this.cacheFile);
|
|
321
396
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
this.lastUpdate = Date.now();
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
331
|
-
console.error('[CVE Checker] Failed to save cache:', message);
|
|
332
|
-
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
399
|
+
console.error('[CVE Checker] Failed to save cache:', message);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
333
402
|
}
|
|
334
403
|
clearCache() {
|
|
335
404
|
this.cache.clear();
|
|
336
|
-
this.
|
|
337
|
-
this.
|
|
405
|
+
this.failedUntil.clear();
|
|
406
|
+
this.debouncedSaveCache();
|
|
338
407
|
}
|
|
339
408
|
destroy() {
|
|
340
409
|
if (this.saveTimer) {
|
|
341
410
|
clearTimeout(this.saveTimer);
|
|
342
411
|
this.saveTimer = null;
|
|
343
412
|
}
|
|
344
|
-
this.
|
|
413
|
+
if (this.dirty) {
|
|
414
|
+
try {
|
|
415
|
+
fs.mkdirSync(path.dirname(this.cacheFile), { recursive: true });
|
|
416
|
+
fs.writeFileSync(this.cacheFile, this.serializeCache(), 'utf8');
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
420
|
+
console.error('[CVE Checker] Failed to save cache:', message);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
345
423
|
this.cache.clear();
|
|
346
424
|
}
|
|
347
425
|
}
|
package/build/lib/metrics.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ 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;
|
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) {
|
|
@@ -163,6 +169,10 @@ class MetricsCollector {
|
|
|
163
169
|
return summary;
|
|
164
170
|
}
|
|
165
171
|
cleanup() {
|
|
172
|
+
if (this.flushTimer) {
|
|
173
|
+
clearInterval(this.flushTimer);
|
|
174
|
+
this.flushTimer = null;
|
|
175
|
+
}
|
|
166
176
|
this.flush();
|
|
167
177
|
}
|
|
168
178
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
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;
|
|
@@ -46,6 +46,7 @@ exports.WhitelistChecker = void 0;
|
|
|
46
46
|
const semver = __importStar(require("semver"));
|
|
47
47
|
class WhitelistChecker {
|
|
48
48
|
constructor(config) {
|
|
49
|
+
this.regexes = new Map();
|
|
49
50
|
this.config = {
|
|
50
51
|
packages: (config === null || config === void 0 ? void 0 : config.packages) || [],
|
|
51
52
|
patterns: (config === null || config === void 0 ? void 0 : config.patterns) || [],
|
|
@@ -75,16 +76,21 @@ class WhitelistChecker {
|
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
matchesPattern(packageName) {
|
|
78
|
-
return this.config.patterns.some(pattern => {
|
|
79
|
+
return this.config.patterns.some(pattern => { var _a; return (_a = this.compile(pattern)) === null || _a === void 0 ? void 0 : _a.test(packageName); });
|
|
80
|
+
}
|
|
81
|
+
compile(pattern) {
|
|
82
|
+
let regex = this.regexes.get(pattern);
|
|
83
|
+
if (regex === undefined) {
|
|
79
84
|
try {
|
|
80
|
-
|
|
81
|
-
return regex.test(packageName);
|
|
85
|
+
regex = new RegExp(pattern);
|
|
82
86
|
}
|
|
83
87
|
catch (_a) {
|
|
84
88
|
console.error(`[Whitelist] Invalid pattern: ${pattern}`);
|
|
85
|
-
|
|
89
|
+
regex = null;
|
|
86
90
|
}
|
|
87
|
-
|
|
91
|
+
this.regexes.set(pattern, regex);
|
|
92
|
+
}
|
|
93
|
+
return regex;
|
|
88
94
|
}
|
|
89
95
|
addPackage(packageName, versionRange) {
|
|
90
96
|
if (!this.config.packages.includes(packageName)) {
|
package/package.json
CHANGED