Skip to content

Commit 66b8770

Browse files
committed
fix(cli): compact, structured rendering for active vulnerability findings
Vuln policies emit a multi-line markdown report in violation.Message ("CVE-X\nSeverity: …\nPackage: …\n…"). Rendering that raw inside the " - " bulleted list left every continuation line unindented and forced the policy cell to grow vertically — same root cause as the suppressed issue, now fixed for active too. Active violations now render as one line each using the structured finding: - Vulnerability: "CVE-X (SEVERITY) name@version [fix: vN]" / "[no fix]" - SAST: "rule-id (SEVERITY) at file:line" - License: "license-id — component@version" With a fallback to Message's first line for unstructured policies. Also split the helpers: violationSummary (active, rich), violationID (suppressed, just the id), suppressedStatus (assessment), prettyPurl (shortens "pkg:type/ns/name@v?qual" → "name@v"). renderSuppressed reuses violationID so the two paths stay aligned. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev> Chainloop-Trace-Sessions: 5bd2a917-fb7b-400c-9772-60ba6af6c9af, b66717f5-626e-4c20-8d33-59c129b5885d
1 parent cb643ea commit 66b8770

1 file changed

Lines changed: 101 additions & 30 deletions

File tree

app/cli/cmd/workflow_workflow_run_describe.go

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,13 @@ func policiesTable(evs []*action.PolicyEvaluation, mt table.Writer, debugMode bo
253253
for _, ev := range evs {
254254
msg := ""
255255

256-
var active []string
257-
var suppressed []*action.PolicyViolation
256+
var active, suppressed []*action.PolicyViolation
258257
for _, v := range ev.Violations {
259258
if v.Suppress {
260259
suppressed = append(suppressed, v)
261-
continue
260+
} else {
261+
active = append(active, v)
262262
}
263-
active = append(active, v.Message)
264263
}
265264

266265
switch {
@@ -278,18 +277,15 @@ func policiesTable(evs []*action.PolicyEvaluation, mt table.Writer, debugMode bo
278277
msg = text.Colors{text.FgHiGreen}.Sprint("Ok")
279278
default:
280279
color := text.Colors{text.FgHiRed}
281-
var prefix = ""
282-
// For multiple violations, we want to indent the list
283-
if len(active) > 1 {
284-
prefix = "\n - "
280+
lines := make([]string, 0, len(active))
281+
for _, v := range active {
282+
lines = append(lines, color.Sprint(violationSummary(v)))
285283
}
286-
287-
// Color the violations text before joining
288-
for i, v := range active {
289-
active[i] = color.Sprint(v)
284+
if len(lines) == 1 {
285+
msg = lines[0]
286+
} else {
287+
msg = "\n - " + strings.Join(lines, "\n - ")
290288
}
291-
292-
msg = prefix + strings.Join(active, prefix)
293289
}
294290

295291
if s := renderSuppressed(suppressed); s != "" {
@@ -310,8 +306,8 @@ func renderSuppressed(suppressed []*action.PolicyViolation) string {
310306
}
311307
parts := make([]string, 0, len(suppressed))
312308
for _, v := range suppressed {
313-
id, status := suppressedIdentity(v)
314-
if status != "" {
309+
id := violationID(v)
310+
if status := suppressedStatus(v); status != "" {
315311
parts = append(parts, fmt.Sprintf("%s (%s)", id, status))
316312
} else {
317313
parts = append(parts, id)
@@ -320,32 +316,107 @@ func renderSuppressed(suppressed []*action.PolicyViolation) string {
320316
return text.Colors{text.FgHiYellow}.Sprintf("Suppressed (%d): %s", len(suppressed), strings.Join(parts, ", "))
321317
}
322318

323-
// suppressedIdentity returns a compact label for a suppressed violation plus
324-
// its precedence-resolved assessment status (no scope — keep the row tight).
325-
// Prefers the structured finding's identifier over Message; vuln policies
326-
// emit a multi-line markdown blob in Message that breaks row layout.
327-
func suppressedIdentity(v *action.PolicyViolation) (id, status string) {
319+
// violationSummary builds a single-line description of an active violation
320+
// using the structured finding when present: vulnerability gets severity,
321+
// package, and fix info; SAST gets location; license gets component. Falls
322+
// back to the first line of Message for unstructured policies — vuln
323+
// policies typically emit a multi-line markdown report in Message that
324+
// would otherwise break the row layout.
325+
func violationSummary(v *action.PolicyViolation) string {
326+
switch {
327+
case v.Vulnerability != nil:
328+
f := v.Vulnerability
329+
parts := []string{f.GetExternalId()}
330+
if s := f.GetSeverity(); s != "" {
331+
parts[0] = fmt.Sprintf("%s (%s)", f.GetExternalId(), strings.ToUpper(s))
332+
}
333+
if pkg := prettyPurl(f.GetPackagePurl()); pkg != "" {
334+
parts = append(parts, pkg)
335+
}
336+
out := strings.Join(parts, " ")
337+
if fix := f.GetFixedVersion(); fix != "" {
338+
out += " [fix: " + fix + "]"
339+
} else {
340+
out += " [no fix]"
341+
}
342+
return out
343+
case v.Sast != nil:
344+
f := v.Sast
345+
out := f.GetRuleId()
346+
if s := f.GetSeverity(); s != "" {
347+
out = fmt.Sprintf("%s (%s)", out, strings.ToUpper(s))
348+
}
349+
if loc := f.GetLocation(); loc != "" {
350+
if ln := f.GetLineNumber(); ln > 0 {
351+
out = fmt.Sprintf("%s at %s:%d", out, loc, ln)
352+
} else {
353+
out = fmt.Sprintf("%s at %s", out, loc)
354+
}
355+
}
356+
return out
357+
case v.LicenseViolation != nil:
358+
f := v.LicenseViolation
359+
out := f.GetLicenseId()
360+
if c := f.GetComponentName(); c != "" {
361+
if ver := f.GetComponentVersion(); ver != "" {
362+
out = fmt.Sprintf("%s — %s@%s", out, c, ver)
363+
} else {
364+
out = fmt.Sprintf("%s — %s", out, c)
365+
}
366+
}
367+
return out
368+
}
369+
return strings.SplitN(strings.TrimSpace(v.Message), "\n", 2)[0]
370+
}
371+
372+
// violationID returns just the identifier portion (CVE id, rule id, license
373+
// id) — used for the compact suppressed list where the assessment status
374+
// already carries the qualifying context.
375+
func violationID(v *action.PolicyViolation) string {
328376
switch {
329377
case v.Vulnerability != nil:
330-
id = v.Vulnerability.GetExternalId()
331-
status = prettyAssessmentStatus(v.Vulnerability.GetAssessment().GetEffectiveStatus())
378+
return v.Vulnerability.GetExternalId()
332379
case v.Sast != nil:
333-
id = v.Sast.GetRuleId()
334-
status = prettyAssessmentStatus(v.Sast.GetAssessment().GetEffectiveStatus())
380+
return v.Sast.GetRuleId()
335381
case v.LicenseViolation != nil:
336-
id = v.LicenseViolation.GetLicenseId()
337-
status = prettyAssessmentStatus(v.LicenseViolation.GetAssessment().GetEffectiveStatus())
382+
return v.LicenseViolation.GetLicenseId()
338383
}
339-
if id == "" {
340-
id = strings.SplitN(strings.TrimSpace(v.Message), "\n", 2)[0]
384+
return strings.SplitN(strings.TrimSpace(v.Message), "\n", 2)[0]
385+
}
386+
387+
func suppressedStatus(v *action.PolicyViolation) string {
388+
switch {
389+
case v.Vulnerability != nil:
390+
return prettyAssessmentStatus(v.Vulnerability.GetAssessment().GetEffectiveStatus())
391+
case v.Sast != nil:
392+
return prettyAssessmentStatus(v.Sast.GetAssessment().GetEffectiveStatus())
393+
case v.LicenseViolation != nil:
394+
return prettyAssessmentStatus(v.LicenseViolation.GetAssessment().GetEffectiveStatus())
341395
}
342-
return id, status
396+
return ""
343397
}
344398

345399
func prettyAssessmentStatus(s string) string {
346400
return strings.TrimPrefix(s, "ASSESSMENT_STATUS_")
347401
}
348402

403+
// prettyPurl shortens "pkg:type/[namespace/]name@version[?qualifiers][#sub]"
404+
// to "name@version" — the rest is rarely useful in a one-line summary and
405+
// has the most variability across ecosystems.
406+
func prettyPurl(purl string) string {
407+
if purl == "" {
408+
return ""
409+
}
410+
s := strings.TrimPrefix(purl, "pkg:")
411+
if i := strings.IndexAny(s, "?#"); i >= 0 {
412+
s = s[:i]
413+
}
414+
if i := strings.LastIndex(s, "/"); i >= 0 {
415+
s = s[i+1:]
416+
}
417+
return s
418+
}
419+
349420
func encodeAttestationOutput(run *action.WorkflowRunItemFull, writer io.Writer) error {
350421
// Try to encode as a table or json
351422
err := output.EncodeOutput(flagOutputFormat, run, workflowRunDescribeTableOutput)

0 commit comments

Comments
 (0)