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.
Files changed (66) hide show
  1. package/dist/attack-surface/analyzer.d.ts.map +1 -1
  2. package/dist/attack-surface/analyzer.js +3 -2
  3. package/dist/license/engine.d.ts.map +1 -1
  4. package/dist/license/engine.js +3 -2
  5. package/dist/sbom/generator.d.ts +42 -0
  6. package/dist/sbom/generator.d.ts.map +1 -1
  7. package/dist/sbom/generator.js +168 -7
  8. package/dist/secrets/allowlist.d.ts +38 -0
  9. package/dist/secrets/allowlist.d.ts.map +1 -0
  10. package/dist/secrets/allowlist.js +131 -0
  11. package/dist/secrets/config-loader.d.ts +25 -0
  12. package/dist/secrets/config-loader.d.ts.map +1 -0
  13. package/dist/secrets/config-loader.js +103 -0
  14. package/dist/secrets/contextual-risk.d.ts +19 -0
  15. package/dist/secrets/contextual-risk.d.ts.map +1 -0
  16. package/dist/secrets/contextual-risk.js +88 -0
  17. package/dist/secrets/git-scanner.d.ts +29 -0
  18. package/dist/secrets/git-scanner.d.ts.map +1 -0
  19. package/dist/secrets/git-scanner.js +109 -0
  20. package/dist/secrets/guardian.d.ts +70 -57
  21. package/dist/secrets/guardian.d.ts.map +1 -1
  22. package/dist/secrets/guardian.js +532 -240
  23. package/dist/secrets/index.d.ts +4 -0
  24. package/dist/secrets/index.d.ts.map +1 -1
  25. package/dist/secrets/index.js +11 -1
  26. package/dist/secrets/patterns.d.ts +39 -10
  27. package/dist/secrets/patterns.d.ts.map +1 -1
  28. package/dist/secrets/patterns.js +129 -71
  29. package/dist/secrets/pre-commit.d.ts.map +1 -1
  30. package/dist/secrets/pre-commit.js +1 -1
  31. package/dist/secrets/vault-integration.d.ts.map +1 -1
  32. package/dist/secrets/vault-integration.js +1 -0
  33. package/dist/supply-chain/detector.d.ts.map +1 -1
  34. package/dist/supply-chain/detector.js +4 -3
  35. package/dist/supply-chain/vulnerability-db.d.ts +89 -16
  36. package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
  37. package/dist/supply-chain/vulnerability-db.js +404 -115
  38. package/dist/utils/semver.d.ts +37 -0
  39. package/dist/utils/semver.d.ts.map +1 -0
  40. package/dist/utils/semver.js +109 -0
  41. package/package.json +17 -4
  42. package/src/__tests__/license/engine.test.ts +0 -250
  43. package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
  44. package/src/attack-surface/analyzer.ts +0 -152
  45. package/src/attack-surface/index.ts +0 -5
  46. package/src/index.ts +0 -21
  47. package/src/languages/index.ts +0 -91
  48. package/src/languages/java-analyzer.ts +0 -490
  49. package/src/languages/python-analyzer.ts +0 -498
  50. package/src/license/compatibility-matrix.ts +0 -366
  51. package/src/license/engine.ts +0 -345
  52. package/src/license/index.ts +0 -6
  53. package/src/sbom/generator.ts +0 -355
  54. package/src/sbom/index.ts +0 -5
  55. package/src/secrets/guardian.ts +0 -448
  56. package/src/secrets/index.ts +0 -10
  57. package/src/secrets/patterns.ts +0 -186
  58. package/src/secrets/pre-commit.ts +0 -158
  59. package/src/secrets/vault-integration.ts +0 -360
  60. package/src/secrets/vault-providers.ts +0 -446
  61. package/src/supply-chain/detector.ts +0 -252
  62. package/src/supply-chain/index.ts +0 -11
  63. package/src/supply-chain/malicious-db.ts +0 -103
  64. package/src/supply-chain/script-analyzer.ts +0 -194
  65. package/src/supply-chain/typosquat.ts +0 -302
  66. package/src/supply-chain/vulnerability-db.ts +0 -386
