@@ -178,7 +178,8 @@ type ModifiedEFIBinaryEvidence struct {
178178 To EFIBinaryEvidence `json:"to"`
179179 Changes []FieldChange `json:"changes,omitempty"`
180180
181- UKI * UKIDiff `json:"uki,omitempty"`
181+ UKI * UKIDiff `json:"uki,omitempty"`
182+ BootConfig * BootloaderConfigDiff `json:"bootConfig,omitempty"`
182183}
183184
184185// UKIDiff represents differences in the UKI-related fields of an EFI binary.
@@ -201,6 +202,54 @@ type SectionMapDiff struct {
201202 Modified map [string ]ValueDiff [string ] `json:"modified,omitempty"`
202203}
203204
205+ // BootloaderConfigDiff represents differences in bootloader configuration.
206+ type BootloaderConfigDiff struct {
207+ ConfigFileChanges []ConfigFileChange `json:"configFileChanges,omitempty"`
208+ BootEntryChanges []BootEntryChange `json:"bootEntryChanges,omitempty"`
209+ KernelRefChanges []KernelRefChange `json:"kernelRefChanges,omitempty"`
210+ UUIDReferenceChanges []UUIDRefChange `json:"uuidReferenceChanges,omitempty"`
211+ NotesAdded []string `json:"notesAdded,omitempty"`
212+ NotesRemoved []string `json:"notesRemoved,omitempty"`
213+ }
214+
215+ // ConfigFileChange represents a change to a bootloader config file.
216+ type ConfigFileChange struct {
217+ Path string `json:"path" yaml:"path"`
218+ Status string `json:"status" yaml:"status"` // "added", "removed", "modified"
219+ HashFrom string `json:"hashFrom,omitempty" yaml:"hashFrom,omitempty"`
220+ HashTo string `json:"hashTo,omitempty" yaml:"hashTo,omitempty"`
221+ }
222+
223+ // BootEntryChange represents a change to a boot entry.
224+ type BootEntryChange struct {
225+ Name string `json:"name" yaml:"name"`
226+ Status string `json:"status" yaml:"status"` // "added", "removed", "modified"
227+ KernelFrom string `json:"kernelFrom,omitempty" yaml:"kernelFrom,omitempty"`
228+ KernelTo string `json:"kernelTo,omitempty" yaml:"kernelTo,omitempty"`
229+ InitrdFrom string `json:"initrdFrom,omitempty" yaml:"initrdFrom,omitempty"`
230+ InitrdTo string `json:"initrdTo,omitempty" yaml:"initrdTo,omitempty"`
231+ CmdlineFrom string `json:"cmdlineFrom,omitempty" yaml:"cmdlineFrom,omitempty"`
232+ CmdlineTo string `json:"cmdlineTo,omitempty" yaml:"cmdlineTo,omitempty"`
233+ }
234+
235+ // KernelRefChange represents a change to a kernel reference.
236+ type KernelRefChange struct {
237+ Path string `json:"path" yaml:"path"`
238+ Status string `json:"status" yaml:"status"` // "added", "removed", "modified"
239+ UUIDFrom string `json:"uuidFrom,omitempty" yaml:"uuidFrom,omitempty"`
240+ UUIDTo string `json:"uuidTo,omitempty" yaml:"uuidTo,omitempty"`
241+ }
242+
243+ // UUIDRefChange represents a change to a UUID reference.
244+ type UUIDRefChange struct {
245+ UUID string `json:"uuid" yaml:"uuid"`
246+ Status string `json:"status" yaml:"status"` // "added", "removed", "modified"
247+ ContextFrom string `json:"contextFrom,omitempty" yaml:"contextFrom,omitempty"`
248+ ContextTo string `json:"contextTo,omitempty" yaml:"contextTo,omitempty"`
249+ MismatchFrom bool `json:"mismatchFrom,omitempty" yaml:"mismatchFrom,omitempty"`
250+ MismatchTo bool `json:"mismatchTo,omitempty" yaml:"mismatchTo,omitempty"`
251+ }
252+
204253// CompareImages compares two ImageSummary objects and returns a structured diff.
205254func CompareImages (from , to * ImageSummary ) ImageCompareResult {
206255 if from == nil || to == nil {
@@ -797,7 +846,6 @@ func tallyDiffs(d ImageDiff) diffTally {
797846 if d .PartitionTable .MisalignedParts != nil {
798847 t .addMeaningful (1 , "PT MisalignedParts" )
799848 }
800-
801849 if len (d .Partitions .Added ) > 0 {
802850 t .addMeaningful (len (d .Partitions .Added ), "Partitions Added" )
803851 }
@@ -930,32 +978,35 @@ func tallyEFIBinaryDiff(t *diffTally, d EFIBinaryDiff) {
930978 }
931979
932980 // UKI diffs
933- if m .UKI == nil || ! m .UKI .Changed {
934- continue
935- }
936-
937- if m .UKI .KernelSHA256 != nil {
938- t .addMeaningful (1 , "EFI " + m .Key + " UKI KernelSHA" )
939- }
940- if m .UKI .OSRelSHA256 != nil {
941- t .addMeaningful (1 , "EFI " + m .Key + " UKI OSRelSHA" )
942- }
943- if m .UKI .UnameSHA256 != nil {
944- t .addMeaningful (1 , "EFI " + m .Key + " UKI UnameSHA" )
945- }
981+ if m .UKI != nil && m .UKI .Changed {
982+ if m .UKI .KernelSHA256 != nil {
983+ t .addMeaningful (1 , "EFI " + m .Key + " UKI KernelSHA" )
984+ }
985+ if m .UKI .OSRelSHA256 != nil {
986+ t .addMeaningful (1 , "EFI " + m .Key + " UKI OSRelSHA" )
987+ }
988+ if m .UKI .UnameSHA256 != nil {
989+ t .addMeaningful (1 , "EFI " + m .Key + " UKI UnameSHA" )
990+ }
946991
947- otherSectionChanged := false
948- for sec := range m .UKI .SectionSHA256 .Modified {
949- secL := strings .ToLower (strings .TrimSpace (sec ))
950- if secL == ".cmdline" || secL == "cmdline" ||
951- secL == ".initrd" || secL == "initrd" {
952- continue
992+ otherSectionChanged := false
993+ for sec := range m .UKI .SectionSHA256 .Modified {
994+ secL := strings .ToLower (strings .TrimSpace (sec ))
995+ if secL == ".cmdline" || secL == "cmdline" ||
996+ secL == ".initrd" || secL == "initrd" {
997+ continue
998+ }
999+ otherSectionChanged = true
1000+ break
1001+ }
1002+ if otherSectionChanged {
1003+ t .addMeaningful (1 , "EFI " + m .Key + " UKI otherSectionChanged" )
9531004 }
954- otherSectionChanged = true
955- break
9561005 }
957- if otherSectionChanged {
958- t .addMeaningful (1 , "EFI " + m .Key + " UKI otherSectionChanged" )
1006+
1007+ // Bootloader config diffs
1008+ if m .BootConfig != nil {
1009+ tallyBootloaderConfigDiff (t , m .BootConfig , m .Key )
9591010 }
9601011 }
9611012}
@@ -1009,3 +1060,96 @@ func tallyFilesystemChange(t *diffTally, fs *FilesystemChange) {
10091060 }
10101061 }
10111062}
1063+
1064+ func tallyBootloaderConfigDiff (t * diffTally , diff * BootloaderConfigDiff , efiKey string ) {
1065+ if diff == nil {
1066+ return
1067+ }
1068+
1069+ // Config file changes are meaningful (actual bootloader configuration changed)
1070+ for _ , cf := range diff .ConfigFileChanges {
1071+ switch cf .Status {
1072+ case "added" :
1073+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] config file added: " + cf .Path )
1074+ case "removed" :
1075+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] config file removed: " + cf .Path )
1076+ case "modified" :
1077+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] config file modified: " + cf .Path )
1078+ }
1079+ }
1080+
1081+ // Boot entry changes are meaningful (boot menu changed)
1082+ for _ , be := range diff .BootEntryChanges {
1083+ switch be .Status {
1084+ case "added" :
1085+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] boot entry added: " + be .Name )
1086+ case "removed" :
1087+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] boot entry removed: " + be .Name )
1088+ case "modified" :
1089+ // Check if kernel path or cmdline actually changed (meaningful)
1090+ // vs just the display name changed
1091+ if be .KernelFrom != be .KernelTo {
1092+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] boot entry kernel changed: " + be .Name )
1093+ } else if be .InitrdFrom != be .InitrdTo {
1094+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] boot entry initrd changed: " + be .Name )
1095+ } else if normalizeKernelCmdline (be .CmdlineFrom ) != normalizeKernelCmdline (be .CmdlineTo ) {
1096+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] boot entry cmdline changed: " + be .Name )
1097+ } else {
1098+ // Only cosmetic/metadata changes
1099+ t .addVolatile (1 , "BootConfig[" + efiKey + "] boot entry metadata changed: " + be .Name )
1100+ }
1101+ }
1102+ }
1103+
1104+ // Kernel reference changes
1105+ for _ , kr := range diff .KernelRefChanges {
1106+ switch kr .Status {
1107+ case "added" :
1108+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] kernel ref added: " + kr .Path )
1109+ case "removed" :
1110+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] kernel ref removed: " + kr .Path )
1111+ case "modified" :
1112+ // UUID change is typically volatile (regenerated each build)
1113+ if kr .UUIDFrom != kr .UUIDTo {
1114+ t .addVolatile (1 , "BootConfig[" + efiKey + "] kernel ref UUID changed: " + kr .Path )
1115+ } else {
1116+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] kernel ref modified: " + kr .Path )
1117+ }
1118+ }
1119+ }
1120+
1121+ // UUID reference changes - typically volatile (UUIDs regenerate)
1122+ for _ , ur := range diff .UUIDReferenceChanges {
1123+ switch ur .Status {
1124+ case "added" :
1125+ // New UUID reference found - could be meaningful or volatile depending on context
1126+ if ur .MismatchTo {
1127+ // UUID mismatch is a potential issue - meaningful
1128+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] UUID ref mismatch added: " + ur .UUID )
1129+ } else {
1130+ t .addVolatile (1 , "BootConfig[" + efiKey + "] UUID ref added: " + ur .UUID )
1131+ }
1132+ case "removed" :
1133+ if ur .MismatchFrom {
1134+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] UUID ref mismatch removed: " + ur .UUID )
1135+ } else {
1136+ t .addVolatile (1 , "BootConfig[" + efiKey + "] UUID ref removed: " + ur .UUID )
1137+ }
1138+ case "modified" :
1139+ // UUID context changed - typically volatile unless introducing/fixing mismatch
1140+ if ur .MismatchFrom != ur .MismatchTo {
1141+ t .addMeaningful (1 , "BootConfig[" + efiKey + "] UUID ref mismatch status changed: " + ur .UUID )
1142+ } else {
1143+ t .addVolatile (1 , "BootConfig[" + efiKey + "] UUID ref context changed: " + ur .UUID )
1144+ }
1145+ }
1146+ }
1147+
1148+ // Notes changes are informational - count as volatile
1149+ if len (diff .NotesAdded ) > 0 {
1150+ t .addVolatile (len (diff .NotesAdded ), "BootConfig[" + efiKey + "] notes added" )
1151+ }
1152+ if len (diff .NotesRemoved ) > 0 {
1153+ t .addVolatile (len (diff .NotesRemoved ), "BootConfig[" + efiKey + "] notes removed" )
1154+ }
1155+ }
0 commit comments