guardrail-cli 1.0.6 → 2.0.1

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 (144) hide show
  1. package/README.md +483 -10
  2. package/dist/commands/baseline.d.ts +7 -0
  3. package/dist/commands/baseline.d.ts.map +1 -0
  4. package/dist/commands/baseline.js +79 -0
  5. package/dist/commands/baseline.js.map +1 -0
  6. package/dist/commands/cache.d.ts +13 -0
  7. package/dist/commands/cache.d.ts.map +1 -0
  8. package/dist/commands/cache.js +165 -0
  9. package/dist/commands/cache.js.map +1 -0
  10. package/dist/commands/evidence.d.ts +45 -0
  11. package/dist/commands/evidence.d.ts.map +1 -0
  12. package/dist/commands/evidence.js +197 -0
  13. package/dist/commands/evidence.js.map +1 -0
  14. package/dist/commands/index.d.ts +8 -0
  15. package/dist/commands/index.d.ts.map +1 -0
  16. package/dist/commands/index.js +15 -0
  17. package/dist/commands/index.js.map +1 -0
  18. package/dist/commands/scan-secrets.d.ts +47 -0
  19. package/dist/commands/scan-secrets.d.ts.map +1 -0
  20. package/dist/commands/scan-secrets.js +225 -0
  21. package/dist/commands/scan-secrets.js.map +1 -0
  22. package/dist/commands/scan-vulnerabilities-enhanced.d.ts +41 -0
  23. package/dist/commands/scan-vulnerabilities-enhanced.d.ts.map +1 -0
  24. package/dist/commands/scan-vulnerabilities-enhanced.js +368 -0
  25. package/dist/commands/scan-vulnerabilities-enhanced.js.map +1 -0
  26. package/dist/commands/scan-vulnerabilities-osv.d.ts +58 -0
  27. package/dist/commands/scan-vulnerabilities-osv.d.ts.map +1 -0
  28. package/dist/commands/scan-vulnerabilities-osv.js +716 -0
  29. package/dist/commands/scan-vulnerabilities-osv.js.map +1 -0
  30. package/dist/commands/scan-vulnerabilities.d.ts +32 -0
  31. package/dist/commands/scan-vulnerabilities.d.ts.map +1 -0
  32. package/dist/commands/scan-vulnerabilities.js +283 -0
  33. package/dist/commands/scan-vulnerabilities.js.map +1 -0
  34. package/dist/commands/secrets-allowlist.d.ts +7 -0
  35. package/dist/commands/secrets-allowlist.d.ts.map +1 -0
  36. package/dist/commands/secrets-allowlist.js +85 -0
  37. package/dist/commands/secrets-allowlist.js.map +1 -0
  38. package/dist/fix/applicator.d.ts +44 -0
  39. package/dist/fix/applicator.d.ts.map +1 -0
  40. package/dist/fix/applicator.js +144 -0
  41. package/dist/fix/applicator.js.map +1 -0
  42. package/dist/fix/backup.d.ts +38 -0
  43. package/dist/fix/backup.d.ts.map +1 -0
  44. package/dist/fix/backup.js +154 -0
  45. package/dist/fix/backup.js.map +1 -0
  46. package/dist/fix/engine.d.ts +55 -0
  47. package/dist/fix/engine.d.ts.map +1 -0
  48. package/dist/fix/engine.js +285 -0
  49. package/dist/fix/engine.js.map +1 -0
  50. package/dist/fix/index.d.ts +5 -0
  51. package/dist/fix/index.d.ts.map +1 -0
  52. package/dist/fix/index.js +12 -0
  53. package/dist/fix/index.js.map +1 -0
  54. package/dist/fix/interactive.d.ts +22 -0
  55. package/dist/fix/interactive.d.ts.map +1 -0
  56. package/dist/fix/interactive.js +172 -0
  57. package/dist/fix/interactive.js.map +1 -0
  58. package/dist/formatters/index.d.ts +6 -0
  59. package/dist/formatters/index.d.ts.map +1 -0
  60. package/dist/formatters/index.js +11 -0
  61. package/dist/formatters/index.js.map +1 -0
  62. package/dist/formatters/sarif-enhanced.d.ts +78 -0
  63. package/dist/formatters/sarif-enhanced.d.ts.map +1 -0
  64. package/dist/formatters/sarif-enhanced.js +144 -0
  65. package/dist/formatters/sarif-enhanced.js.map +1 -0
  66. package/dist/formatters/sarif-v2.d.ts +121 -0
  67. package/dist/formatters/sarif-v2.d.ts.map +1 -0
  68. package/dist/formatters/sarif-v2.js +356 -0
  69. package/dist/formatters/sarif-v2.js.map +1 -0
  70. package/dist/formatters/sarif.d.ts +72 -0
  71. package/dist/formatters/sarif.d.ts.map +1 -0
  72. package/dist/formatters/sarif.js +146 -0
  73. package/dist/formatters/sarif.js.map +1 -0
  74. package/dist/index.js +3362 -1397
  75. package/dist/index.js.map +1 -1
  76. package/dist/init/ci-generator.d.ts +18 -0
  77. package/dist/init/ci-generator.d.ts.map +1 -0
  78. package/dist/init/ci-generator.js +251 -0
  79. package/dist/init/ci-generator.js.map +1 -0
  80. package/dist/init/detect-framework.d.ts +15 -0
  81. package/dist/init/detect-framework.d.ts.map +1 -0
  82. package/dist/init/detect-framework.js +299 -0
  83. package/dist/init/detect-framework.js.map +1 -0
  84. package/dist/init/hooks-installer.d.ts +22 -0
  85. package/dist/init/hooks-installer.d.ts.map +1 -0
  86. package/dist/init/hooks-installer.js +302 -0
  87. package/dist/init/hooks-installer.js.map +1 -0
  88. package/dist/init/index.d.ts +8 -0
  89. package/dist/init/index.d.ts.map +1 -0
  90. package/dist/init/index.js +22 -0
  91. package/dist/init/index.js.map +1 -0
  92. package/dist/init/templates.d.ts +401 -0
  93. package/dist/init/templates.d.ts.map +1 -0
  94. package/dist/init/templates.js +240 -0
  95. package/dist/init/templates.js.map +1 -0
  96. package/dist/reality/reality-runner.d.ts +76 -0
  97. package/dist/reality/reality-runner.d.ts.map +1 -0
  98. package/dist/reality/reality-runner.js +454 -0
  99. package/dist/reality/reality-runner.js.map +1 -0
  100. package/dist/runtime/auth-utils.d.ts +43 -0
  101. package/dist/runtime/auth-utils.d.ts.map +1 -0
  102. package/dist/runtime/auth-utils.js +126 -0
  103. package/dist/runtime/auth-utils.js.map +1 -0
  104. package/dist/runtime/client.d.ts +74 -0
  105. package/dist/runtime/client.d.ts.map +1 -0
  106. package/dist/runtime/client.js +222 -0
  107. package/dist/runtime/client.js.map +1 -0
  108. package/dist/runtime/creds.d.ts +48 -0
  109. package/dist/runtime/creds.d.ts.map +1 -0
  110. package/dist/runtime/creds.js +245 -0
  111. package/dist/runtime/creds.js.map +1 -0
  112. package/dist/runtime/exit-codes.d.ts +47 -0
  113. package/dist/runtime/exit-codes.d.ts.map +1 -0
  114. package/dist/runtime/exit-codes.js +91 -0
  115. package/dist/runtime/exit-codes.js.map +1 -0
  116. package/dist/runtime/index.d.ts +9 -0
  117. package/dist/runtime/index.d.ts.map +1 -0
  118. package/dist/runtime/index.js +25 -0
  119. package/dist/runtime/index.js.map +1 -0
  120. package/dist/runtime/semver.d.ts +37 -0
  121. package/dist/runtime/semver.d.ts.map +1 -0
  122. package/dist/runtime/semver.js +110 -0
  123. package/dist/runtime/semver.js.map +1 -0
  124. package/dist/scanner/baseline.d.ts +52 -0
  125. package/dist/scanner/baseline.d.ts.map +1 -0
  126. package/dist/scanner/baseline.js +85 -0
  127. package/dist/scanner/baseline.js.map +1 -0
  128. package/dist/scanner/incremental.d.ts +30 -0
  129. package/dist/scanner/incremental.d.ts.map +1 -0
  130. package/dist/scanner/incremental.js +82 -0
  131. package/dist/scanner/incremental.js.map +1 -0
  132. package/dist/scanner/parallel.d.ts +43 -0
  133. package/dist/scanner/parallel.d.ts.map +1 -0
  134. package/dist/scanner/parallel.js +99 -0
  135. package/dist/scanner/parallel.js.map +1 -0
  136. package/dist/ui/frame.d.ts +68 -0
  137. package/dist/ui/frame.d.ts.map +1 -0
  138. package/dist/ui/frame.js +165 -0
  139. package/dist/ui/frame.js.map +1 -0
  140. package/dist/ui/index.d.ts +5 -0
  141. package/dist/ui/index.d.ts.map +1 -0
  142. package/dist/ui/index.js +16 -0
  143. package/dist/ui/index.js.map +1 -0
  144. package/package.json +42 -9
