Skip to content

Commit 3fc3abc

Browse files
committed
feat(cli): show assessment status on active violations too, drop "[no fix]"
Assessment statuses other than NOT_AFFECTED/FIXED (AFFECTED, UNDER_INVESTIGATION, etc.) are equally meaningful on findings that remain active. Surface them in the same severity-parens regardless of the suppress flag so active rows can read e.g. "CVE-X (HIGH, AFFECTED) libc6@2.41" or "CVE-X (HIGH, UNDER_INVESTIGATION) libc6@2.41". Suppressed findings without an explicit assessment fall back to the literal "suppressed" tag (unchanged for existing tests). Coloring still red/yellow by suppress flag — assessment status is metadata, gate state is independent. Also drop "[no fix]" — the absence of "[fix: vN]" already conveys it and shaves a column. Extracted violationAssessment helper centralizing the Vulnerability/Sast/LicenseViolation dispatch (replaces suppressedStatus). joinTag inlined to direct conditional. 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 86b1ad8 commit 3fc3abc

2 files changed

Lines changed: 68 additions & 43 deletions

File tree

app/cli/cmd/workflow_workflow_run_describe.go

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/chainloop-dev/chainloop/app/cli/cmd/output"
2929
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
30+
attv1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
3031
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
3132
"github.com/jedib0t/go-pretty/v6/table"
3233
"github.com/jedib0t/go-pretty/v6/text"
@@ -295,40 +296,34 @@ func policiesTable(evs []*action.PolicyEvaluation, mt table.Writer, debugMode bo
295296
// structured finding when present (CVE id + severity + package + fix info,
296297
// or SAST rule + location, or license + component). Falls back to the first
297298
// line of Message — vuln policies emit a multi-line markdown report there
298-
// which would otherwise break the row layout. Suppressed entries get the
299-
// resolved assessment status appended inside the same severity-parens so
300-
// the row reads "CVE-X (HIGH, NOT_AFFECTED) …".
299+
// which would otherwise break the row layout. The resolved assessment
300+
// status, if any, is appended inside the same severity-parens regardless
301+
// of suppression so AFFECTED / UNDER_INVESTIGATION / etc. surface on
302+
// active findings too.
301303
func violationSummary(v *action.PolicyViolation) string {
302-
suppressTag := ""
303-
if v.Suppress {
304-
suppressTag = "suppressed"
305-
if s := suppressedStatus(v); s != "" {
306-
suppressTag = s
307-
}
304+
statusTag := prettyAssessmentStatus(violationAssessment(v).GetEffectiveStatus())
305+
if statusTag == "" && v.Suppress {
306+
statusTag = "suppressed"
308307
}
309308

310309
switch {
311310
case v.Vulnerability != nil:
312311
f := v.Vulnerability
313312
head := f.GetExternalId()
314-
if tag := joinTag(f.GetSeverity(), suppressTag); tag != "" {
313+
if tag := joinTag(f.GetSeverity(), statusTag); tag != "" {
315314
head = fmt.Sprintf("%s (%s)", head, tag)
316315
}
317-
parts := []string{head}
318316
if pkg := prettyPurl(f.GetPackagePurl()); pkg != "" {
319-
parts = append(parts, pkg)
317+
head += " " + pkg
320318
}
321-
out := strings.Join(parts, " ")
322319
if fix := f.GetFixedVersion(); fix != "" {
323-
out += " [fix: " + fix + "]"
324-
} else {
325-
out += " [no fix]"
320+
head += " [fix: " + fix + "]"
326321
}
327-
return out
322+
return head
328323
case v.Sast != nil:
329324
f := v.Sast
330325
out := f.GetRuleId()
331-
if tag := joinTag(f.GetSeverity(), suppressTag); tag != "" {
326+
if tag := joinTag(f.GetSeverity(), statusTag); tag != "" {
332327
out = fmt.Sprintf("%s (%s)", out, tag)
333328
}
334329
if loc := f.GetLocation(); loc != "" {
@@ -342,8 +337,8 @@ func violationSummary(v *action.PolicyViolation) string {
342337
case v.LicenseViolation != nil:
343338
f := v.LicenseViolation
344339
out := f.GetLicenseId()
345-
if suppressTag != "" {
346-
out = fmt.Sprintf("%s (%s)", out, suppressTag)
340+
if statusTag != "" {
341+
out = fmt.Sprintf("%s (%s)", out, statusTag)
347342
}
348343
if c := f.GetComponentName(); c != "" {
349344
if ver := f.GetComponentVersion(); ver != "" {
@@ -358,36 +353,38 @@ func violationSummary(v *action.PolicyViolation) string {
358353
if head == "" {
359354
head = v.Subject
360355
}
361-
if suppressTag != "" {
362-
head = fmt.Sprintf("%s (%s)", head, suppressTag)
356+
if statusTag != "" {
357+
head = fmt.Sprintf("%s (%s)", head, statusTag)
363358
}
364359
return head
365360
}
366361

367-
// joinTag combines severity and the suppression tag (assessment status when
368-
// available, "suppressed" otherwise) into the comma-separated parenthetical
369-
// shown after the finding id. Empty inputs are skipped.
370-
func joinTag(severity, suppress string) string {
371-
var parts []string
372-
if severity != "" {
373-
parts = append(parts, strings.ToUpper(severity))
374-
}
375-
if suppress != "" {
376-
parts = append(parts, suppress)
362+
// joinTag combines severity and the assessment status into the
363+
// comma-separated parenthetical shown after the finding id.
364+
func joinTag(severity, status string) string {
365+
sev := strings.ToUpper(severity)
366+
switch {
367+
case sev != "" && status != "":
368+
return sev + ", " + status
369+
case sev != "":
370+
return sev
371+
default:
372+
return status
377373
}
378-
return strings.Join(parts, ", ")
379374
}
380375

381-
func suppressedStatus(v *action.PolicyViolation) string {
376+
// violationAssessment returns the assessment attached to whichever structured
377+
// finding the violation carries, or nil. Centralizes the type dispatch.
378+
func violationAssessment(v *action.PolicyViolation) *attv1.PolicyAssessmentResult {
382379
switch {
383380
case v.Vulnerability != nil:
384-
return prettyAssessmentStatus(v.Vulnerability.GetAssessment().GetEffectiveStatus())
381+
return v.Vulnerability.GetAssessment()
385382
case v.Sast != nil:
386-
return prettyAssessmentStatus(v.Sast.GetAssessment().GetEffectiveStatus())
383+
return v.Sast.GetAssessment()
387384
case v.LicenseViolation != nil:
388-
return prettyAssessmentStatus(v.LicenseViolation.GetAssessment().GetEffectiveStatus())
385+
return v.LicenseViolation.GetAssessment()
389386
}
390-
return ""
387+
return nil
391388
}
392389

393390
func prettyAssessmentStatus(s string) string {

app/cli/cmd/workflow_workflow_run_describe_test.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ func TestViolationSummary(t *testing.T) {
5656
want string
5757
}{
5858
{
59-
name: "vulnerability with severity, package, no fix",
59+
name: "vulnerability with severity and package, no fix",
6060
violation: &action.PolicyViolation{
6161
Vulnerability: &attv1.PolicyVulnerabilityFinding{
6262
ExternalId: "CVE-2026-5450",
6363
Severity: "critical",
6464
PackagePurl: "pkg:deb/debian/libc6@2.41-12%2Bdeb13u2",
6565
},
6666
},
67-
want: "CVE-2026-5450 (CRITICAL) libc6@2.41-12+deb13u2 [no fix]",
67+
want: "CVE-2026-5450 (CRITICAL) libc6@2.41-12+deb13u2",
6868
},
6969
{
7070
name: "vulnerability with fix version",
@@ -78,6 +78,34 @@ func TestViolationSummary(t *testing.T) {
7878
},
7979
want: "CVE-2024-9999 (HIGH) lib@v1.0.0 [fix: 1.0.1]",
8080
},
81+
{
82+
name: "active vulnerability surfaces assessment status (AFFECTED)",
83+
violation: &action.PolicyViolation{
84+
Vulnerability: &attv1.PolicyVulnerabilityFinding{
85+
ExternalId: "CVE-2024-1",
86+
Severity: "high",
87+
PackagePurl: "pkg:deb/debian/libc6@2.41",
88+
Assessment: &attv1.PolicyAssessmentResult{
89+
EffectiveStatus: "ASSESSMENT_STATUS_AFFECTED",
90+
},
91+
},
92+
},
93+
want: "CVE-2024-1 (HIGH, AFFECTED) libc6@2.41",
94+
},
95+
{
96+
name: "active vulnerability surfaces assessment status (UNDER_INVESTIGATION)",
97+
violation: &action.PolicyViolation{
98+
Vulnerability: &attv1.PolicyVulnerabilityFinding{
99+
ExternalId: "CVE-2024-2",
100+
Severity: "medium",
101+
PackagePurl: "pkg:deb/debian/libc6@2.41",
102+
Assessment: &attv1.PolicyAssessmentResult{
103+
EffectiveStatus: "ASSESSMENT_STATUS_UNDER_INVESTIGATION",
104+
},
105+
},
106+
},
107+
want: "CVE-2024-2 (MEDIUM, UNDER_INVESTIGATION) libc6@2.41",
108+
},
81109
{
82110
name: "suppressed vulnerability gets assessment status inline",
83111
violation: &action.PolicyViolation{
@@ -91,7 +119,7 @@ func TestViolationSummary(t *testing.T) {
91119
},
92120
},
93121
},
94-
want: "CVE-2018-XXXX (LOW, NOT_AFFECTED) libc6@2.41 [no fix]",
122+
want: "CVE-2018-XXXX (LOW, NOT_AFFECTED) libc6@2.41",
95123
},
96124
{
97125
name: "SAST with location and line number",
@@ -153,12 +181,12 @@ func TestViolationSummary(t *testing.T) {
153181
name: "vulnerability with purl qualifiers strips them",
154182
violation: &action.PolicyViolation{
155183
Vulnerability: &attv1.PolicyVulnerabilityFinding{
156-
ExternalId: "CVE-2024-1",
184+
ExternalId: "CVE-2024-3",
157185
Severity: "high",
158186
PackagePurl: "pkg:deb/debian/libc6@2.41?arch=amd64",
159187
},
160188
},
161-
want: "CVE-2024-1 (HIGH) libc6@2.41 [no fix]",
189+
want: "CVE-2024-3 (HIGH) libc6@2.41",
162190
},
163191
}
164192

0 commit comments

Comments
 (0)