guardrail-security 1.0.1 → 2.0.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/dist/attack-surface/analyzer.d.ts.map +1 -1
- package/dist/attack-surface/analyzer.js +3 -2
- package/dist/license/engine.d.ts.map +1 -1
- package/dist/license/engine.js +3 -2
- package/dist/sbom/generator.d.ts +42 -0
- package/dist/sbom/generator.d.ts.map +1 -1
- package/dist/sbom/generator.js +168 -7
- package/dist/secrets/allowlist.d.ts +38 -0
- package/dist/secrets/allowlist.d.ts.map +1 -0
- package/dist/secrets/allowlist.js +131 -0
- package/dist/secrets/config-loader.d.ts +25 -0
- package/dist/secrets/config-loader.d.ts.map +1 -0
- package/dist/secrets/config-loader.js +103 -0
- package/dist/secrets/contextual-risk.d.ts +19 -0
- package/dist/secrets/contextual-risk.d.ts.map +1 -0
- package/dist/secrets/contextual-risk.js +88 -0
- package/dist/secrets/git-scanner.d.ts +29 -0
- package/dist/secrets/git-scanner.d.ts.map +1 -0
- package/dist/secrets/git-scanner.js +109 -0
- package/dist/secrets/guardian.d.ts +70 -57
- package/dist/secrets/guardian.d.ts.map +1 -1
- package/dist/secrets/guardian.js +532 -240
- package/dist/secrets/index.d.ts +4 -0
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +11 -1
- package/dist/secrets/patterns.d.ts +39 -10
- package/dist/secrets/patterns.d.ts.map +1 -1
- package/dist/secrets/patterns.js +129 -71
- package/dist/secrets/pre-commit.d.ts.map +1 -1
- package/dist/secrets/pre-commit.js +1 -1
- package/dist/secrets/vault-integration.d.ts.map +1 -1
- package/dist/secrets/vault-integration.js +1 -0
- package/dist/supply-chain/detector.d.ts.map +1 -1
- package/dist/supply-chain/detector.js +4 -3
- package/dist/supply-chain/vulnerability-db.d.ts +89 -16
- package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
- package/dist/supply-chain/vulnerability-db.js +404 -115
- package/dist/utils/semver.d.ts +37 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +109 -0
- package/package.json +17 -4
- package/src/__tests__/license/engine.test.ts +0 -250
- package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
- package/src/attack-surface/analyzer.ts +0 -152
- package/src/attack-surface/index.ts +0 -5
- package/src/index.ts +0 -21
- package/src/languages/index.ts +0 -91
- package/src/languages/java-analyzer.ts +0 -490
- package/src/languages/python-analyzer.ts +0 -498
- package/src/license/compatibility-matrix.ts +0 -366
- package/src/license/engine.ts +0 -345
- package/src/license/index.ts +0 -6
- package/src/sbom/generator.ts +0 -355
- package/src/sbom/index.ts +0 -5
- package/src/secrets/guardian.ts +0 -448
- package/src/secrets/index.ts +0 -10
- package/src/secrets/patterns.ts +0 -186
- package/src/secrets/pre-commit.ts +0 -158
- package/src/secrets/vault-integration.ts +0 -360
- package/src/secrets/vault-providers.ts +0 -446
- package/src/supply-chain/detector.ts +0 -252
- package/src/supply-chain/index.ts +0 -11
- package/src/supply-chain/malicious-db.ts +0 -103
- package/src/supply-chain/script-analyzer.ts +0 -194
- package/src/supply-chain/typosquat.ts +0 -302
- package/src/supply-chain/vulnerability-db.ts +0 -386
|
@@ -2,169 +2,378 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Vulnerability Database Integration
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
5
|
+
* Real-time OSV (Open Source Vulnerabilities) integration with:
|
|
6
|
+
* - Multi-ecosystem support (npm, PyPI, RubyGems, Go)
|
|
7
|
+
* - Persistent caching with 24h TTL
|
|
8
|
+
* - Batch request optimization
|
|
9
|
+
* - CVSS scoring and vectors
|
|
10
|
+
* - Remediation path analysis
|
|
11
|
+
* - Optional NVD enrichment for CVE details
|
|
12
|
+
* - Retry logic with exponential backoff
|
|
13
|
+
* - Configurable timeouts
|
|
9
14
|
*/
|
|
10
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
16
|
exports.vulnerabilityDatabase = exports.VulnerabilityDatabase = void 0;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const semver_1 = require("../utils/semver");
|
|
20
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
21
|
+
const CACHE_VERSION = '1.1';
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
23
|
+
const DEFAULT_RETRIES = 3;
|
|
24
|
+
const NVD_API_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
|
|
14
25
|
class VulnerabilityDatabase {
|
|
15
26
|
osvApiUrl = 'https://api.osv.dev/v1';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
cacheDir;
|
|
28
|
+
cachePath;
|
|
29
|
+
memoryCache = new Map();
|
|
30
|
+
cacheHits = 0;
|
|
31
|
+
cacheMisses = 0;
|
|
32
|
+
options;
|
|
33
|
+
constructor(cacheDirOrOptions) {
|
|
34
|
+
if (typeof cacheDirOrOptions === 'string') {
|
|
35
|
+
this.options = { cacheDir: cacheDirOrOptions };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.options = cacheDirOrOptions || {};
|
|
39
|
+
}
|
|
40
|
+
this.cacheDir = this.options.cacheDir || (0, path_1.join)(process.cwd(), '.guardrail', 'cache');
|
|
41
|
+
this.cachePath = (0, path_1.join)(this.cacheDir, 'osv.json');
|
|
42
|
+
if (!this.options.noCache) {
|
|
43
|
+
this.loadDiskCache();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Update options at runtime
|
|
48
|
+
*/
|
|
49
|
+
setOptions(options) {
|
|
50
|
+
this.options = { ...this.options, ...options };
|
|
51
|
+
if (options.noCache) {
|
|
52
|
+
this.memoryCache.clear();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Load cache from disk
|
|
57
|
+
*/
|
|
58
|
+
loadDiskCache() {
|
|
59
|
+
try {
|
|
60
|
+
if ((0, fs_1.existsSync)(this.cachePath)) {
|
|
61
|
+
const diskCache = JSON.parse((0, fs_1.readFileSync)(this.cachePath, 'utf-8'));
|
|
62
|
+
if (diskCache.version === CACHE_VERSION) {
|
|
63
|
+
for (const [key, entry] of Object.entries(diskCache.entries)) {
|
|
64
|
+
if (Date.now() - entry.fetchedAt < CACHE_TTL_MS) {
|
|
65
|
+
this.memoryCache.set(key, entry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// Cache load failed, start fresh
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Save cache to disk
|
|
77
|
+
*/
|
|
78
|
+
saveDiskCache() {
|
|
79
|
+
try {
|
|
80
|
+
if (!(0, fs_1.existsSync)(this.cacheDir)) {
|
|
81
|
+
(0, fs_1.mkdirSync)(this.cacheDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
const diskCache = {
|
|
84
|
+
version: CACHE_VERSION,
|
|
85
|
+
entries: Object.fromEntries(this.memoryCache),
|
|
86
|
+
};
|
|
87
|
+
(0, fs_1.writeFileSync)(this.cachePath, JSON.stringify(diskCache, null, 2));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Cache save failed, continue without caching
|
|
91
|
+
}
|
|
92
|
+
}
|
|
19
93
|
/**
|
|
20
94
|
* Check a single package for vulnerabilities
|
|
21
95
|
*/
|
|
22
|
-
async checkPackage(name, version) {
|
|
23
|
-
const cacheKey = `${name}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
vulnerabilities.
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
96
|
+
async checkPackage(name, version, ecosystem = 'npm', isDirect = true) {
|
|
97
|
+
const cacheKey = `${ecosystem}:${name}:${version}`;
|
|
98
|
+
if (!this.options.noCache) {
|
|
99
|
+
const cached = this.memoryCache.get(cacheKey);
|
|
100
|
+
if (cached && (Date.now() - cached.fetchedAt) < CACHE_TTL_MS) {
|
|
101
|
+
this.cacheHits++;
|
|
102
|
+
return this.buildResult(name, version, cached.data, ecosystem, isDirect);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.cacheMisses++;
|
|
106
|
+
let vulnerabilities = await this.queryOSVWithRetry(name, version, ecosystem);
|
|
107
|
+
// Optional NVD enrichment
|
|
108
|
+
if (this.options.nvdEnrichment && vulnerabilities.length > 0) {
|
|
109
|
+
vulnerabilities = await this.enrichWithNVD(vulnerabilities);
|
|
110
|
+
}
|
|
111
|
+
// Cache the result (unless noCache is set)
|
|
112
|
+
if (!this.options.noCache) {
|
|
113
|
+
this.memoryCache.set(cacheKey, {
|
|
114
|
+
data: vulnerabilities,
|
|
115
|
+
fetchedAt: Date.now(),
|
|
116
|
+
ecosystem,
|
|
117
|
+
});
|
|
118
|
+
// Persist to disk periodically
|
|
119
|
+
if ((this.cacheHits + this.cacheMisses) % 10 === 0) {
|
|
120
|
+
this.saveDiskCache();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return this.buildResult(name, version, vulnerabilities, ecosystem, isDirect);
|
|
48
124
|
}
|
|
49
125
|
/**
|
|
50
|
-
* Check multiple packages in bulk
|
|
126
|
+
* Check multiple packages in bulk with batching
|
|
51
127
|
*/
|
|
52
128
|
async checkPackages(packages) {
|
|
53
|
-
const batchSize =
|
|
129
|
+
const batchSize = 50;
|
|
54
130
|
const results = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
131
|
+
// Group by ecosystem for efficient batching
|
|
132
|
+
const byEcosystem = new Map();
|
|
133
|
+
for (const pkg of packages) {
|
|
134
|
+
const eco = pkg.ecosystem || 'npm';
|
|
135
|
+
if (!byEcosystem.has(eco)) {
|
|
136
|
+
byEcosystem.set(eco, []);
|
|
137
|
+
}
|
|
138
|
+
byEcosystem.get(eco).push(pkg);
|
|
59
139
|
}
|
|
140
|
+
// Process each ecosystem in batches
|
|
141
|
+
for (const [ecosystem, pkgs] of byEcosystem) {
|
|
142
|
+
for (let i = 0; i < pkgs.length; i += batchSize) {
|
|
143
|
+
const batch = pkgs.slice(i, i + batchSize);
|
|
144
|
+
const batchResults = await Promise.all(batch.map(pkg => this.checkPackage(pkg.name, pkg.version, ecosystem, pkg.isDirect ?? true)));
|
|
145
|
+
results.push(...batchResults);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Final cache save
|
|
149
|
+
this.saveDiskCache();
|
|
60
150
|
return results;
|
|
61
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Query OSV with retry logic and exponential backoff
|
|
154
|
+
*/
|
|
155
|
+
async queryOSVWithRetry(packageName, version, ecosystem) {
|
|
156
|
+
const maxRetries = this.options.retries ?? DEFAULT_RETRIES;
|
|
157
|
+
const timeout = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
158
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
159
|
+
try {
|
|
160
|
+
return await this.queryOSV(packageName, version, ecosystem, timeout);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Exponential backoff: 100ms, 200ms, 400ms...
|
|
164
|
+
if (attempt < maxRetries - 1) {
|
|
165
|
+
await this.delay(100 * Math.pow(2, attempt));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// All retries failed
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
62
172
|
/**
|
|
63
173
|
* Query OSV (Open Source Vulnerabilities) API
|
|
64
174
|
*/
|
|
65
|
-
async queryOSV(packageName, version) {
|
|
175
|
+
async queryOSV(packageName, version, ecosystem, timeout) {
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
66
178
|
try {
|
|
67
179
|
const response = await fetch(`${this.osvApiUrl}/query`, {
|
|
68
180
|
method: 'POST',
|
|
69
181
|
headers: {
|
|
70
182
|
'Content-Type': 'application/json',
|
|
183
|
+
'User-Agent': 'guardrail-cli/1.0',
|
|
71
184
|
},
|
|
72
185
|
body: JSON.stringify({
|
|
73
186
|
package: {
|
|
74
187
|
name: packageName,
|
|
75
|
-
ecosystem:
|
|
188
|
+
ecosystem: ecosystem,
|
|
76
189
|
},
|
|
77
190
|
version: version,
|
|
78
191
|
}),
|
|
79
|
-
signal:
|
|
192
|
+
signal: controller.signal,
|
|
80
193
|
});
|
|
194
|
+
clearTimeout(timeoutId);
|
|
81
195
|
if (!response.ok) {
|
|
82
|
-
|
|
196
|
+
throw new Error(`OSV API returned ${response.status}`);
|
|
83
197
|
}
|
|
84
198
|
const data = await response.json();
|
|
85
|
-
return this.parseOSVResponse(data);
|
|
199
|
+
return this.parseOSVResponse(data, version);
|
|
86
200
|
}
|
|
87
201
|
catch (error) {
|
|
88
|
-
|
|
89
|
-
|
|
202
|
+
clearTimeout(timeoutId);
|
|
203
|
+
throw error;
|
|
90
204
|
}
|
|
91
205
|
}
|
|
92
206
|
/**
|
|
93
|
-
*
|
|
207
|
+
* Enrich vulnerabilities with NVD data (CVSS scores)
|
|
94
208
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
209
|
+
async enrichWithNVD(vulnerabilities) {
|
|
210
|
+
const enriched = [];
|
|
211
|
+
for (const vuln of vulnerabilities) {
|
|
212
|
+
// Find CVE alias
|
|
213
|
+
const cveId = vuln.aliases?.find(a => a.startsWith('CVE-')) ||
|
|
214
|
+
(vuln.id.startsWith('CVE-') ? vuln.id : null);
|
|
215
|
+
if (cveId && (!vuln.cvssScore || !vuln.cvssVector)) {
|
|
216
|
+
try {
|
|
217
|
+
const nvdData = await this.queryNVD(cveId);
|
|
218
|
+
if (nvdData) {
|
|
219
|
+
enriched.push({
|
|
220
|
+
...vuln,
|
|
221
|
+
cvssScore: nvdData.cvssScore ?? vuln.cvssScore,
|
|
222
|
+
cvssVector: nvdData.cvssVector ?? vuln.cvssVector,
|
|
223
|
+
severity: nvdData.severity ?? vuln.severity,
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// NVD enrichment failed, continue with original data
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
enriched.push(vuln);
|
|
98
233
|
}
|
|
99
|
-
return
|
|
100
|
-
id: vuln.id,
|
|
101
|
-
source: 'osv',
|
|
102
|
-
severity: this.mapOSVSeverity(vuln.severity),
|
|
103
|
-
cvssScore: vuln.severity?.[0]?.score,
|
|
104
|
-
title: vuln.summary || vuln.id,
|
|
105
|
-
description: vuln.details || '',
|
|
106
|
-
affectedVersions: this.extractAffectedVersions(vuln.affected),
|
|
107
|
-
patchedVersions: this.extractPatchedVersions(vuln.affected),
|
|
108
|
-
references: (vuln.references || []).map((r) => r.url),
|
|
109
|
-
publishedAt: new Date(vuln.published || Date.now()),
|
|
110
|
-
updatedAt: new Date(vuln.modified || Date.now()),
|
|
111
|
-
cwe: vuln.database_specific?.cwe_ids || [],
|
|
112
|
-
}));
|
|
234
|
+
return enriched;
|
|
113
235
|
}
|
|
114
236
|
/**
|
|
115
|
-
* Query
|
|
237
|
+
* Query NVD API for CVE details
|
|
116
238
|
*/
|
|
117
|
-
async
|
|
239
|
+
async queryNVD(cveId) {
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // Shorter timeout for enrichment
|
|
118
242
|
try {
|
|
119
|
-
const response = await fetch(
|
|
120
|
-
method: '
|
|
243
|
+
const response = await fetch(`${NVD_API_URL}?cveId=${encodeURIComponent(cveId)}`, {
|
|
244
|
+
method: 'GET',
|
|
121
245
|
headers: {
|
|
122
|
-
'
|
|
246
|
+
'User-Agent': 'guardrail-cli/1.0',
|
|
123
247
|
},
|
|
124
|
-
|
|
125
|
-
[packageName]: [version],
|
|
126
|
-
}),
|
|
127
|
-
signal: AbortSignal.timeout(10000),
|
|
248
|
+
signal: controller.signal,
|
|
128
249
|
});
|
|
250
|
+
clearTimeout(timeoutId);
|
|
129
251
|
if (!response.ok) {
|
|
130
|
-
return
|
|
252
|
+
return null;
|
|
131
253
|
}
|
|
132
254
|
const data = await response.json();
|
|
133
|
-
|
|
255
|
+
const vulns = data['vulnerabilities'];
|
|
256
|
+
const cveWrapper = vulns?.[0];
|
|
257
|
+
const cve = cveWrapper?.['cve'];
|
|
258
|
+
if (!cve)
|
|
259
|
+
return null;
|
|
260
|
+
// Extract CVSS v3.1 or v3.0 metrics
|
|
261
|
+
const metricsObj = cve['metrics'];
|
|
262
|
+
const cvssV31 = metricsObj?.['cvssMetricV31'];
|
|
263
|
+
const cvssV30 = metricsObj?.['cvssMetricV30'];
|
|
264
|
+
const metrics = cvssV31?.[0] || cvssV30?.[0];
|
|
265
|
+
if (!metrics)
|
|
266
|
+
return null;
|
|
267
|
+
const cvssData = metrics['cvssData'];
|
|
268
|
+
return {
|
|
269
|
+
cvssScore: cvssData?.['baseScore'],
|
|
270
|
+
cvssVector: cvssData?.['vectorString'],
|
|
271
|
+
severity: this.mapCVSSSeverity(cvssData?.['baseScore']),
|
|
272
|
+
};
|
|
134
273
|
}
|
|
135
|
-
catch
|
|
136
|
-
|
|
274
|
+
catch {
|
|
275
|
+
clearTimeout(timeoutId);
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Map CVSS score to severity level
|
|
281
|
+
*/
|
|
282
|
+
mapCVSSSeverity(score) {
|
|
283
|
+
if (!score)
|
|
284
|
+
return 'medium';
|
|
285
|
+
if (score >= 9.0)
|
|
286
|
+
return 'critical';
|
|
287
|
+
if (score >= 7.0)
|
|
288
|
+
return 'high';
|
|
289
|
+
if (score >= 4.0)
|
|
290
|
+
return 'medium';
|
|
291
|
+
return 'low';
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Delay helper for retry backoff
|
|
295
|
+
*/
|
|
296
|
+
delay(ms) {
|
|
297
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse OSV API response
|
|
301
|
+
*/
|
|
302
|
+
parseOSVResponse(data, currentVersion) {
|
|
303
|
+
if (!data.vulns || !Array.isArray(data.vulns)) {
|
|
137
304
|
return [];
|
|
138
305
|
}
|
|
306
|
+
return data.vulns
|
|
307
|
+
.filter((vuln) => {
|
|
308
|
+
// Verify this version is actually affected
|
|
309
|
+
if (!vuln.affected || !Array.isArray(vuln.affected))
|
|
310
|
+
return false;
|
|
311
|
+
return this.isVersionAffected(currentVersion, vuln.affected);
|
|
312
|
+
})
|
|
313
|
+
.map((vuln) => {
|
|
314
|
+
const severity = vuln.severity?.[0] || vuln.database_specific?.severity_score;
|
|
315
|
+
return {
|
|
316
|
+
id: vuln.id,
|
|
317
|
+
source: 'osv',
|
|
318
|
+
severity: this.mapOSVSeverity(severity),
|
|
319
|
+
cvssScore: severity?.score || severity?.cvss_score,
|
|
320
|
+
cvssVector: severity?.vector || severity?.cvss_vector,
|
|
321
|
+
title: vuln.summary || vuln.id,
|
|
322
|
+
description: vuln.details || '',
|
|
323
|
+
affectedVersions: this.extractAffectedVersions(vuln.affected),
|
|
324
|
+
patchedVersions: this.extractPatchedVersions(vuln.affected),
|
|
325
|
+
references: (vuln.references || []).map((r) => r.url).filter(Boolean),
|
|
326
|
+
publishedAt: new Date(vuln.published || Date.now()),
|
|
327
|
+
updatedAt: new Date(vuln.modified || Date.now()),
|
|
328
|
+
cwe: vuln.database_specific?.cwe_ids || [],
|
|
329
|
+
aliases: vuln.aliases || [],
|
|
330
|
+
};
|
|
331
|
+
});
|
|
139
332
|
}
|
|
140
333
|
/**
|
|
141
|
-
*
|
|
334
|
+
* Check if a version is affected by vulnerability ranges
|
|
142
335
|
*/
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
336
|
+
isVersionAffected(version, affected) {
|
|
337
|
+
for (const aff of affected) {
|
|
338
|
+
if (aff.ranges) {
|
|
339
|
+
for (const range of aff.ranges) {
|
|
340
|
+
if (range.type === 'SEMVER' && range.events) {
|
|
341
|
+
let introduced = '0.0.0';
|
|
342
|
+
let fixed = null;
|
|
343
|
+
for (const event of range.events) {
|
|
344
|
+
if (event.introduced)
|
|
345
|
+
introduced = event.introduced;
|
|
346
|
+
if (event.fixed)
|
|
347
|
+
fixed = event.fixed;
|
|
348
|
+
}
|
|
349
|
+
const afterIntroduced = (0, semver_1.compareSemver)(version, introduced) >= 0;
|
|
350
|
+
const beforeFixed = !fixed || (0, semver_1.compareSemver)(version, fixed) < 0;
|
|
351
|
+
if (afterIntroduced && beforeFixed) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Also check explicit versions
|
|
358
|
+
if (aff.versions && Array.isArray(aff.versions)) {
|
|
359
|
+
if (aff.versions.includes(version)) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
159
365
|
}
|
|
160
366
|
/**
|
|
161
367
|
* Map OSV severity to standard levels
|
|
162
368
|
*/
|
|
163
369
|
mapOSVSeverity(severity) {
|
|
164
|
-
if (!severity
|
|
370
|
+
if (!severity)
|
|
165
371
|
return 'medium';
|
|
372
|
+
// Handle array format
|
|
373
|
+
if (Array.isArray(severity) && severity.length > 0) {
|
|
374
|
+
severity = severity[0];
|
|
166
375
|
}
|
|
167
|
-
const score = severity
|
|
376
|
+
const score = severity.score || severity.cvss_score || 0;
|
|
168
377
|
if (score >= 9.0)
|
|
169
378
|
return 'critical';
|
|
170
379
|
if (score >= 7.0)
|
|
@@ -222,22 +431,46 @@ class VulnerabilityDatabase {
|
|
|
222
431
|
return versions;
|
|
223
432
|
}
|
|
224
433
|
/**
|
|
225
|
-
*
|
|
434
|
+
* Calculate remediation path for a vulnerability
|
|
226
435
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
436
|
+
calculateRemediationPath(currentVersion, patchedVersions) {
|
|
437
|
+
if (patchedVersions.length === 0) {
|
|
438
|
+
return {
|
|
439
|
+
action: 'remove',
|
|
440
|
+
breakingChange: false,
|
|
441
|
+
description: 'No fix available. Consider removing or replacing this dependency.',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// Find nearest non-breaking upgrade
|
|
445
|
+
const currentParts = currentVersion.split('.').map(Number);
|
|
446
|
+
const currentMajor = currentParts[0] || 0;
|
|
447
|
+
const nonBreaking = patchedVersions.filter(v => {
|
|
448
|
+
const parts = v.split('.').map(Number);
|
|
449
|
+
return (parts[0] || 0) === currentMajor;
|
|
235
450
|
});
|
|
451
|
+
if (nonBreaking.length > 0) {
|
|
452
|
+
// Sort and pick the lowest non-breaking version
|
|
453
|
+
const sorted = nonBreaking.sort(semver_1.compareSemver);
|
|
454
|
+
return {
|
|
455
|
+
action: 'upgrade',
|
|
456
|
+
targetVersion: sorted[0],
|
|
457
|
+
breakingChange: false,
|
|
458
|
+
description: `Upgrade to ${sorted[0]} (non-breaking)`,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// Breaking change required
|
|
462
|
+
const sorted = patchedVersions.sort(semver_1.compareSemver);
|
|
463
|
+
return {
|
|
464
|
+
action: 'upgrade',
|
|
465
|
+
targetVersion: sorted[0],
|
|
466
|
+
breakingChange: true,
|
|
467
|
+
description: `Upgrade to ${sorted[0]} (breaking change - major version bump)`,
|
|
468
|
+
};
|
|
236
469
|
}
|
|
237
470
|
/**
|
|
238
471
|
* Build result object
|
|
239
472
|
*/
|
|
240
|
-
buildResult(name, version, vulnerabilities) {
|
|
473
|
+
buildResult(name, version, vulnerabilities, _ecosystem, isDirect) {
|
|
241
474
|
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1, none: 0 };
|
|
242
475
|
let highestSeverity = 'none';
|
|
243
476
|
for (const vuln of vulnerabilities) {
|
|
@@ -245,9 +478,13 @@ class VulnerabilityDatabase {
|
|
|
245
478
|
highestSeverity = vuln.severity;
|
|
246
479
|
}
|
|
247
480
|
}
|
|
248
|
-
//
|
|
481
|
+
// Collect all patched versions
|
|
249
482
|
const patchedVersions = vulnerabilities.flatMap(v => v.patchedVersions).filter(Boolean);
|
|
250
483
|
const recommendedVersion = patchedVersions.length > 0 ? patchedVersions[0] : undefined;
|
|
484
|
+
// Calculate remediation path
|
|
485
|
+
const remediationPath = vulnerabilities.length > 0
|
|
486
|
+
? this.calculateRemediationPath(version, patchedVersions)
|
|
487
|
+
: undefined;
|
|
251
488
|
return {
|
|
252
489
|
package: name,
|
|
253
490
|
version,
|
|
@@ -255,12 +492,14 @@ class VulnerabilityDatabase {
|
|
|
255
492
|
isVulnerable: vulnerabilities.length > 0,
|
|
256
493
|
highestSeverity,
|
|
257
494
|
recommendedVersion,
|
|
495
|
+
isDirect,
|
|
496
|
+
remediationPath,
|
|
258
497
|
};
|
|
259
498
|
}
|
|
260
499
|
/**
|
|
261
500
|
* Generate a full vulnerability report for a project
|
|
262
501
|
*/
|
|
263
|
-
async generateReport(projectPath, packages) {
|
|
502
|
+
async generateReport(projectPath, packages, ecosystem = 'npm') {
|
|
264
503
|
const results = await this.checkPackages(packages);
|
|
265
504
|
const summary = {
|
|
266
505
|
critical: 0,
|
|
@@ -268,12 +507,23 @@ class VulnerabilityDatabase {
|
|
|
268
507
|
medium: 0,
|
|
269
508
|
low: 0,
|
|
270
509
|
};
|
|
510
|
+
let directVulnerabilities = 0;
|
|
511
|
+
let transitiveVulnerabilities = 0;
|
|
271
512
|
for (const result of results) {
|
|
272
513
|
for (const vuln of result.vulnerabilities) {
|
|
273
514
|
summary[vuln.severity]++;
|
|
515
|
+
if (result.isDirect) {
|
|
516
|
+
directVulnerabilities++;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
transitiveVulnerabilities++;
|
|
520
|
+
}
|
|
274
521
|
}
|
|
275
522
|
}
|
|
276
523
|
const vulnerablePackages = results.filter(r => r.isVulnerable).length;
|
|
524
|
+
const cacheHitRate = this.cacheHits + this.cacheMisses > 0
|
|
525
|
+
? this.cacheHits / (this.cacheHits + this.cacheMisses)
|
|
526
|
+
: 0;
|
|
277
527
|
return {
|
|
278
528
|
projectPath,
|
|
279
529
|
scanDate: new Date(),
|
|
@@ -281,27 +531,66 @@ class VulnerabilityDatabase {
|
|
|
281
531
|
vulnerablePackages,
|
|
282
532
|
results,
|
|
283
533
|
summary,
|
|
534
|
+
ecosystem,
|
|
535
|
+
directVulnerabilities,
|
|
536
|
+
transitiveVulnerabilities,
|
|
537
|
+
cacheHitRate,
|
|
284
538
|
};
|
|
285
539
|
}
|
|
286
540
|
/**
|
|
287
541
|
* Clear vulnerability cache
|
|
288
542
|
*/
|
|
289
543
|
clearCache() {
|
|
290
|
-
|
|
544
|
+
this.memoryCache.clear();
|
|
545
|
+
this.cacheHits = 0;
|
|
546
|
+
this.cacheMisses = 0;
|
|
547
|
+
try {
|
|
548
|
+
if ((0, fs_1.existsSync)(this.cachePath)) {
|
|
549
|
+
(0, fs_1.writeFileSync)(this.cachePath, JSON.stringify({ version: CACHE_VERSION, entries: {} }));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// Ignore
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Clear entire cache directory
|
|
558
|
+
*/
|
|
559
|
+
static clearCacheDirectory(cacheDir) {
|
|
560
|
+
const targetDir = cacheDir || (0, path_1.join)(process.cwd(), '.guardrail', 'cache');
|
|
561
|
+
try {
|
|
562
|
+
if ((0, fs_1.existsSync)(targetDir)) {
|
|
563
|
+
(0, fs_1.rmSync)(targetDir, { recursive: true, force: true });
|
|
564
|
+
(0, fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
565
|
+
}
|
|
566
|
+
return { success: true, path: targetDir };
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
return {
|
|
570
|
+
success: false,
|
|
571
|
+
path: targetDir,
|
|
572
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
573
|
+
};
|
|
574
|
+
}
|
|
291
575
|
}
|
|
292
576
|
/**
|
|
293
577
|
* Get cache statistics
|
|
294
578
|
*/
|
|
295
579
|
getCacheStats() {
|
|
296
580
|
let oldest = null;
|
|
297
|
-
for (const entry of
|
|
298
|
-
|
|
299
|
-
|
|
581
|
+
for (const entry of this.memoryCache.values()) {
|
|
582
|
+
const date = new Date(entry.fetchedAt);
|
|
583
|
+
if (!oldest || date < oldest) {
|
|
584
|
+
oldest = date;
|
|
300
585
|
}
|
|
301
586
|
}
|
|
587
|
+
const hitRate = this.cacheHits + this.cacheMisses > 0
|
|
588
|
+
? this.cacheHits / (this.cacheHits + this.cacheMisses)
|
|
589
|
+
: 0;
|
|
302
590
|
return {
|
|
303
|
-
size:
|
|
591
|
+
size: this.memoryCache.size,
|
|
304
592
|
oldestEntry: oldest,
|
|
593
|
+
hitRate,
|
|
305
594
|
};
|
|
306
595
|
}
|
|
307
596
|
}
|