github.com/wolfi-dev/wolfictl@v0.16.11/pkg/advisory/diff.go (about) 1 package advisory 2 3 import ( 4 "reflect" 5 6 "github.com/wolfi-dev/wolfictl/pkg/configs" 7 v2 "github.com/wolfi-dev/wolfictl/pkg/configs/advisory/v2" 8 ) 9 10 // IndexDiffResult is the result of diffing two advisory document indexes. 11 type IndexDiffResult struct { 12 Added []v2.Document 13 Removed []v2.Document 14 15 Modified []DocumentDiffResult 16 } 17 18 // IsZero returns true there is no difference between the compared advisory 19 // document indexes. 20 func (r IndexDiffResult) IsZero() bool { 21 return len(r.Added) == 0 && len(r.Removed) == 0 && len(r.Modified) == 0 22 } 23 24 // DocumentDiffResult is the result of diffing two advisory documents. 25 type DocumentDiffResult struct { 26 Name string 27 28 Added v2.Advisories 29 Removed v2.Advisories 30 31 Modified []DiffResult 32 } 33 34 // IsZero returns true if there is no difference between the compared advisory 35 // documents. 36 func (r DocumentDiffResult) IsZero() bool { 37 return len(r.Added) == 0 && len(r.Removed) == 0 && len(r.Modified) == 0 38 } 39 40 // DiffResult is the result of diffing two advisories. 41 type DiffResult struct { 42 ID string 43 44 Added v2.Advisory 45 Removed v2.Advisory 46 47 AddedEvents []v2.Event 48 RemovedEvents []v2.Event 49 } 50 51 // IsZero returns true if there is no difference between the compared 52 // advisories. 53 func (r DiffResult) IsZero() bool { 54 return r.Added.IsZero() && r.Removed.IsZero() 55 } 56 57 type EventDiffResult struct { 58 ID string 59 60 Added v2.Event 61 Removed v2.Event 62 } 63 64 // IndexDiff takes two advisory document indexes and returns a diff of the 65 // advisory data between them. 66 func IndexDiff(a, b *configs.Index[v2.Document]) IndexDiffResult { 67 removed, added, common := venn( 68 a.Select().Configurations(), 69 b.Select().Configurations(), 70 func(a, b v2.Document) bool { 71 return a.Name() == b.Name() 72 }, 73 ) 74 75 result := IndexDiffResult{ 76 Added: added, 77 Removed: removed, 78 } 79 80 for _, name := range documentNames(common) { 81 diff := documentDiff( 82 a.Select().WhereName(name).Configurations()[0], 83 b.Select().WhereName(name).Configurations()[0], 84 ) 85 if !diff.IsZero() { 86 result.Modified = append(result.Modified, diff) 87 } 88 } 89 90 return result 91 } 92 93 // documentDiff takes two advisory documents and returns a diff of their 94 // respective advisory lists. 95 func documentDiff(a, b v2.Document) DocumentDiffResult { 96 removed, added, common := venn( 97 a.Advisories, 98 b.Advisories, 99 func(a, b v2.Advisory) bool { 100 return a.ID == b.ID 101 }, 102 ) 103 104 result := DocumentDiffResult{ 105 Name: a.Name(), 106 Added: added, 107 Removed: removed, 108 } 109 110 for _, id := range advisoryIDs(common) { 111 advA, _ := a.Advisories.Get(id) 112 advB, _ := b.Advisories.Get(id) 113 114 diff := advisoryDiff(advA, advB) 115 if !diff.IsZero() { 116 result.Modified = append(result.Modified, diff) 117 } 118 } 119 120 return result 121 } 122 123 // advisoryDiff takes two advisories and if they are different, returns a DiffResult 124 // wrapping the two advisories; otherwise if they are the same, it returns a 125 // zero-value DiffResult. 126 func advisoryDiff(a, b v2.Advisory) DiffResult { 127 if reflect.DeepEqual(a, b) { 128 return DiffResult{} 129 } 130 131 if reflect.DeepEqual(a.SortedEvents(), b.SortedEvents()) { 132 // No change with regard to events, so just return the advisories. 133 return DiffResult{ID: a.ID, Added: b, Removed: a} 134 } 135 136 // Otherwise, we need to diff the events. 137 138 removedEvents, addedEvents, _ := venn( 139 a.SortedEvents(), 140 b.SortedEvents(), 141 func(a, b v2.Event) bool { 142 return reflect.DeepEqual(a, b) // This means we won't diff the common events 143 }, 144 ) 145 146 result := DiffResult{ 147 ID: a.ID, 148 Added: b, 149 Removed: a, 150 AddedEvents: addedEvents, 151 RemovedEvents: removedEvents, 152 } 153 154 return result 155 } 156 157 func documentNames(documents []v2.Document) []string { 158 names := make([]string, len(documents)) 159 for i, document := range documents { 160 names[i] = document.Name() 161 } 162 return names 163 } 164 165 func advisoryIDs(advisories []v2.Advisory) []string { 166 ids := make([]string, len(advisories)) 167 for i, advisory := range advisories { 168 ids[i] = advisory.ID 169 } 170 return ids 171 } 172 173 // venn takes two slices and returns three slices, the first containing the 174 // items found in the first slice but not the second, the second containing the 175 // items found in the second slice but not the first, and the third containing 176 // the items found in common between the two input slices (but the versions of 177 // the items from the first input slice in particular, since a given slice item 178 // can't represent both of the item's instances between the two input slices). 179 // venn also takes a function to compare two items for equality. 180 func venn[S interface{ ~[]T }, T any](s1, s2 S, equal func(a, b T) bool) (s1Only, s2Only, s1Common S) { 181 for _, a := range s1 { 182 found := false 183 for _, b := range s2 { 184 if equal(a, b) { 185 found = true 186 s1Common = append(s1Common, a) 187 break 188 } 189 } 190 191 if !found { 192 s1Only = append(s1Only, a) 193 } 194 } 195 196 for _, b := range s2 { 197 found := false 198 for _, a := range s1 { 199 if equal(a, b) { 200 found = true 201 break 202 } 203 } 204 205 if !found { 206 s2Only = append(s2Only, b) 207 } 208 } 209 210 return s1Only, s2Only, s1Common 211 }