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  }