@@ -0,0 +1,716 @@
1
+ "use strict";
2
+ /**
3
+ * scan:vulnerabilities command (OSV Integration)
4
+ *
5
+ * Enterprise-grade vulnerability detection using real-time OSV API
6
+ *
7
+ * Features:
8
+ * - Real-time OSV API queries with 24h caching
9
+ * - Lockfile parsing (package-lock.json, pnpm-lock.yaml, yarn.lock)
10
+ * - Multi-ecosystem support (npm, PyPI, RubyGems, Go)
11
+ * - CVSS scoring and vectors with optional NVD enrichment
12
+ * - Remediation path analysis
13
+ * - SARIF v2.1.0 output for GitHub code scanning
14
+ * - Direct vs transitive vulnerability grouping
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.scanVulnerabilitiesOSV = scanVulnerabilitiesOSV;
18
+ exports.toSarifVulnerabilitiesOSV = toSarifVulnerabilitiesOSV;
19
+ exports.outputOSVVulnResults = outputOSVVulnResults;
20
+ exports.registerScanVulnerabilitiesOSVCommand = registerScanVulnerabilitiesOSVCommand;
21
+ const path_1 = require("path");
22
+ const fs_1 = require("fs");
23
+ const exit_codes_1 = require("../runtime/exit-codes");
24
+ const vulnerability_db_1 = require("guardrail-security/supply-chain/vulnerability-db");
25
+ const evidence_1 = require("./evidence");
26
+ const c = {
27
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
28
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
29
+ critical: (s) => `\x1b[35m${s}\x1b[0m`,
30
+ high: (s) => `\x1b[31m${s}\x1b[0m`,
31
+ medium: (s) => `\x1b[33m${s}\x1b[0m`,
32
+ low: (s) => `\x1b[36m${s}\x1b[0m`,
33
+ success: (s) => `\x1b[32m${s}\x1b[0m`,
34
+ info: (s) => `\x1b[34m${s}\x1b[0m`,
35
+ };
36
+ /**
37
+ * Detect ecosystems from project files
38
+ */
39
+ function detectEcosystems(projectPath) {
40
+ const ecosystems = [];
41
+ if ((0, fs_1.existsSync)((0, path_1.join)(projectPath, 'package.json')) ||
42
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'package-lock.json')) ||
43
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'pnpm-lock.yaml')) ||
44
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'yarn.lock'))) {
45
+ ecosystems.push('npm');
46
+ }
47
+ if ((0, fs_1.existsSync)((0, path_1.join)(projectPath, 'requirements.txt')) ||
48
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'Pipfile')) ||
49
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'poetry.lock')) ||
50
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'pyproject.toml'))) {
51
+ ecosystems.push('PyPI');
52
+ }
53
+ if ((0, fs_1.existsSync)((0, path_1.join)(projectPath, 'Gemfile')) ||
54
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'Gemfile.lock'))) {
55
+ ecosystems.push('RubyGems');
56
+ }
57
+ if ((0, fs_1.existsSync)((0, path_1.join)(projectPath, 'go.mod')) ||
58
+ (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'go.sum'))) {
59
+ ecosystems.push('Go');
60
+ }
61
+ return ecosystems;
62
+ }
63
+ /**
64
+ * Find line number of a dependency in package.json
65
+ */
66
+ function findPackageJsonLine(content, packageName) {
67
+ const lines = content.split('\n');
68
+ for (let i = 0; i < lines.length; i++) {
69
+ if (lines[i].includes(`"${packageName}"`)) {
70
+ return i + 1;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ /**
76
+ * Parse npm dependencies from package.json and lockfiles
77
+ */
78
+ function parseNpmDependencies(projectPath) {
79
+ const packages = [];
80
+ const lockfiles = [];
81
+ const packageJsonPath = (0, path_1.join)(projectPath, 'package.json');
82
+ if (!(0, fs_1.existsSync)(packageJsonPath))
83
+ return { packages, lockfiles };
84
+ let packageJsonContent = '';
85
+ try {
86
+ packageJsonContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
87
+ const packageJson = JSON.parse(packageJsonContent);
88
+ const deps = packageJson.dependencies || {};
89
+ const devDeps = packageJson.devDependencies || {};
90
+ for (const [name, version] of Object.entries(deps)) {
91
+ const cleanVersion = String(version).replace(/^[\^~>=<]+/, '');
92
+ const line = findPackageJsonLine(packageJsonContent, name);
93
+ packages.push({
94
+ name,
95
+ version: cleanVersion,
96
+ ecosystem: 'npm',
97
+ isDirect: true,
98
+ location: { file: 'package.json', line }
99
+ });
100
+ }
101
+ for (const [name, version] of Object.entries(devDeps)) {
102
+ const cleanVersion = String(version).replace(/^[\^~>=<]+/, '');
103
+ const line = findPackageJsonLine(packageJsonContent, name);
104
+ packages.push({
105
+ name,
106
+ version: cleanVersion,
107
+ ecosystem: 'npm',
108
+ isDirect: true,
109
+ location: { file: 'package.json', line }
110
+ });
111
+ }
112
+ }
113
+ catch {
114
+ return { packages, lockfiles };
115
+ }
116
+ // Parse package-lock.json
117
+ const npmLockPath = (0, path_1.join)(projectPath, 'package-lock.json');
118
+ if ((0, fs_1.existsSync)(npmLockPath)) {
119
+ lockfiles.push('package-lock.json');
120
+ try {
121
+ const lockData = JSON.parse((0, fs_1.readFileSync)(npmLockPath, 'utf-8'));
122
+ const lockPackages = lockData.packages || {};
123
+ for (const [pkgPath, pkgInfo] of Object.entries(lockPackages)) {
124
+ if (typeof pkgInfo === 'object' && pkgInfo !== null) {
125
+ const info = pkgInfo;
126
+ const name = info.name || pkgPath.replace(/^node_modules\//, '');
127
+ const version = info.version;
128
+ if (name && version && !packages.find(p => p.name === name && p.version === version)) {
129
+ packages.push({
130
+ name,
131
+ version,
132
+ ecosystem: 'npm',
133
+ isDirect: false,
134
+ location: { file: 'package-lock.json' }
135
+ });
136
+ }
137
+ }
138
+ }
139
+ }
140
+ catch {
141
+ // Lockfile parsing failed
142
+ }
143
+ }
144
+ // Parse pnpm-lock.yaml
145
+ const pnpmLockPath = (0, path_1.join)(projectPath, 'pnpm-lock.yaml');
146
+ if ((0, fs_1.existsSync)(pnpmLockPath)) {
147
+ lockfiles.push('pnpm-lock.yaml');
148
+ try {
149
+ const content = (0, fs_1.readFileSync)(pnpmLockPath, 'utf-8');
150
+ // Simple YAML parsing for pnpm lockfile
151
+ const lines = content.split('\n');
152
+ let inPackages = false;
153
+ for (const line of lines) {
154
+ if (line.startsWith('packages:')) {
155
+ inPackages = true;
156
+ continue;
157
+ }
158
+ if (inPackages && line.match(/^\s{2}'?\/([^@]+)@([^':]+)/)) {
159
+ const match = line.match(/^\s{2}'?\/([^@]+)@([^':]+)/);
160
+ if (match) {
161
+ const name = match[1];
162
+ const version = match[2].replace(/['"]/g, '');
163
+ if (!packages.find(p => p.name === name && p.version === version)) {
164
+ packages.push({
165
+ name,
166
+ version,
167
+ ecosystem: 'npm',
168
+ isDirect: false,
169
+ location: { file: 'pnpm-lock.yaml' }
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ // Lockfile parsing failed
178
+ }
179
+ }
180
+ // Parse yarn.lock
181
+ const yarnLockPath = (0, path_1.join)(projectPath, 'yarn.lock');
182
+ if ((0, fs_1.existsSync)(yarnLockPath)) {
183
+ lockfiles.push('yarn.lock');
184
+ try {
185
+ const content = (0, fs_1.readFileSync)(yarnLockPath, 'utf-8');
186
+ const lines = content.split('\n');
187
+ let currentPackage = '';
188
+ for (const line of lines) {
189
+ // Match package header: "package@version:" or package@version:
190
+ const headerMatch = line.match(/^"?([^@]+)@[^"]+:?\s*$/);
191
+ if (headerMatch) {
192
+ currentPackage = headerMatch[1];
193
+ continue;
194
+ }
195
+ // Match version line
196
+ if (currentPackage && line.match(/^\s+version\s+"?([^"]+)"?/)) {
197
+ const versionMatch = line.match(/^\s+version\s+"?([^"]+)"?/);
198
+ if (versionMatch) {
199
+ const version = versionMatch[1];
200
+ if (!packages.find(p => p.name === currentPackage && p.version === version)) {
201
+ packages.push({
202
+ name: currentPackage,
203
+ version,
204
+ ecosystem: 'npm',
205
+ isDirect: false,
206
+ location: { file: 'yarn.lock' }
207
+ });
208
+ }
209
+ }
210
+ currentPackage = '';
211
+ }
212
+ }
213
+ }
214
+ catch {
215
+ // Lockfile parsing failed
216
+ }
217
+ }
218
+ return { packages, lockfiles };
219
+ }
220
+ /**
221
+ * Parse Python dependencies
222
+ */
223
+ function parsePythonDependencies(projectPath) {
224
+ const packages = [];
225
+ const lockfiles = [];
226
+ const requirementsPath = (0, path_1.join)(projectPath, 'requirements.txt');
227
+ if ((0, fs_1.existsSync)(requirementsPath)) {
228
+ lockfiles.push('requirements.txt');
229
+ try {
230
+ const content = (0, fs_1.readFileSync)(requirementsPath, 'utf-8');
231
+ const lines = content.split('\n');
232
+ for (let i = 0; i < lines.length; i++) {
233
+ const line = lines[i].trim();
234
+ if (!line || line.startsWith('#'))
235
+ continue;
236
+ const match = line.match(/^([a-zA-Z0-9_-]+)(?:==|>=|<=|~=|>|<)?([\d.]+)?/);
237
+ if (match) {
238
+ const name = match[1];
239
+ const version = match[2] || 'latest';
240
+ packages.push({
241
+ name,
242
+ version,
243
+ ecosystem: 'PyPI',
244
+ isDirect: true,
245
+ location: { file: 'requirements.txt', line: i + 1 }
246
+ });
247
+ }
248
+ }
249
+ }
250
+ catch {
251
+ // Requirements parsing failed
252
+ }
253
+ }
254
+ // Parse Pipfile.lock
255
+ const pipfileLockPath = (0, path_1.join)(projectPath, 'Pipfile.lock');
256
+ if ((0, fs_1.existsSync)(pipfileLockPath)) {
257
+ lockfiles.push('Pipfile.lock');
258
+ try {
259
+ const lockData = JSON.parse((0, fs_1.readFileSync)(pipfileLockPath, 'utf-8'));
260
+ const sections = ['default', 'develop'];
261
+ for (const section of sections) {
262
+ const deps = lockData[section] || {};
263
+ for (const [name, info] of Object.entries(deps)) {
264
+ if (typeof info === 'object' && info !== null) {
265
+ const pkgInfo = info;
266
+ const version = pkgInfo.version?.replace(/^==/, '') || 'latest';
267
+ if (!packages.find(p => p.name === name)) {
268
+ packages.push({
269
+ name,
270
+ version,
271
+ ecosystem: 'PyPI',
272
+ isDirect: section === 'default',
273
+ location: { file: 'Pipfile.lock' }
274
+ });
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ catch {
281
+ // Pipfile.lock parsing failed
282
+ }
283
+ }
284
+ return { packages, lockfiles };
285
+ }
286
+ /**
287
+ * Parse Ruby dependencies
288
+ */
289
+ function parseRubyDependencies(projectPath) {
290
+ const packages = [];
291
+ const lockfiles = [];
292
+ // Parse Gemfile.lock for exact versions
293
+ const gemfileLockPath = (0, path_1.join)(projectPath, 'Gemfile.lock');
294
+ if ((0, fs_1.existsSync)(gemfileLockPath)) {
295
+ lockfiles.push('Gemfile.lock');
296
+ try {
297
+ const content = (0, fs_1.readFileSync)(gemfileLockPath, 'utf-8');
298
+ const lines = content.split('\n');
299
+ let inSpecs = false;
300
+ for (const line of lines) {
301
+ if (line.trim() === 'specs:') {
302
+ inSpecs = true;
303
+ continue;
304
+ }
305
+ if (inSpecs && line.match(/^\s{4}(\S+)\s+\(([^)]+)\)/)) {
306
+ const match = line.match(/^\s{4}(\S+)\s+\(([^)]+)\)/);
307
+ if (match) {
308
+ packages.push({
309
+ name: match[1],
310
+ version: match[2],
311
+ ecosystem: 'RubyGems',
312
+ isDirect: true,
313
+ location: { file: 'Gemfile.lock' }
314
+ });
315
+ }
316
+ }
317
+ if (inSpecs && !line.startsWith(' ') && line.trim() !== '') {
318
+ inSpecs = false;
319
+ }
320
+ }
321
+ }
322
+ catch {
323
+ // Gemfile.lock parsing failed
324
+ }
325
+ }
326
+ return { packages, lockfiles };
327
+ }
328
+ /**
329
+ * Parse Go dependencies
330
+ */
331
+ function parseGoDependencies(projectPath) {
332
+ const packages = [];
333
+ const lockfiles = [];
334
+ // Parse go.sum for exact versions
335
+ const goSumPath = (0, path_1.join)(projectPath, 'go.sum');
336
+ if ((0, fs_1.existsSync)(goSumPath)) {
337
+ lockfiles.push('go.sum');
338
+ try {
339
+ const content = (0, fs_1.readFileSync)(goSumPath, 'utf-8');
340
+ const lines = content.split('\n');
341
+ const seen = new Set();
342
+ for (const line of lines) {
343
+ const match = line.match(/^(\S+)\s+v?([^\s/]+)/);
344
+ if (match) {
345
+ const name = match[1];
346
+ const version = match[2].replace('/go.mod', '');
347
+ const key = `${name}@${version}`;
348
+ if (!seen.has(key)) {
349
+ seen.add(key);
350
+ packages.push({
351
+ name,
352
+ version,
353
+ ecosystem: 'Go',
354
+ isDirect: true,
355
+ location: { file: 'go.sum' }
356
+ });
357
+ }
358
+ }
359
+ }
360
+ }
361
+ catch {
362
+ // go.sum parsing failed
363
+ }
364
+ }
365
+ return { packages, lockfiles };
366
+ }
367
+ /**
368
+ * Scan vulnerabilities with OSV integration
369
+ */
370
+ async function scanVulnerabilitiesOSV(projectPath, options) {
371
+ const startTime = Date.now();
372
+ const ecosystems = options.ecosystem
373
+ ? [options.ecosystem]
374
+ : detectEcosystems(projectPath);
375
+ if (ecosystems.length === 0) {
376
+ return {
377
+ projectPath,
378
+ scanType: 'vulnerabilities',
379
+ ecosystem: 'npm',
380
+ packagesScanned: 0,
381
+ findings: [],
382
+ summary: { critical: 0, high: 0, medium: 0, low: 0 },
383
+ directVulnerabilities: 0,
384
+ transitiveVulnerabilities: 0,
385
+ cacheHitRate: 0,
386
+ scanDuration: Date.now() - startTime,
387
+ nvdEnriched: false,
388
+ lockfilesParsed: [],
389
+ };
390
+ }
391
+ // Parse dependencies from all detected ecosystems
392
+ let allPackages = [];
393
+ let allLockfiles = [];
394
+ for (const ecosystem of ecosystems) {
395
+ let result;
396
+ switch (ecosystem) {
397
+ case 'npm':
398
+ result = parseNpmDependencies(projectPath);
399
+ break;
400
+ case 'PyPI':
401
+ result = parsePythonDependencies(projectPath);
402
+ break;
403
+ case 'RubyGems':
404
+ result = parseRubyDependencies(projectPath);
405
+ break;
406
+ case 'Go':
407
+ result = parseGoDependencies(projectPath);
408
+ break;
409
+ default:
410
+ result = { packages: [], lockfiles: [] };
411
+ }
412
+ allPackages.push(...result.packages);
413
+ allLockfiles.push(...result.lockfiles);
414
+ }
415
+ // Deduplicate packages
416
+ const seen = new Set();
417
+ allPackages = allPackages.filter(pkg => {
418
+ const key = `${pkg.ecosystem}:${pkg.name}:${pkg.version}`;
419
+ if (seen.has(key))
420
+ return false;
421
+ seen.add(key);
422
+ return true;
423
+ });
424
+ // Configure and query OSV
425
+ const dbOptions = {
426
+ noCache: options.noCache,
427
+ nvdEnrichment: options.nvd,
428
+ cacheDir: (0, path_1.join)(projectPath, '.guardrail', 'cache'),
429
+ };
430
+ const db = new vulnerability_db_1.VulnerabilityDatabase(dbOptions);
431
+ const results = await db.checkPackages(allPackages);
432
+ // Attach location info to results
433
+ const resultsWithLocation = results.map((result, idx) => ({
434
+ ...result,
435
+ location: allPackages[idx]?.location,
436
+ }));
437
+ // Calculate summary
438
+ const summary = {
439
+ critical: 0,
440
+ high: 0,
441
+ medium: 0,
442
+ low: 0,
443
+ };
444
+ let directVulnerabilities = 0;
445
+ let transitiveVulnerabilities = 0;
446
+ for (const result of resultsWithLocation) {
447
+ if (result.isVulnerable) {
448
+ for (const vuln of result.vulnerabilities) {
449
+ summary[vuln.severity]++;
450
+ }
451
+ if (result.isDirect) {
452
+ directVulnerabilities += result.vulnerabilities.length;
453
+ }
454
+ else {
455
+ transitiveVulnerabilities += result.vulnerabilities.length;
456
+ }
457
+ }
458
+ }
459
+ const cacheStats = db.getCacheStats();
460
+ return {
461
+ projectPath,
462
+ scanType: 'vulnerabilities',
463
+ ecosystem: ecosystems[0],
464
+ packagesScanned: allPackages.length,
465
+ findings: resultsWithLocation.filter(r => r.isVulnerable),
466
+ summary,
467
+ directVulnerabilities,
468
+ transitiveVulnerabilities,
469
+ cacheHitRate: cacheStats.hitRate,
470
+ scanDuration: Date.now() - startTime,
471
+ nvdEnriched: options.nvd || false,
472
+ lockfilesParsed: [...new Set(allLockfiles)],
473
+ };
474
+ }
475
+ /**
476
+ * Generate SARIF v2.1.0 output
477
+ */
478
+ function toSarifVulnerabilitiesOSV(results) {
479
+ const version = '1.0.0';
480
+ const ruleMap = new Map();
481
+ // Build rules from unique vulnerability IDs
482
+ for (const finding of results.findings) {
483
+ for (const vuln of finding.vulnerabilities) {
484
+ if (!ruleMap.has(vuln.id)) {
485
+ const cveId = vuln.aliases?.find(a => a.startsWith('CVE-'));
486
+ ruleMap.set(vuln.id, {
487
+ id: vuln.id,
488
+ name: vuln.title.substring(0, 100),
489
+ shortDescription: { text: vuln.title },
490
+ fullDescription: { text: vuln.description || vuln.title },
491
+ helpUri: vuln.references?.[0] || `https://osv.dev/vulnerability/${vuln.id}`,
492
+ help: {
493
+ text: `Vulnerability ${vuln.id} affects this package.\n\n` +
494
+ `Severity: ${vuln.severity.toUpperCase()}\n` +
495
+ (vuln.cvssScore ? `CVSS Score: ${vuln.cvssScore}\n` : '') +
496
+ (cveId ? `CVE: ${cveId}\n` : '') +
497
+ `\nReferences:\n${vuln.references?.map(r => `- ${r}`).join('\n') || 'None'}`,
498
+ markdown: `## ${vuln.title}\n\n` +
499
+ `**Severity:** ${vuln.severity.toUpperCase()}\n\n` +
500
+ (vuln.cvssScore ? `**CVSS Score:** ${vuln.cvssScore}\n\n` : '') +
501
+ (vuln.cvssVector ? `**CVSS Vector:** \`${vuln.cvssVector}\`\n\n` : '') +
502
+ (cveId ? `**CVE:** [${cveId}](https://nvd.nist.gov/vuln/detail/${cveId})\n\n` : '') +
503
+ `### References\n${vuln.references?.map(r => `- [${r}](${r})`).join('\n') || 'None'}`,
504
+ },
505
+ defaultConfiguration: {
506
+ level: vuln.severity === 'critical' || vuln.severity === 'high' ? 'error' :
507
+ vuln.severity === 'medium' ? 'warning' : 'note',
508
+ },
509
+ properties: {
510
+ 'security-severity': vuln.cvssScore?.toString() ||
511
+ (vuln.severity === 'critical' ? '9.0' :
512
+ vuln.severity === 'high' ? '7.0' :
513
+ vuln.severity === 'medium' ? '4.0' : '2.0'),
514
+ tags: ['security', 'vulnerability', 'dependency', vuln.source],
515
+ precision: 'high',
516
+ },
517
+ });
518
+ }
519
+ }
520
+ }
521
+ const sarifResults = [];
522
+ for (const finding of results.findings) {
523
+ for (const vuln of finding.vulnerabilities) {
524
+ const location = finding.location || { file: 'package.json', line: 1 };
525
+ const remediationText = finding.remediationPath
526
+ ? `${finding.remediationPath.description}${finding.remediationPath.breakingChange ? ' (Breaking change)' : ''}`
527
+ : `Upgrade to ${finding.recommendedVersion || 'latest'}`;
528
+ sarifResults.push({
529
+ ruleId: vuln.id,
530
+ ruleIndex: Array.from(ruleMap.keys()).indexOf(vuln.id),
531
+ level: vuln.severity === 'critical' || vuln.severity === 'high' ? 'error' :
532
+ vuln.severity === 'medium' ? 'warning' : 'note',
533
+ message: {
534
+ text: `${vuln.title} in ${finding.package}@${finding.version}. ${remediationText}`,
535
+ },
536
+ locations: [{
537
+ physicalLocation: {
538
+ artifactLocation: {
539
+ uri: location.file,
540
+ uriBaseId: '%SRCROOT%',
541
+ },
542
+ region: {
543
+ startLine: location.line || 1,
544
+ startColumn: 1,
545
+ },
546
+ },
547
+ }],
548
+ fingerprints: {
549
+ 'guardrail/v1': `${vuln.id}:${finding.package}:${finding.version}`,
550
+ },
551
+ partialFingerprints: {
552
+ 'primaryLocationLineHash': `${finding.package}:${finding.version}:${vuln.id}`,
553
+ },
554
+ properties: {
555
+ package: finding.package,
556
+ version: finding.version,
557
+ ecosystem: results.ecosystem,
558
+ isDirect: finding.isDirect,
559
+ severity: vuln.severity,
560
+ cvssScore: vuln.cvssScore,
561
+ cvssVector: vuln.cvssVector,
562
+ cwe: vuln.cwe,
563
+ aliases: vuln.aliases,
564
+ source: vuln.source,
565
+ affectedVersions: vuln.affectedVersions,
566
+ patchedVersions: vuln.patchedVersions,
567
+ references: vuln.references,
568
+ remediationPath: finding.remediationPath,
569
+ recommendedVersion: finding.recommendedVersion,
570
+ },
571
+ });
572
+ }
573
+ }
574
+ return {
575
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
576
+ version: '2.1.0',
577
+ runs: [{
578
+ tool: {
579
+ driver: {
580
+ name: 'guardrail-cli',
581
+ version,
582
+ informationUri: 'https://guardrail.dev',
583
+ rules: Array.from(ruleMap.values()),
584
+ },
585
+ },
586
+ results: sarifResults,
587
+ invocations: [{
588
+ executionSuccessful: true,
589
+ commandLine: `guardrail scan:vulnerabilities --path ${results.projectPath}`,
590
+ startTimeUtc: new Date().toISOString(),
591
+ workingDirectory: { uri: results.projectPath.replace(/\\/g, '/') },
592
+ }],
593
+ }],
594
+ };
595
+ }
596
+ /**
597
+ * Output OSV vulnerability results
598
+ */
599
+ function outputOSVVulnResults(results, options) {
600
+ if (options.format === 'json') {
601
+ console.log(JSON.stringify(results, null, 2));
602
+ return;
603
+ }
604
+ if (options.format === 'sarif') {
605
+ const sarif = toSarifVulnerabilitiesOSV(results);
606
+ console.log(JSON.stringify(sarif, null, 2));
607
+ return;
608
+ }
609
+ console.log(`\n ${c.info('Ecosystem:')} ${results.ecosystem}`);
610
+ console.log(` ${c.info('Packages scanned:')} ${results.packagesScanned}`);
611
+ console.log(` ${c.info('Lockfiles parsed:')} ${results.lockfilesParsed.join(', ') || 'none'}`);
612
+ console.log(` ${c.info('Cache hit rate:')} ${(results.cacheHitRate * 100).toFixed(1)}%`);
613
+ console.log(` ${c.info('NVD enrichment:')} ${results.nvdEnriched ? 'enabled' : 'disabled'}`);
614
+ console.log(` ${c.info('Scan duration:')} ${(results.scanDuration / 1000).toFixed(2)}s\n`);
615
+ const { summary } = results;
616
+ const total = summary.critical + summary.high + summary.medium + summary.low;
617
+ if (total === 0) {
618
+ console.log(` ${c.success('✓')} ${c.bold('No vulnerabilities found!')}\n`);
619
+ return;
620
+ }
621
+ console.log(` ${c.critical('CRITICAL')} ${summary.critical}`);
622
+ console.log(` ${c.high('HIGH')} ${summary.high}`);
623
+ console.log(` ${c.medium('MEDIUM')} ${summary.medium}`);
624
+ console.log(` ${c.low('LOW')} ${summary.low}\n`);
625
+ console.log(` ${c.info('Direct:')} ${results.directVulnerabilities} | ${c.info('Transitive:')} ${results.transitiveVulnerabilities}\n`);
626
+ // Group by direct vs transitive
627
+ const directFindings = results.findings.filter(f => f.isDirect);
628
+ const transitiveFindings = results.findings.filter(f => !f.isDirect);
629
+ if (directFindings.length > 0) {
630
+ console.log(`${c.bold(' DIRECT DEPENDENCIES:')}\n`);
631
+ outputFindingsList(directFindings);
632
+ }
633
+ if (transitiveFindings.length > 0) {
634
+ console.log(`\n${c.bold(' TRANSITIVE DEPENDENCIES:')}\n`);
635
+ outputFindingsList(transitiveFindings);
636
+ }
637
+ }
638
+ function outputFindingsList(findings) {
639
+ for (const finding of findings) {
640
+ for (const vuln of finding.vulnerabilities) {
641
+ const severityLabel = vuln.severity === 'critical' ? c.critical('CRITICAL') :
642
+ vuln.severity === 'high' ? c.high('HIGH') :
643
+ vuln.severity === 'medium' ? c.medium('MEDIUM') :
644
+ c.low('LOW');
645
+ console.log(` ${severityLabel} ${finding.package}@${finding.version}`);
646
+ console.log(` ${c.dim('├─')} ${c.info('ID:')} ${vuln.id}`);
647
+ console.log(` ${c.dim('├─')} ${c.info('Summary:')} ${vuln.title}`);
648
+ if (vuln.cvssScore) {
649
+ console.log(` ${c.dim('├─')} ${c.info('CVSS:')} ${vuln.cvssScore.toFixed(1)}${vuln.cvssVector ? ` (${vuln.cvssVector.substring(0, 30)}...)` : ''}`);
650
+ }
651
+ if (finding.remediationPath) {
652
+ const remed = finding.remediationPath;
653
+ const breakingLabel = remed.breakingChange ? c.medium(' [BREAKING]') : c.success(' [NON-BREAKING]');
654
+ console.log(` ${c.dim('└─')} ${c.info('Fix:')} ${remed.description}${breakingLabel}\n`);
655
+ }
656
+ else {
657
+ console.log(` ${c.dim('└─')} ${c.info('Fix:')} Upgrade to ${finding.recommendedVersion || 'latest'}\n`);
658
+ }
659
+ }
660
+ }
661
+ }
662
+ /**
663
+ * Register scan:vulnerabilities command with OSV integration
664
+ */
665
+ function registerScanVulnerabilitiesOSVCommand(program, requireAuth, printLogo) {
666
+ program
667
+ .command('scan:vulnerabilities')
668
+ .description('Scan dependencies for known vulnerabilities using OSV')
669
+ .option('-p, --path <path>', 'Project path to scan', '.')
670
+ .option('-f, --format <format>', 'Output format: table, json, sarif', 'table')
671
+ .option('-o, --output <file>', 'Output file path')
672
+ .option('--no-cache', 'Bypass cache and fetch fresh data from OSV')
673
+ .option('--nvd', 'Enable NVD enrichment for CVSS scores (slower)')
674
+ .option('--fail-on-critical', 'Exit with error if critical vulnerabilities found', false)
675
+ .option('--fail-on-high', 'Exit with error if high+ vulnerabilities found', false)
676
+ .option('--evidence', 'Generate signed evidence pack', false)
677
+ .option('--ecosystem <ecosystem>', 'Filter by ecosystem: npm, PyPI, RubyGems, Go')
678
+ .action(async (opts) => {
679
+ requireAuth();
680
+ printLogo();
681
+ console.log(`\n${c.bold('🛡️ VULNERABILITY SCAN (OSV Integration)')}\n`);
682
+ const projectPath = (0, path_1.resolve)(opts.path);
683
+ if (opts.noCache) {
684
+ console.log(` ${c.dim('Cache:')} disabled (--no-cache)\n`);
685
+ }
686
+ if (opts.nvd) {
687
+ console.log(` ${c.dim('NVD enrichment:')} enabled\n`);
688
+ }
689
+ const results = await scanVulnerabilitiesOSV(projectPath, {
690
+ noCache: opts.noCache,
691
+ nvd: opts.nvd,
692
+ ecosystem: opts.ecosystem,
693
+ });
694
+ console.log(`${c.success('✓')} Vulnerability scan complete`);
695
+ outputOSVVulnResults(results, opts);
696
+ // Write output file if specified
697
+ if (opts.output) {
698
+ const { writeFileSync } = require('fs');
699
+ const output = opts.format === 'sarif'
700
+ ? toSarifVulnerabilitiesOSV(results)
701
+ : results;
702
+ writeFileSync(opts.output, JSON.stringify(output, null, 2));
703
+ console.log(`\n ${c.success('✓')} Report written to ${opts.output}`);
704
+ }
705
+ if (opts.evidence) {
706
+ await (0, evidence_1.generateEvidence)('vulnerabilities', results, projectPath);
707
+ }
708
+ if (opts.failOnCritical && results.summary.critical > 0) {
709
+ (0, exit_codes_1.exitWith)(exit_codes_1.ExitCode.POLICY_FAIL, `${results.summary.critical} critical vulnerabilities found`);
710
+ }
711
+ if (opts.failOnHigh && (results.summary.critical + results.summary.high) > 0) {
712
+ (0, exit_codes_1.exitWith)(exit_codes_1.ExitCode.POLICY_FAIL, `${results.summary.critical + results.summary.high} high+ vulnerabilities found`);
713
+ }
714
+ });
715
+ }
716
+ //# sourceMappingURL=scan-vulnerabilities-osv.js.map