pnpm-catalog-updates 1.0.3 → 1.1.2
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/README.md +15 -0
- package/dist/index.js +22031 -10684
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/__tests__/commandRegistrar.test.ts +248 -0
- package/src/cli/commandRegistrar.ts +785 -0
- package/src/cli/commands/__tests__/aiCommand.test.ts +161 -0
- package/src/cli/commands/__tests__/analyzeCommand.test.ts +283 -0
- package/src/cli/commands/__tests__/checkCommand.test.ts +435 -0
- package/src/cli/commands/__tests__/graphCommand.test.ts +312 -0
- package/src/cli/commands/__tests__/initCommand.test.ts +317 -0
- package/src/cli/commands/__tests__/rollbackCommand.test.ts +400 -0
- package/src/cli/commands/__tests__/securityCommand.test.ts +467 -0
- package/src/cli/commands/__tests__/themeCommand.test.ts +166 -0
- package/src/cli/commands/__tests__/updateCommand.test.ts +720 -0
- package/src/cli/commands/__tests__/workspaceCommand.test.ts +286 -0
- package/src/cli/commands/aiCommand.ts +163 -0
- package/src/cli/commands/analyzeCommand.ts +219 -0
- package/src/cli/commands/checkCommand.ts +91 -98
- package/src/cli/commands/graphCommand.ts +475 -0
- package/src/cli/commands/initCommand.ts +64 -54
- package/src/cli/commands/rollbackCommand.ts +334 -0
- package/src/cli/commands/securityCommand.ts +165 -100
- package/src/cli/commands/themeCommand.ts +148 -0
- package/src/cli/commands/updateCommand.ts +215 -263
- package/src/cli/commands/workspaceCommand.ts +73 -0
- package/src/cli/constants/cliChoices.ts +93 -0
- package/src/cli/formatters/__tests__/__snapshots__/outputFormatter.test.ts.snap +557 -0
- package/src/cli/formatters/__tests__/ciFormatter.test.ts +526 -0
- package/src/cli/formatters/__tests__/outputFormatter.test.ts +448 -0
- package/src/cli/formatters/__tests__/progressBar.test.ts +709 -0
- package/src/cli/formatters/ciFormatter.ts +964 -0
- package/src/cli/formatters/colorUtils.ts +145 -0
- package/src/cli/formatters/outputFormatter.ts +615 -332
- package/src/cli/formatters/progressBar.ts +43 -52
- package/src/cli/formatters/versionFormatter.ts +132 -0
- package/src/cli/handlers/aiAnalysisHandler.ts +205 -0
- package/src/cli/handlers/changelogHandler.ts +113 -0
- package/src/cli/handlers/index.ts +9 -0
- package/src/cli/handlers/installHandler.ts +130 -0
- package/src/cli/index.ts +175 -726
- package/src/cli/interactive/InteractiveOptionsCollector.ts +387 -0
- package/src/cli/interactive/interactivePrompts.ts +189 -83
- package/src/cli/interactive/optionUtils.ts +89 -0
- package/src/cli/themes/colorTheme.ts +43 -16
- package/src/cli/utils/cliOutput.ts +118 -0
- package/src/cli/utils/commandHelpers.ts +249 -0
- package/src/cli/validators/commandValidator.ts +321 -336
- package/src/cli/validators/index.ts +37 -2
- package/src/cli/options/globalOptions.ts +0 -437
- package/src/cli/options/index.ts +0 -5
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI/CD Output Formatter
|
|
3
|
+
*
|
|
4
|
+
* Provides formatted output specifically designed for CI/CD pipelines.
|
|
5
|
+
* Supports GitHub Actions, GitLab CI, JUnit XML, and SARIF formats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OutdatedReport, UpdatePlan, UpdateResult, WorkspaceValidationReport } from '@pcu/core'
|
|
9
|
+
import { t } from '@pcu/utils'
|
|
10
|
+
import type { SecurityReport } from '../commands/securityCommand.js'
|
|
11
|
+
|
|
12
|
+
export type CIOutputFormat = 'github' | 'gitlab' | 'junit' | 'sarif'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GitHub Actions annotation levels
|
|
16
|
+
*/
|
|
17
|
+
type GitHubAnnotationLevel = 'error' | 'warning' | 'notice'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SARIF severity levels
|
|
21
|
+
*/
|
|
22
|
+
type SARIFLevel = 'error' | 'warning' | 'note' | 'none'
|
|
23
|
+
|
|
24
|
+
export class CIFormatter {
|
|
25
|
+
constructor(private readonly format: CIOutputFormat) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format outdated dependencies report for CI
|
|
29
|
+
*/
|
|
30
|
+
formatOutdatedReport(report: OutdatedReport): string {
|
|
31
|
+
switch (this.format) {
|
|
32
|
+
case 'github':
|
|
33
|
+
return this.formatOutdatedGitHub(report)
|
|
34
|
+
case 'gitlab':
|
|
35
|
+
return this.formatOutdatedGitLab(report)
|
|
36
|
+
case 'junit':
|
|
37
|
+
return this.formatOutdatedJUnit(report)
|
|
38
|
+
case 'sarif':
|
|
39
|
+
return this.formatOutdatedSARIF(report)
|
|
40
|
+
default:
|
|
41
|
+
return JSON.stringify(report, null, 2)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format update result for CI
|
|
47
|
+
*/
|
|
48
|
+
formatUpdateResult(result: UpdateResult): string {
|
|
49
|
+
switch (this.format) {
|
|
50
|
+
case 'github':
|
|
51
|
+
return this.formatUpdateResultGitHub(result)
|
|
52
|
+
case 'gitlab':
|
|
53
|
+
return this.formatUpdateResultGitLab(result)
|
|
54
|
+
case 'junit':
|
|
55
|
+
return this.formatUpdateResultJUnit(result)
|
|
56
|
+
case 'sarif':
|
|
57
|
+
return this.formatUpdateResultSARIF(result)
|
|
58
|
+
default:
|
|
59
|
+
return JSON.stringify(result, null, 2)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format update plan for CI
|
|
65
|
+
*/
|
|
66
|
+
formatUpdatePlan(plan: UpdatePlan): string {
|
|
67
|
+
switch (this.format) {
|
|
68
|
+
case 'github':
|
|
69
|
+
return this.formatUpdatePlanGitHub(plan)
|
|
70
|
+
case 'gitlab':
|
|
71
|
+
return this.formatUpdatePlanGitLab(plan)
|
|
72
|
+
case 'junit':
|
|
73
|
+
return this.formatUpdatePlanJUnit(plan)
|
|
74
|
+
case 'sarif':
|
|
75
|
+
return this.formatUpdatePlanSARIF(plan)
|
|
76
|
+
default:
|
|
77
|
+
return JSON.stringify(plan, null, 2)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format validation report for CI
|
|
83
|
+
*/
|
|
84
|
+
formatValidationReport(report: WorkspaceValidationReport): string {
|
|
85
|
+
switch (this.format) {
|
|
86
|
+
case 'github':
|
|
87
|
+
return this.formatValidationGitHub(report)
|
|
88
|
+
case 'gitlab':
|
|
89
|
+
return this.formatValidationGitLab(report)
|
|
90
|
+
case 'junit':
|
|
91
|
+
return this.formatValidationJUnit(report)
|
|
92
|
+
case 'sarif':
|
|
93
|
+
return this.formatValidationSARIF(report)
|
|
94
|
+
default:
|
|
95
|
+
return JSON.stringify(report, null, 2)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format security report for CI
|
|
101
|
+
*/
|
|
102
|
+
formatSecurityReport(report: SecurityReport): string {
|
|
103
|
+
switch (this.format) {
|
|
104
|
+
case 'github':
|
|
105
|
+
return this.formatSecurityGitHub(report)
|
|
106
|
+
case 'gitlab':
|
|
107
|
+
return this.formatSecurityGitLab(report)
|
|
108
|
+
case 'junit':
|
|
109
|
+
return this.formatSecurityJUnit(report)
|
|
110
|
+
case 'sarif':
|
|
111
|
+
return this.formatSecuritySARIF(report)
|
|
112
|
+
default:
|
|
113
|
+
return JSON.stringify(report, null, 2)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ==================== GitHub Actions Format ====================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create GitHub Actions annotation
|
|
121
|
+
* @see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
|
|
122
|
+
*/
|
|
123
|
+
private createGitHubAnnotation(
|
|
124
|
+
level: GitHubAnnotationLevel,
|
|
125
|
+
message: string,
|
|
126
|
+
options?: {
|
|
127
|
+
file?: string
|
|
128
|
+
line?: number
|
|
129
|
+
endLine?: number
|
|
130
|
+
col?: number
|
|
131
|
+
endColumn?: number
|
|
132
|
+
title?: string
|
|
133
|
+
}
|
|
134
|
+
): string {
|
|
135
|
+
const params: string[] = []
|
|
136
|
+
if (options?.file) params.push(`file=${options.file}`)
|
|
137
|
+
if (options?.line) params.push(`line=${options.line}`)
|
|
138
|
+
if (options?.endLine) params.push(`endLine=${options.endLine}`)
|
|
139
|
+
if (options?.col) params.push(`col=${options.col}`)
|
|
140
|
+
if (options?.endColumn) params.push(`endColumn=${options.endColumn}`)
|
|
141
|
+
if (options?.title) params.push(`title=${options.title}`)
|
|
142
|
+
|
|
143
|
+
const paramStr = params.length > 0 ? ` ${params.join(',')}` : ''
|
|
144
|
+
return `::${level}${paramStr}::${message.replace(/\n/g, '%0A')}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create GitHub Actions group
|
|
149
|
+
*/
|
|
150
|
+
private createGitHubGroup(name: string, content: string): string {
|
|
151
|
+
return `::group::${name}\n${content}\n::endgroup::`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private formatOutdatedGitHub(report: OutdatedReport): string {
|
|
155
|
+
const lines: string[] = []
|
|
156
|
+
|
|
157
|
+
if (!report.hasUpdates) {
|
|
158
|
+
lines.push(
|
|
159
|
+
this.createGitHubAnnotation('notice', t('format.allUpToDate'), { title: 'pcu check' })
|
|
160
|
+
)
|
|
161
|
+
return lines.join('\n')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Summary notice
|
|
165
|
+
lines.push(
|
|
166
|
+
this.createGitHubAnnotation(
|
|
167
|
+
'warning',
|
|
168
|
+
t('format.foundOutdated', { count: String(report.totalOutdated) }),
|
|
169
|
+
{ title: 'pcu check' }
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// Group outdated dependencies
|
|
174
|
+
const depLines: string[] = []
|
|
175
|
+
for (const catalog of report.catalogs) {
|
|
176
|
+
for (const dep of catalog.outdatedDependencies) {
|
|
177
|
+
const level: GitHubAnnotationLevel = dep.updateType === 'major' ? 'warning' : 'notice'
|
|
178
|
+
const securityNote = dep.isSecurityUpdate ? ' [SECURITY]' : ''
|
|
179
|
+
depLines.push(
|
|
180
|
+
this.createGitHubAnnotation(
|
|
181
|
+
level,
|
|
182
|
+
`${dep.packageName}: ${dep.currentVersion} → ${dep.latestVersion} (${dep.updateType})${securityNote}`,
|
|
183
|
+
{
|
|
184
|
+
file: 'pnpm-workspace.yaml',
|
|
185
|
+
title: `Outdated: ${dep.packageName}`,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
lines.push(this.createGitHubGroup('Outdated Dependencies', depLines.join('\n')))
|
|
192
|
+
|
|
193
|
+
return lines.join('\n')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private formatUpdateResultGitHub(result: UpdateResult): string {
|
|
197
|
+
const lines: string[] = []
|
|
198
|
+
|
|
199
|
+
if (result.success) {
|
|
200
|
+
lines.push(
|
|
201
|
+
this.createGitHubAnnotation(
|
|
202
|
+
'notice',
|
|
203
|
+
t('format.updatedCount', { count: String(result.totalUpdated) }),
|
|
204
|
+
{ title: 'pcu update' }
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
} else {
|
|
208
|
+
lines.push(
|
|
209
|
+
this.createGitHubAnnotation(
|
|
210
|
+
'error',
|
|
211
|
+
t('format.errorCount', { count: String(result.totalErrors) }),
|
|
212
|
+
{ title: 'pcu update' }
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Updated dependencies
|
|
218
|
+
if (result.updatedDependencies.length > 0) {
|
|
219
|
+
const updateLines = result.updatedDependencies.map(
|
|
220
|
+
(dep) => `${dep.packageName}: ${dep.fromVersion} → ${dep.toVersion}`
|
|
221
|
+
)
|
|
222
|
+
lines.push(this.createGitHubGroup('Updated Dependencies', updateLines.join('\n')))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Errors
|
|
226
|
+
for (const error of result.errors) {
|
|
227
|
+
lines.push(
|
|
228
|
+
this.createGitHubAnnotation('error', `${error.packageName}: ${error.error}`, {
|
|
229
|
+
file: 'pnpm-workspace.yaml',
|
|
230
|
+
title: 'Update Error',
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join('\n')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private formatUpdatePlanGitHub(plan: UpdatePlan): string {
|
|
239
|
+
const lines: string[] = []
|
|
240
|
+
|
|
241
|
+
if (plan.totalUpdates === 0) {
|
|
242
|
+
lines.push(
|
|
243
|
+
this.createGitHubAnnotation('notice', t('format.noUpdatesPlanned'), {
|
|
244
|
+
title: 'pcu update --dry-run',
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
return lines.join('\n')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push(
|
|
251
|
+
this.createGitHubAnnotation(
|
|
252
|
+
'notice',
|
|
253
|
+
t('format.plannedUpdates', { count: String(plan.totalUpdates) }),
|
|
254
|
+
{ title: 'pcu update --dry-run' }
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// Planned updates
|
|
259
|
+
const updateLines = plan.updates.map(
|
|
260
|
+
(update) =>
|
|
261
|
+
`${update.packageName}: ${update.currentVersion} → ${update.newVersion} (${update.updateType})`
|
|
262
|
+
)
|
|
263
|
+
lines.push(this.createGitHubGroup('Planned Updates', updateLines.join('\n')))
|
|
264
|
+
|
|
265
|
+
// Conflicts
|
|
266
|
+
if (plan.hasConflicts && plan.conflicts.length > 0) {
|
|
267
|
+
for (const conflict of plan.conflicts) {
|
|
268
|
+
lines.push(
|
|
269
|
+
this.createGitHubAnnotation('warning', `Version conflict: ${conflict.packageName}`, {
|
|
270
|
+
file: 'pnpm-workspace.yaml',
|
|
271
|
+
title: 'Version Conflict',
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return lines.join('\n')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private formatValidationGitHub(report: WorkspaceValidationReport): string {
|
|
281
|
+
const lines: string[] = []
|
|
282
|
+
|
|
283
|
+
if (report.isValid) {
|
|
284
|
+
lines.push(
|
|
285
|
+
this.createGitHubAnnotation(
|
|
286
|
+
'notice',
|
|
287
|
+
`${t('format.workspaceValidation')}: ${t('format.valid')}`,
|
|
288
|
+
{
|
|
289
|
+
title: 'pcu workspace --validate',
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
} else {
|
|
294
|
+
lines.push(
|
|
295
|
+
this.createGitHubAnnotation(
|
|
296
|
+
'error',
|
|
297
|
+
`${t('format.workspaceValidation')}: ${t('format.invalid')}`,
|
|
298
|
+
{
|
|
299
|
+
title: 'pcu workspace --validate',
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const error of report.errors) {
|
|
306
|
+
lines.push(this.createGitHubAnnotation('error', error, { title: 'Validation Error' }))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const warning of report.warnings) {
|
|
310
|
+
lines.push(this.createGitHubAnnotation('warning', warning, { title: 'Validation Warning' }))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return lines.join('\n')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private formatSecurityGitHub(report: SecurityReport): string {
|
|
317
|
+
const lines: string[] = []
|
|
318
|
+
|
|
319
|
+
const total = report.summary.totalVulnerabilities
|
|
320
|
+
if (total === 0) {
|
|
321
|
+
lines.push(
|
|
322
|
+
this.createGitHubAnnotation('notice', t('format.noVulnsFound'), { title: 'pcu security' })
|
|
323
|
+
)
|
|
324
|
+
return lines.join('\n')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Summary
|
|
328
|
+
const level: GitHubAnnotationLevel =
|
|
329
|
+
report.summary.critical > 0 || report.summary.high > 0 ? 'error' : 'warning'
|
|
330
|
+
lines.push(
|
|
331
|
+
this.createGitHubAnnotation(
|
|
332
|
+
level,
|
|
333
|
+
`Found ${total} vulnerabilities (${report.summary.critical} critical, ${report.summary.high} high)`,
|
|
334
|
+
{ title: 'pcu security' }
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
// Individual vulnerabilities
|
|
339
|
+
for (const vuln of report.vulnerabilities) {
|
|
340
|
+
const vulnLevel: GitHubAnnotationLevel =
|
|
341
|
+
vuln.severity === 'critical' || vuln.severity === 'high' ? 'error' : 'warning'
|
|
342
|
+
lines.push(
|
|
343
|
+
this.createGitHubAnnotation(vulnLevel, `${vuln.package}: ${vuln.title}`, {
|
|
344
|
+
title: `${vuln.severity.toUpperCase()}: ${vuln.package}`,
|
|
345
|
+
})
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return lines.join('\n')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ==================== GitLab CI Format ====================
|
|
353
|
+
|
|
354
|
+
private formatOutdatedGitLab(report: OutdatedReport): string {
|
|
355
|
+
// GitLab CI uses code quality report format for dependency issues
|
|
356
|
+
const issues = []
|
|
357
|
+
|
|
358
|
+
for (const catalog of report.catalogs) {
|
|
359
|
+
for (const dep of catalog.outdatedDependencies) {
|
|
360
|
+
issues.push({
|
|
361
|
+
description: `${dep.packageName} is outdated: ${dep.currentVersion} → ${dep.latestVersion}`,
|
|
362
|
+
check_name: 'outdated-dependency',
|
|
363
|
+
fingerprint: `outdated-${catalog.catalogName}-${dep.packageName}`,
|
|
364
|
+
severity:
|
|
365
|
+
dep.updateType === 'major' ? 'major' : dep.updateType === 'minor' ? 'minor' : 'info',
|
|
366
|
+
location: {
|
|
367
|
+
path: 'pnpm-workspace.yaml',
|
|
368
|
+
lines: { begin: 1 },
|
|
369
|
+
},
|
|
370
|
+
categories: ['Dependency'],
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return JSON.stringify(issues, null, 2)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private formatUpdateResultGitLab(result: UpdateResult): string {
|
|
379
|
+
const issues = []
|
|
380
|
+
|
|
381
|
+
for (const error of result.errors) {
|
|
382
|
+
issues.push({
|
|
383
|
+
description: `Update failed for ${error.packageName}: ${error.error}`,
|
|
384
|
+
check_name: 'update-error',
|
|
385
|
+
fingerprint: `update-error-${error.catalogName}-${error.packageName}`,
|
|
386
|
+
severity: error.fatal ? 'critical' : 'major',
|
|
387
|
+
location: {
|
|
388
|
+
path: 'pnpm-workspace.yaml',
|
|
389
|
+
lines: { begin: 1 },
|
|
390
|
+
},
|
|
391
|
+
categories: ['Dependency Update'],
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return JSON.stringify(issues, null, 2)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private formatUpdatePlanGitLab(plan: UpdatePlan): string {
|
|
399
|
+
const issues = []
|
|
400
|
+
|
|
401
|
+
for (const conflict of plan.conflicts) {
|
|
402
|
+
issues.push({
|
|
403
|
+
description: `Version conflict for ${conflict.packageName}`,
|
|
404
|
+
check_name: 'version-conflict',
|
|
405
|
+
fingerprint: `conflict-${conflict.packageName}`,
|
|
406
|
+
severity: 'major',
|
|
407
|
+
location: {
|
|
408
|
+
path: 'pnpm-workspace.yaml',
|
|
409
|
+
lines: { begin: 1 },
|
|
410
|
+
},
|
|
411
|
+
categories: ['Dependency'],
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return JSON.stringify(issues, null, 2)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private formatValidationGitLab(report: WorkspaceValidationReport): string {
|
|
419
|
+
const issues = []
|
|
420
|
+
|
|
421
|
+
for (const error of report.errors) {
|
|
422
|
+
issues.push({
|
|
423
|
+
description: error,
|
|
424
|
+
check_name: 'workspace-validation',
|
|
425
|
+
fingerprint: `validation-error-${Buffer.from(error).toString('base64').slice(0, 20)}`,
|
|
426
|
+
severity: 'critical',
|
|
427
|
+
location: {
|
|
428
|
+
path: 'pnpm-workspace.yaml',
|
|
429
|
+
lines: { begin: 1 },
|
|
430
|
+
},
|
|
431
|
+
categories: ['Workspace'],
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const warning of report.warnings) {
|
|
436
|
+
issues.push({
|
|
437
|
+
description: warning,
|
|
438
|
+
check_name: 'workspace-validation',
|
|
439
|
+
fingerprint: `validation-warning-${Buffer.from(warning).toString('base64').slice(0, 20)}`,
|
|
440
|
+
severity: 'minor',
|
|
441
|
+
location: {
|
|
442
|
+
path: 'pnpm-workspace.yaml',
|
|
443
|
+
lines: { begin: 1 },
|
|
444
|
+
},
|
|
445
|
+
categories: ['Workspace'],
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return JSON.stringify(issues, null, 2)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private formatSecurityGitLab(report: SecurityReport): string {
|
|
453
|
+
// GitLab Security Report format
|
|
454
|
+
const securityReport = {
|
|
455
|
+
version: '15.0.0',
|
|
456
|
+
vulnerabilities: report.vulnerabilities.map((vuln) => ({
|
|
457
|
+
id: vuln.id || `vuln-${vuln.package}-${Date.now()}`,
|
|
458
|
+
category: 'dependency_scanning',
|
|
459
|
+
name: vuln.title,
|
|
460
|
+
message: vuln.title,
|
|
461
|
+
description: vuln.overview || vuln.title,
|
|
462
|
+
severity: this.mapSeverityToGitLab(vuln.severity),
|
|
463
|
+
solution: vuln.fixAvailable
|
|
464
|
+
? typeof vuln.fixAvailable === 'string'
|
|
465
|
+
? `Update to ${vuln.fixAvailable}`
|
|
466
|
+
: 'Update available'
|
|
467
|
+
: 'No fix available',
|
|
468
|
+
scanner: {
|
|
469
|
+
id: 'pcu-security',
|
|
470
|
+
name: 'PCU Security Scanner',
|
|
471
|
+
},
|
|
472
|
+
location: {
|
|
473
|
+
file: 'pnpm-workspace.yaml',
|
|
474
|
+
dependency: {
|
|
475
|
+
package: { name: vuln.package },
|
|
476
|
+
version: vuln.installedVersion || 'unknown',
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
identifiers: vuln.cwe
|
|
480
|
+
? [
|
|
481
|
+
{
|
|
482
|
+
type: 'cwe',
|
|
483
|
+
name: `CWE-${vuln.cwe}`,
|
|
484
|
+
value: String(vuln.cwe),
|
|
485
|
+
},
|
|
486
|
+
]
|
|
487
|
+
: [],
|
|
488
|
+
links: vuln.url ? [{ url: vuln.url }] : [],
|
|
489
|
+
})),
|
|
490
|
+
scan: {
|
|
491
|
+
scanner: {
|
|
492
|
+
id: 'pcu-security',
|
|
493
|
+
name: 'PCU Security Scanner',
|
|
494
|
+
version: '1.0.0',
|
|
495
|
+
vendor: { name: 'PCU' },
|
|
496
|
+
},
|
|
497
|
+
type: 'dependency_scanning',
|
|
498
|
+
start_time: report.metadata.scanDate,
|
|
499
|
+
end_time: new Date().toISOString(),
|
|
500
|
+
status: 'success',
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return JSON.stringify(securityReport, null, 2)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private mapSeverityToGitLab(severity: string): string {
|
|
508
|
+
switch (severity.toLowerCase()) {
|
|
509
|
+
case 'critical':
|
|
510
|
+
return 'Critical'
|
|
511
|
+
case 'high':
|
|
512
|
+
return 'High'
|
|
513
|
+
case 'moderate':
|
|
514
|
+
case 'medium':
|
|
515
|
+
return 'Medium'
|
|
516
|
+
case 'low':
|
|
517
|
+
return 'Low'
|
|
518
|
+
default:
|
|
519
|
+
return 'Info'
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ==================== JUnit XML Format ====================
|
|
524
|
+
|
|
525
|
+
private escapeXml(str: string): string {
|
|
526
|
+
return str
|
|
527
|
+
.replace(/&/g, '&')
|
|
528
|
+
.replace(/</g, '<')
|
|
529
|
+
.replace(/>/g, '>')
|
|
530
|
+
.replace(/"/g, '"')
|
|
531
|
+
.replace(/'/g, ''')
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private formatOutdatedJUnit(report: OutdatedReport): string {
|
|
535
|
+
const testcases: string[] = []
|
|
536
|
+
let failures = 0
|
|
537
|
+
|
|
538
|
+
for (const catalog of report.catalogs) {
|
|
539
|
+
for (const dep of catalog.outdatedDependencies) {
|
|
540
|
+
const isFailure = dep.updateType === 'major' || dep.isSecurityUpdate
|
|
541
|
+
if (isFailure) failures++
|
|
542
|
+
|
|
543
|
+
const failureElement = isFailure
|
|
544
|
+
? `<failure message="${this.escapeXml(`${dep.packageName} needs ${dep.updateType} update`)}" type="OutdatedDependency">
|
|
545
|
+
Current: ${this.escapeXml(dep.currentVersion)}
|
|
546
|
+
Latest: ${this.escapeXml(dep.latestVersion)}
|
|
547
|
+
Type: ${dep.updateType}${dep.isSecurityUpdate ? '\nSecurity Update Required' : ''}
|
|
548
|
+
</failure>`
|
|
549
|
+
: ''
|
|
550
|
+
|
|
551
|
+
testcases.push(`
|
|
552
|
+
<testcase classname="pcu.check.${this.escapeXml(catalog.catalogName)}" name="${this.escapeXml(dep.packageName)}" time="0">
|
|
553
|
+
${failureElement}
|
|
554
|
+
</testcase>`)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
559
|
+
<testsuites name="pcu-check" tests="${report.totalOutdated}" failures="${failures}" errors="0" time="0">
|
|
560
|
+
<testsuite name="outdated-dependencies" tests="${report.totalOutdated}" failures="${failures}" errors="0">
|
|
561
|
+
${testcases.join('\n')}
|
|
562
|
+
</testsuite>
|
|
563
|
+
</testsuites>`
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private formatUpdateResultJUnit(result: UpdateResult): string {
|
|
567
|
+
const testcases: string[] = []
|
|
568
|
+
|
|
569
|
+
for (const dep of result.updatedDependencies) {
|
|
570
|
+
testcases.push(`
|
|
571
|
+
<testcase classname="pcu.update.${this.escapeXml(dep.catalogName)}" name="${this.escapeXml(dep.packageName)}" time="0">
|
|
572
|
+
</testcase>`)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
for (const error of result.errors) {
|
|
576
|
+
testcases.push(`
|
|
577
|
+
<testcase classname="pcu.update.${this.escapeXml(error.catalogName)}" name="${this.escapeXml(error.packageName)}" time="0">
|
|
578
|
+
<failure message="${this.escapeXml(error.error)}" type="UpdateError">${this.escapeXml(error.error)}</failure>
|
|
579
|
+
</testcase>`)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const total = result.updatedDependencies.length + result.errors.length
|
|
583
|
+
|
|
584
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
585
|
+
<testsuites name="pcu-update" tests="${total}" failures="${result.errors.length}" errors="0" time="0">
|
|
586
|
+
<testsuite name="dependency-updates" tests="${total}" failures="${result.errors.length}" errors="0">
|
|
587
|
+
${testcases.join('\n')}
|
|
588
|
+
</testsuite>
|
|
589
|
+
</testsuites>`
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private formatUpdatePlanJUnit(plan: UpdatePlan): string {
|
|
593
|
+
const testcases: string[] = []
|
|
594
|
+
let failures = 0
|
|
595
|
+
|
|
596
|
+
for (const update of plan.updates) {
|
|
597
|
+
const hasConflict = plan.conflicts.some((c) => c.packageName === update.packageName)
|
|
598
|
+
if (hasConflict) failures++
|
|
599
|
+
|
|
600
|
+
const failureElement = hasConflict
|
|
601
|
+
? `<failure message="Version conflict detected" type="VersionConflict">Multiple catalogs have different versions</failure>`
|
|
602
|
+
: ''
|
|
603
|
+
|
|
604
|
+
testcases.push(`
|
|
605
|
+
<testcase classname="pcu.plan.${this.escapeXml(update.catalogName)}" name="${this.escapeXml(update.packageName)}" time="0">
|
|
606
|
+
${failureElement}
|
|
607
|
+
</testcase>`)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
611
|
+
<testsuites name="pcu-update-plan" tests="${plan.totalUpdates}" failures="${failures}" errors="0" time="0">
|
|
612
|
+
<testsuite name="planned-updates" tests="${plan.totalUpdates}" failures="${failures}" errors="0">
|
|
613
|
+
${testcases.join('\n')}
|
|
614
|
+
</testsuite>
|
|
615
|
+
</testsuites>`
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private formatValidationJUnit(report: WorkspaceValidationReport): string {
|
|
619
|
+
const testcases: string[] = []
|
|
620
|
+
|
|
621
|
+
testcases.push(`
|
|
622
|
+
<testcase classname="pcu.validation" name="workspace-structure" time="0">
|
|
623
|
+
${!report.isValid ? `<failure message="Workspace validation failed" type="ValidationError">${this.escapeXml(report.errors.join('\n'))}</failure>` : ''}
|
|
624
|
+
</testcase>`)
|
|
625
|
+
|
|
626
|
+
for (const warning of report.warnings) {
|
|
627
|
+
testcases.push(`
|
|
628
|
+
<testcase classname="pcu.validation" name="warning" time="0">
|
|
629
|
+
<system-out>${this.escapeXml(warning)}</system-out>
|
|
630
|
+
</testcase>`)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
634
|
+
<testsuites name="pcu-validation" tests="${1 + report.warnings.length}" failures="${report.isValid ? 0 : 1}" errors="0" time="0">
|
|
635
|
+
<testsuite name="workspace-validation" tests="${1 + report.warnings.length}" failures="${report.isValid ? 0 : 1}" errors="0">
|
|
636
|
+
${testcases.join('\n')}
|
|
637
|
+
</testsuite>
|
|
638
|
+
</testsuites>`
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private formatSecurityJUnit(report: SecurityReport): string {
|
|
642
|
+
const testcases: string[] = []
|
|
643
|
+
|
|
644
|
+
for (const vuln of report.vulnerabilities) {
|
|
645
|
+
const severity = vuln.severity.toLowerCase()
|
|
646
|
+
const isFailure = severity === 'critical' || severity === 'high'
|
|
647
|
+
|
|
648
|
+
testcases.push(`
|
|
649
|
+
<testcase classname="pcu.security.${this.escapeXml(vuln.package)}" name="${this.escapeXml(vuln.title)}" time="0">
|
|
650
|
+
${
|
|
651
|
+
isFailure
|
|
652
|
+
? `<failure message="${this.escapeXml(vuln.title)}" type="SecurityVulnerability">
|
|
653
|
+
Severity: ${vuln.severity}
|
|
654
|
+
Package: ${this.escapeXml(vuln.package)}
|
|
655
|
+
${vuln.overview ? `Overview: ${this.escapeXml(vuln.overview)}` : ''}
|
|
656
|
+
${vuln.fixAvailable ? `Fix: ${typeof vuln.fixAvailable === 'string' ? vuln.fixAvailable : 'Available'}` : 'No fix available'}
|
|
657
|
+
</failure>`
|
|
658
|
+
: ''
|
|
659
|
+
}
|
|
660
|
+
</testcase>`)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const failures = report.vulnerabilities.filter(
|
|
664
|
+
(v) => v.severity === 'critical' || v.severity === 'high'
|
|
665
|
+
).length
|
|
666
|
+
|
|
667
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
668
|
+
<testsuites name="pcu-security" tests="${report.summary.totalVulnerabilities}" failures="${failures}" errors="0" time="0">
|
|
669
|
+
<testsuite name="security-vulnerabilities" tests="${report.summary.totalVulnerabilities}" failures="${failures}" errors="0">
|
|
670
|
+
${testcases.join('\n')}
|
|
671
|
+
</testsuite>
|
|
672
|
+
</testsuites>`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ==================== SARIF Format ====================
|
|
676
|
+
|
|
677
|
+
private mapSeverityToSARIF(severity: string): SARIFLevel {
|
|
678
|
+
switch (severity.toLowerCase()) {
|
|
679
|
+
case 'critical':
|
|
680
|
+
case 'high':
|
|
681
|
+
return 'error'
|
|
682
|
+
case 'moderate':
|
|
683
|
+
case 'medium':
|
|
684
|
+
return 'warning'
|
|
685
|
+
case 'low':
|
|
686
|
+
return 'note'
|
|
687
|
+
default:
|
|
688
|
+
return 'none'
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private formatOutdatedSARIF(report: OutdatedReport): string {
|
|
693
|
+
const results: object[] = []
|
|
694
|
+
const rules: object[] = []
|
|
695
|
+
const ruleIds = new Set<string>()
|
|
696
|
+
|
|
697
|
+
for (const catalog of report.catalogs) {
|
|
698
|
+
for (const dep of catalog.outdatedDependencies) {
|
|
699
|
+
const ruleId = `outdated-${dep.updateType}`
|
|
700
|
+
if (!ruleIds.has(ruleId)) {
|
|
701
|
+
ruleIds.add(ruleId)
|
|
702
|
+
rules.push({
|
|
703
|
+
id: ruleId,
|
|
704
|
+
name: `Outdated${dep.updateType.charAt(0).toUpperCase() + dep.updateType.slice(1)}Dependency`,
|
|
705
|
+
shortDescription: { text: `Outdated ${dep.updateType} dependency` },
|
|
706
|
+
fullDescription: { text: `A dependency has an outdated ${dep.updateType} version` },
|
|
707
|
+
defaultConfiguration: {
|
|
708
|
+
level: dep.updateType === 'major' ? 'warning' : 'note',
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
results.push({
|
|
714
|
+
ruleId,
|
|
715
|
+
level: dep.updateType === 'major' ? 'warning' : 'note',
|
|
716
|
+
message: {
|
|
717
|
+
text: `${dep.packageName} is outdated: ${dep.currentVersion} → ${dep.latestVersion}`,
|
|
718
|
+
},
|
|
719
|
+
locations: [
|
|
720
|
+
{
|
|
721
|
+
physicalLocation: {
|
|
722
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
723
|
+
region: { startLine: 1 },
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
properties: {
|
|
728
|
+
packageName: dep.packageName,
|
|
729
|
+
currentVersion: dep.currentVersion,
|
|
730
|
+
latestVersion: dep.latestVersion,
|
|
731
|
+
updateType: dep.updateType,
|
|
732
|
+
isSecurityUpdate: dep.isSecurityUpdate,
|
|
733
|
+
catalog: catalog.catalogName,
|
|
734
|
+
},
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return this.createSARIFDocument('pcu-check', rules, results)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private formatUpdateResultSARIF(result: UpdateResult): string {
|
|
743
|
+
const results: object[] = []
|
|
744
|
+
const rules: object[] = [
|
|
745
|
+
{
|
|
746
|
+
id: 'update-success',
|
|
747
|
+
name: 'DependencyUpdateSuccess',
|
|
748
|
+
shortDescription: { text: 'Dependency updated successfully' },
|
|
749
|
+
defaultConfiguration: { level: 'note' },
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
id: 'update-error',
|
|
753
|
+
name: 'DependencyUpdateError',
|
|
754
|
+
shortDescription: { text: 'Dependency update failed' },
|
|
755
|
+
defaultConfiguration: { level: 'error' },
|
|
756
|
+
},
|
|
757
|
+
]
|
|
758
|
+
|
|
759
|
+
for (const dep of result.updatedDependencies) {
|
|
760
|
+
results.push({
|
|
761
|
+
ruleId: 'update-success',
|
|
762
|
+
level: 'note',
|
|
763
|
+
message: { text: `${dep.packageName} updated: ${dep.fromVersion} → ${dep.toVersion}` },
|
|
764
|
+
locations: [
|
|
765
|
+
{
|
|
766
|
+
physicalLocation: {
|
|
767
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
768
|
+
region: { startLine: 1 },
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
for (const error of result.errors) {
|
|
776
|
+
results.push({
|
|
777
|
+
ruleId: 'update-error',
|
|
778
|
+
level: 'error',
|
|
779
|
+
message: { text: `Failed to update ${error.packageName}: ${error.error}` },
|
|
780
|
+
locations: [
|
|
781
|
+
{
|
|
782
|
+
physicalLocation: {
|
|
783
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
784
|
+
region: { startLine: 1 },
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return this.createSARIFDocument('pcu-update', rules, results)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private formatUpdatePlanSARIF(plan: UpdatePlan): string {
|
|
795
|
+
const results: object[] = []
|
|
796
|
+
const rules: object[] = [
|
|
797
|
+
{
|
|
798
|
+
id: 'planned-update',
|
|
799
|
+
name: 'PlannedDependencyUpdate',
|
|
800
|
+
shortDescription: { text: 'Dependency update planned' },
|
|
801
|
+
defaultConfiguration: { level: 'note' },
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
id: 'version-conflict',
|
|
805
|
+
name: 'VersionConflict',
|
|
806
|
+
shortDescription: { text: 'Version conflict detected' },
|
|
807
|
+
defaultConfiguration: { level: 'warning' },
|
|
808
|
+
},
|
|
809
|
+
]
|
|
810
|
+
|
|
811
|
+
for (const update of plan.updates) {
|
|
812
|
+
results.push({
|
|
813
|
+
ruleId: 'planned-update',
|
|
814
|
+
level: 'note',
|
|
815
|
+
message: {
|
|
816
|
+
text: `${update.packageName}: ${update.currentVersion} → ${update.newVersion} (${update.updateType})`,
|
|
817
|
+
},
|
|
818
|
+
locations: [
|
|
819
|
+
{
|
|
820
|
+
physicalLocation: {
|
|
821
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
822
|
+
region: { startLine: 1 },
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
for (const conflict of plan.conflicts) {
|
|
830
|
+
results.push({
|
|
831
|
+
ruleId: 'version-conflict',
|
|
832
|
+
level: 'warning',
|
|
833
|
+
message: { text: `Version conflict for ${conflict.packageName}` },
|
|
834
|
+
locations: [
|
|
835
|
+
{
|
|
836
|
+
physicalLocation: {
|
|
837
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
838
|
+
region: { startLine: 1 },
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return this.createSARIFDocument('pcu-update-plan', rules, results)
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private formatValidationSARIF(report: WorkspaceValidationReport): string {
|
|
849
|
+
const results: object[] = []
|
|
850
|
+
const rules: object[] = [
|
|
851
|
+
{
|
|
852
|
+
id: 'validation-error',
|
|
853
|
+
name: 'WorkspaceValidationError',
|
|
854
|
+
shortDescription: { text: 'Workspace validation error' },
|
|
855
|
+
defaultConfiguration: { level: 'error' },
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
id: 'validation-warning',
|
|
859
|
+
name: 'WorkspaceValidationWarning',
|
|
860
|
+
shortDescription: { text: 'Workspace validation warning' },
|
|
861
|
+
defaultConfiguration: { level: 'warning' },
|
|
862
|
+
},
|
|
863
|
+
]
|
|
864
|
+
|
|
865
|
+
for (const error of report.errors) {
|
|
866
|
+
results.push({
|
|
867
|
+
ruleId: 'validation-error',
|
|
868
|
+
level: 'error',
|
|
869
|
+
message: { text: error },
|
|
870
|
+
locations: [
|
|
871
|
+
{
|
|
872
|
+
physicalLocation: {
|
|
873
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
874
|
+
region: { startLine: 1 },
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
],
|
|
878
|
+
})
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
for (const warning of report.warnings) {
|
|
882
|
+
results.push({
|
|
883
|
+
ruleId: 'validation-warning',
|
|
884
|
+
level: 'warning',
|
|
885
|
+
message: { text: warning },
|
|
886
|
+
locations: [
|
|
887
|
+
{
|
|
888
|
+
physicalLocation: {
|
|
889
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
890
|
+
region: { startLine: 1 },
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
})
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return this.createSARIFDocument('pcu-validation', rules, results)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private formatSecuritySARIF(report: SecurityReport): string {
|
|
901
|
+
const results: object[] = []
|
|
902
|
+
const rules: object[] = []
|
|
903
|
+
const ruleIds = new Set<string>()
|
|
904
|
+
|
|
905
|
+
for (const vuln of report.vulnerabilities) {
|
|
906
|
+
const ruleId = `security-${vuln.severity.toLowerCase()}`
|
|
907
|
+
if (!ruleIds.has(ruleId)) {
|
|
908
|
+
ruleIds.add(ruleId)
|
|
909
|
+
rules.push({
|
|
910
|
+
id: ruleId,
|
|
911
|
+
name: `Security${vuln.severity.charAt(0).toUpperCase() + vuln.severity.slice(1)}`,
|
|
912
|
+
shortDescription: { text: `${vuln.severity} severity vulnerability` },
|
|
913
|
+
defaultConfiguration: { level: this.mapSeverityToSARIF(vuln.severity) },
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
results.push({
|
|
918
|
+
ruleId,
|
|
919
|
+
level: this.mapSeverityToSARIF(vuln.severity),
|
|
920
|
+
message: { text: `${vuln.package}: ${vuln.title}` },
|
|
921
|
+
locations: [
|
|
922
|
+
{
|
|
923
|
+
physicalLocation: {
|
|
924
|
+
artifactLocation: { uri: 'pnpm-workspace.yaml' },
|
|
925
|
+
region: { startLine: 1 },
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
properties: {
|
|
930
|
+
package: vuln.package,
|
|
931
|
+
severity: vuln.severity,
|
|
932
|
+
cwe: vuln.cwe,
|
|
933
|
+
fixAvailable: vuln.fixAvailable,
|
|
934
|
+
url: vuln.url,
|
|
935
|
+
},
|
|
936
|
+
})
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return this.createSARIFDocument('pcu-security', rules, results)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private createSARIFDocument(toolName: string, rules: object[], results: object[]): string {
|
|
943
|
+
const sarif = {
|
|
944
|
+
$schema:
|
|
945
|
+
'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
946
|
+
version: '2.1.0',
|
|
947
|
+
runs: [
|
|
948
|
+
{
|
|
949
|
+
tool: {
|
|
950
|
+
driver: {
|
|
951
|
+
name: toolName,
|
|
952
|
+
informationUri: 'https://github.com/user/pnpm-catalog-updates',
|
|
953
|
+
version: '1.0.0',
|
|
954
|
+
rules,
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
results,
|
|
958
|
+
},
|
|
959
|
+
],
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return JSON.stringify(sarif, null, 2)
|
|
963
|
+
}
|
|
964
|
+
}
|