@@ -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
345399func 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+
349420func 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