@@ -2,169 +2,378 @@
2
2
  /**
3
3
  * Vulnerability Database Integration
4
4
  *
5
- * Integrates with multiple vulnerability databases:
6
- * - OSV (Open Source Vulnerabilities)
7
- * - GitHub Security Advisories
8
- * - NVD (National Vulnerability Database)
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 vulnerabilityCache = new Map();
13
- const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
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
- npmAuditUrl = 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk';
17
- // GitHub API available for future enhancement with authentication
18
- // private githubApiUrl = 'https://api.github.com/advisories';
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}@${version}`;
24
- const cached = vulnerabilityCache.get(cacheKey);
25
- if (cached && (Date.now() - cached.fetchedAt.getTime()) < CACHE_TTL_MS) {
26
- return this.buildResult(name, version, cached.data);
27
- }
28
- const vulnerabilities = [];
29
- // Query multiple sources in parallel
30
- const [osvVulns, npmVulns] = await Promise.allSettled([
31
- this.queryOSV(name, version),
32
- this.queryNpmAudit(name, version),
33
- ]);
34
- if (osvVulns.status === 'fulfilled') {
35
- vulnerabilities.push(...osvVulns.value);
36
- }
37
- if (npmVulns.status === 'fulfilled') {
38
- vulnerabilities.push(...npmVulns.value);
39
- }
40
- // Deduplicate by ID
41
- const uniqueVulns = this.deduplicateVulnerabilities(vulnerabilities);
42
- // Cache the result
43
- vulnerabilityCache.set(cacheKey, {
44
- data: uniqueVulns,
45
- fetchedAt: new Date(),
46
- });
47
- return this.buildResult(name, version, uniqueVulns);
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 = 20;
129
+ const batchSize = 50;
54
130
  const results = [];
55
- for (let i = 0; i < packages.length; i += batchSize) {
56
- const batch = packages.slice(i, i + batchSize);
57
- const batchResults = await Promise.all(batch.map(pkg => this.checkPackage(pkg.name, pkg.version)));
58
- results.push(...batchResults);
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: 'npm',
188
+ ecosystem: ecosystem,
76
189
  },
77
190
  version: version,
78
191
  }),
79
- signal: AbortSignal.timeout(10000),
192
+ signal: controller.signal,
80
193
  });
194
+ clearTimeout(timeoutId);
81
195
  if (!response.ok) {
82
- return [];
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
- console.error(`OSV query failed for ${packageName}@${version}:`, error);
89
- return [];
202
+ clearTimeout(timeoutId);
203
+ throw error;
90
204
  }
91
205
  }
92
206
  /**
93
- * Parse OSV API response
207
+ * Enrich vulnerabilities with NVD data (CVSS scores)
94
208
  */
95
- parseOSVResponse(data) {
96
- if (!data.vulns || !Array.isArray(data.vulns)) {
97
- return [];
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 data.vulns.map((vuln) => ({
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 npm audit API
237
+ * Query NVD API for CVE details
116
238
  */
117
- async queryNpmAudit(packageName, version) {
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(this.npmAuditUrl, {
120
- method: 'POST',
243
+ const response = await fetch(`${NVD_API_URL}?cveId=${encodeURIComponent(cveId)}`, {
244
+ method: 'GET',
121
245
  headers: {
122
- 'Content-Type': 'application/json',
246
+ 'User-Agent': 'guardrail-cli/1.0',
123
247
  },
124
- body: JSON.stringify({
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
- return this.parseNpmAuditResponse(data, packageName);
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 (error) {
136
- console.error(`npm audit query failed for ${packageName}@${version}:`, error);
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
- * Parse npm audit response
334
+ * Check if a version is affected by vulnerability ranges
142
335
  */
143
- parseNpmAuditResponse(data, packageName) {
144
- const advisories = data[packageName] || [];
145
- return advisories.map((advisory) => ({
146
- id: `npm-${advisory.id || advisory.github_advisory_id}`,
147
- source: 'npm',
148
- severity: advisory.severity || 'medium',
149
- cvssScore: advisory.cvss?.score,
150
- title: advisory.title || 'Security Advisory',
151
- description: advisory.overview || advisory.recommendation || '',
152
- affectedVersions: [advisory.vulnerable_versions || '*'],
153
- patchedVersions: [advisory.patched_versions || 'No patch available'],
154
- references: [advisory.url].filter(Boolean),
155
- publishedAt: new Date(advisory.created || Date.now()),
156
- updatedAt: new Date(advisory.updated || Date.now()),
157
- cwe: advisory.cwe ? [advisory.cwe] : [],
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 || !Array.isArray(severity) || severity.length === 0) {
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[0]?.score || 0;
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
- * Deduplicate vulnerabilities by ID
434
+ * Calculate remediation path for a vulnerability
226
435
  */
227
- deduplicateVulnerabilities(vulns) {
228
- const seen = new Set();
229
- return vulns.filter(vuln => {
230
- if (seen.has(vuln.id)) {
231
- return false;
232
- }
233
- seen.add(vuln.id);
234
- return true;
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
- // Find recommended version from patches
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
- vulnerabilityCache.clear();
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 vulnerabilityCache.values()) {
298
- if (!oldest || entry.fetchedAt < oldest) {
299
- oldest = entry.fetchedAt;
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: vulnerabilityCache.size,
591
+ size: this.memoryCache.size,
304
592
  oldestEntry: oldest,
593
+ hitRate,
305
594
  };
306
595
  }
307
596
  }