Skip to content

Commit 63a14f9

Browse files
authored
Added more comprehensive support to inspect bootloader configuration (#404)
1 parent 1e92b2e commit 63a14f9

11 files changed

Lines changed: 2764 additions & 36 deletions

internal/image/imageinspect/bootloader_config.go

Lines changed: 439 additions & 0 deletions
Large diffs are not rendered by default.

internal/image/imageinspect/bootloader_config_test.go

Lines changed: 725 additions & 0 deletions
Large diffs are not rendered by default.

internal/image/imageinspect/bootloader_efi.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func ParsePEFromBytes(p string, blob []byte) (EFIBinaryEvidence, error) {
1717
Kind: BootloaderUnknown, // set after we have more evidence
1818
}
1919

20-
ev.SHA256 = sha256Hex(blob)
20+
ev.SHA256 = hashBytesHex(blob)
2121

2222
r := bytes.NewReader(blob)
2323
f, err := pe.NewFile(r)
@@ -61,7 +61,7 @@ func ParsePEFromBytes(p string, blob []byte) (EFIBinaryEvidence, error) {
6161
ev.Notes = append(ev.Notes, fmt.Sprintf("read section %s: %v", name, err))
6262
continue
6363
}
64-
ev.SectionSHA256[name] = sha256Hex(data)
64+
ev.SectionSHA256[name] = hashBytesHex(data)
6565

6666
switch name {
6767
case ".linux":

internal/image/imageinspect/compare.go

Lines changed: 169 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
205254
func 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

Comments
 (0)