@@ -585,6 +585,45 @@ type AllSectionsData struct {
585585 FallbackResults []validate_utils.Result
586586}
587587
588+ // ComponentResultsDisplay holds all formatted display data for component results
589+ type ComponentResultsDisplay struct {
590+ Header HeaderDisplay
591+ Result ResultDisplay
592+ VSASummary VSASummaryDisplay
593+ PolicyDiff * PolicyDiffDisplay // nil if no policy diff
594+ // Other sections will be added one at a time
595+ }
596+
597+ // HeaderDisplay holds the formatted header section data
598+ type HeaderDisplay struct {
599+ Title string
600+ Timestamp string
601+ }
602+
603+ // ResultDisplay holds the formatted Result section data
604+ type ResultDisplay struct {
605+ Overall string // "✅ PASSED" or "❌ FAILED"
606+ Fallback string // "used for all images", "used for some images", or ""
607+ ImageCount int
608+ ImageLines []string // Formatted image status lines
609+ }
610+
611+ // VSASummaryDisplay holds the formatted VSA Summary section data
612+ type VSASummaryDisplay struct {
613+ Signature string // Signature status
614+ Predicate string // Predicate status line
615+ Policy string // Policy status line
616+ FallbackReasons string // Fallback reason(s) line, or ""
617+ }
618+
619+ // PolicyDiffDisplay holds the formatted Policy Diff section data
620+ type PolicyDiffDisplay struct {
621+ AffectedImages string // Comma-separated list of affected images
622+ Added string // "none" or "[include] N"
623+ Removed string // "none" or "N"
624+ Changed string // "none" or "N"
625+ }
626+
588627// Helper functions
589628
590629// shortenImageDigest extracts and shortens image digest to 8 characters
@@ -788,12 +827,81 @@ func aggregateAllSectionsData(allResults []vsa.ComponentResult) AllSectionsData
788827 return data
789828}
790829
830+ // buildHeaderDisplay creates a HeaderDisplay from a timestamp
831+ func buildHeaderDisplay (timestamp time.Time ) HeaderDisplay {
832+ return HeaderDisplay {
833+ Title : "VALIDATE VSA RESULT" ,
834+ Timestamp : timestamp .Format (time .RFC3339 ),
835+ }
836+ }
837+
838+ // String formats the header for display
839+ func (h HeaderDisplay ) String () string {
840+ return fmt .Sprintf ("=== %s — %s ===\n " , h .Title , h .Timestamp )
841+ }
842+
791843// displayHeaderSection - Displays header with timestamp
844+ // DEPRECATED: Use buildHeaderDisplay and HeaderDisplay.String() instead
792845func displayHeaderSection (timestamp time.Time ) {
793846 fmt .Printf ("=== VALIDATE VSA RESULT — %s ===\n " , timestamp .Format (time .RFC3339 ))
794847}
795848
849+ // buildResultDisplay creates a ResultDisplay from AllSectionsData
850+ func buildResultDisplay (data AllSectionsData ) ResultDisplay {
851+ result := ResultDisplay {
852+ ImageCount : data .TotalImages ,
853+ }
854+
855+ // Set overall status
856+ if data .OverallPassed {
857+ result .Overall = "✅ PASSED"
858+ } else {
859+ result .Overall = "❌ FAILED"
860+ }
861+
862+ // Set fallback status
863+ if data .FallbackUsed {
864+ if data .FallbackCount == data .TotalImages {
865+ result .Fallback = "used for all images"
866+ } else {
867+ result .Fallback = "used for some images"
868+ }
869+ }
870+
871+ // Build image status lines
872+ result .ImageLines = make ([]string , 0 , len (data .ImageStatuses ))
873+ for _ , img := range data .ImageStatuses {
874+ statusLine := fmt .Sprintf (" [%d] …%s VSA=%s" , img .Index , img .Digest , img .VSAStatus )
875+ if img .FallbackStatus != "" {
876+ statusLine += fmt .Sprintf (" Fallback=%s" , img .FallbackStatus )
877+ }
878+ result .ImageLines = append (result .ImageLines , statusLine )
879+ }
880+
881+ return result
882+ }
883+
884+ // String formats the Result section for display
885+ func (r ResultDisplay ) String () string {
886+ var b strings.Builder
887+ b .WriteString ("Result\n " )
888+ b .WriteString (fmt .Sprintf (" Overall: %s\n " , r .Overall ))
889+
890+ if r .Fallback != "" {
891+ b .WriteString (fmt .Sprintf (" Fallback: %s\n " , r .Fallback ))
892+ }
893+
894+ b .WriteString (fmt .Sprintf (" Images (%d):\n " , r .ImageCount ))
895+ for _ , line := range r .ImageLines {
896+ b .WriteString (line )
897+ b .WriteString ("\n " )
898+ }
899+
900+ return b .String ()
901+ }
902+
796903// displayResultSection - Displays Result section
904+ // DEPRECATED: Use buildResultDisplay and ResultDisplay.String() instead
797905func displayResultSection (data AllSectionsData ) {
798906 fmt .Println ("Result" )
799907 if data .OverallPassed {
@@ -820,7 +928,67 @@ func displayResultSection(data AllSectionsData) {
820928 }
821929}
822930
931+ // buildVSASummaryDisplay creates a VSASummaryDisplay from AllSectionsData
932+ func buildVSASummaryDisplay (data AllSectionsData ) VSASummaryDisplay {
933+ summary := VSASummaryDisplay {
934+ Signature : data .SignatureStatus ,
935+ }
936+
937+ // Build predicate status
938+ totalPredicates := data .PredicatePassed + data .PredicateFailed
939+ if totalPredicates == 0 {
940+ summary .Predicate = "(no predicate data)"
941+ } else if data .PredicateFailed == 0 {
942+ summary .Predicate = fmt .Sprintf ("passed (%d/%d)" , data .PredicatePassed , totalPredicates )
943+ } else if data .PredicatePassed == 0 {
944+ summary .Predicate = fmt .Sprintf ("failed (%d/%d)" , data .PredicateFailed , totalPredicates )
945+ } else {
946+ summary .Predicate = fmt .Sprintf ("mixed (passed: %d, failed: %d)" , data .PredicatePassed , data .PredicateFailed )
947+ }
948+
949+ // Build policy status
950+ totalPolicyChecks := data .PolicyMatches + data .PolicyMismatches
951+ if totalPolicyChecks == 0 {
952+ summary .Policy = "(no policy data)"
953+ } else if data .PolicyMismatches == 0 {
954+ summary .Policy = "matches (no differences)"
955+ } else {
956+ // Aggregate policy diff counts
957+ totals := aggregatePolicyDiffTotals (data .PolicyDiffCounts )
958+ summary .Policy = fmt .Sprintf ("mismatches on %d/%d images (adds=%d, removes=%d, changes=%d)" ,
959+ data .PolicyMismatches , totalPolicyChecks , totals .Added , totals .Removed , totals .Changed )
960+ }
961+
962+ // Build fallback reasons
963+ if len (data .FallbackReasons ) > 0 {
964+ var reasons []string
965+ for reason := range data .FallbackReasons {
966+ reasons = append (reasons , reason )
967+ }
968+ sort .Strings (reasons )
969+ summary .FallbackReasons = strings .Join (reasons , ", " )
970+ }
971+
972+ return summary
973+ }
974+
975+ // String formats the VSA Summary section for display
976+ func (v VSASummaryDisplay ) String () string {
977+ var b strings.Builder
978+ b .WriteString ("VSA Summary\n " )
979+ b .WriteString (fmt .Sprintf (" Signature: %s\n " , v .Signature ))
980+ b .WriteString (fmt .Sprintf (" Predicate: %s\n " , v .Predicate ))
981+ b .WriteString (fmt .Sprintf (" Policy: %s\n " , v .Policy ))
982+
983+ if v .FallbackReasons != "" {
984+ b .WriteString (fmt .Sprintf (" Fallback reason(s): %s\n " , v .FallbackReasons ))
985+ }
986+
987+ return b .String ()
988+ }
989+
823990// displayVSASummarySection - Displays VSA Summary section
991+ // DEPRECATED: Use buildVSASummaryDisplay and VSASummaryDisplay.String() instead
824992func displayVSASummarySection (data AllSectionsData ) {
825993 fmt .Println ("VSA Summary" )
826994 fmt .Printf (" Signature: %s\n " , data .SignatureStatus )
@@ -861,7 +1029,57 @@ func displayVSASummarySection(data AllSectionsData) {
8611029 }
8621030}
8631031
1032+ // buildPolicyDiffDisplay creates a PolicyDiffDisplay from AllSectionsData
1033+ // Returns nil if there is no policy diff
1034+ func buildPolicyDiffDisplay (data AllSectionsData ) * PolicyDiffDisplay {
1035+ if ! data .HasPolicyDiff {
1036+ return nil
1037+ }
1038+
1039+ diff := & PolicyDiffDisplay {
1040+ AffectedImages : strings .Join (data .AffectedImages , ", " ),
1041+ }
1042+
1043+ // Aggregate policy diff counts across all affected images
1044+ totals := aggregatePolicyDiffTotals (data .PolicyDiffCounts )
1045+
1046+ // Build added line
1047+ if totals .Added > 0 {
1048+ diff .Added = fmt .Sprintf ("[include] %d" , totals .Added )
1049+ } else {
1050+ diff .Added = "none"
1051+ }
1052+
1053+ // Build removed line
1054+ if totals .Removed > 0 {
1055+ diff .Removed = fmt .Sprintf ("%d" , totals .Removed )
1056+ } else {
1057+ diff .Removed = "none"
1058+ }
1059+
1060+ // Build changed line
1061+ if totals .Changed > 0 {
1062+ diff .Changed = fmt .Sprintf ("%d" , totals .Changed )
1063+ } else {
1064+ diff .Changed = "none"
1065+ }
1066+
1067+ return diff
1068+ }
1069+
1070+ // String formats the Policy Diff section for display
1071+ func (p PolicyDiffDisplay ) String () string {
1072+ var b strings.Builder
1073+ b .WriteString ("Policy Diff (summary)\n " )
1074+ b .WriteString (fmt .Sprintf (" Affected images: [%s]\n " , p .AffectedImages ))
1075+ b .WriteString (fmt .Sprintf (" Added: %s\n " , p .Added ))
1076+ b .WriteString (fmt .Sprintf (" Removed: %s\n " , p .Removed ))
1077+ b .WriteString (fmt .Sprintf (" Changed: %s\n " , p .Changed ))
1078+ return b .String ()
1079+ }
1080+
8641081// displayPolicyDiffSection - Displays Policy Diff section (only if policy diff exists)
1082+ // DEPRECATED: Use buildPolicyDiffDisplay and PolicyDiffDisplay.String() instead
8651083func displayPolicyDiffSection (data AllSectionsData ) {
8661084 if ! data .HasPolicyDiff {
8671085 return
@@ -974,19 +1192,27 @@ func displayComponentResults(allResults []vsa.ComponentResult, data *validateVSA
9741192 // Single aggregation pass - collect everything once
9751193 allData := aggregateAllSectionsData (allResults )
9761194
1195+ // Build display struct
1196+ display := ComponentResultsDisplay {
1197+ Header : buildHeaderDisplay (time .Now ()),
1198+ Result : buildResultDisplay (allData ),
1199+ VSASummary : buildVSASummaryDisplay (allData ),
1200+ PolicyDiff : buildPolicyDiffDisplay (allData ),
1201+ }
1202+
9771203 // Display sections in order (each is self-contained)
978- displayHeaderSection ( time . Now ())
1204+ fmt . Print ( display . Header . String ())
9791205 fmt .Println ()
9801206
981- displayResultSection ( allData )
1207+ fmt . Print ( display . Result . String () )
9821208 fmt .Println ()
9831209
984- displayVSASummarySection ( allData )
1210+ fmt . Print ( display . VSASummary . String () )
9851211 fmt .Println ()
9861212
9871213 // Conditional sections
988- if allData . HasPolicyDiff {
989- displayPolicyDiffSection ( allData )
1214+ if display . PolicyDiff != nil {
1215+ fmt . Print ( display . PolicyDiff . String () )
9901216 fmt .Println ()
9911217 }
9921218
0 commit comments