github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/catalog_test.go (about)

     1  package pkg
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/scylladb/go-set/strset"
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/cpe"
    12  	"github.com/anchore/syft/syft/file"
    13  )
    14  
    15  type expectedIndexes struct {
    16  	byType map[Type]*strset.Set
    17  	byPath map[string]*strset.Set
    18  }
    19  
    20  func TestCatalogMergePackageLicenses(t *testing.T) {
    21  	tests := []struct {
    22  		name         string
    23  		pkgs         []Package
    24  		expectedPkgs []Package
    25  	}{
    26  		{
    27  			name: "merges licenses of packages with equal ID",
    28  			pkgs: []Package{
    29  				{
    30  					id: "equal",
    31  					Licenses: NewLicenseSet(
    32  						NewLicensesFromValues("foo", "baq", "quz")...,
    33  					),
    34  				},
    35  				{
    36  					id: "equal",
    37  					Licenses: NewLicenseSet(
    38  						NewLicensesFromValues("bar", "baz", "foo", "qux")...,
    39  					),
    40  				},
    41  			},
    42  			expectedPkgs: []Package{
    43  				{
    44  					id: "equal",
    45  					Licenses: NewLicenseSet(
    46  						NewLicensesFromValues("foo", "baq", "quz", "qux", "bar", "baz")...,
    47  					),
    48  				},
    49  			},
    50  		},
    51  	}
    52  
    53  	for _, test := range tests {
    54  		t.Run(test.name, func(t *testing.T) {
    55  			collection := NewCollection(test.pkgs...)
    56  			for i, p := range collection.Sorted() {
    57  				assert.Equal(t, test.expectedPkgs[i].Licenses, p.Licenses)
    58  			}
    59  		})
    60  	}
    61  }
    62  
    63  func TestCatalogDeleteRemovesPackages(t *testing.T) {
    64  	tests := []struct {
    65  		name            string
    66  		pkgs            []Package
    67  		deleteIDs       []artifact.ID
    68  		expectedIndexes expectedIndexes
    69  	}{
    70  		{
    71  			name: "delete one package",
    72  			pkgs: []Package{
    73  				{
    74  					id:      "pkg:deb/debian/1",
    75  					Name:    "debian",
    76  					Version: "1",
    77  					Type:    DebPkg,
    78  					Locations: file.NewLocationSet(
    79  						file.NewVirtualLocation("/c/path", "/another/path1"),
    80  					),
    81  				},
    82  				{
    83  					id:      "pkg:deb/debian/2",
    84  					Name:    "debian",
    85  					Version: "2",
    86  					Type:    DebPkg,
    87  					Locations: file.NewLocationSet(
    88  						file.NewVirtualLocation("/d/path", "/another/path2"),
    89  					),
    90  				},
    91  			},
    92  			deleteIDs: []artifact.ID{
    93  				artifact.ID("pkg:deb/debian/1"),
    94  			},
    95  			expectedIndexes: expectedIndexes{
    96  				byType: map[Type]*strset.Set{
    97  					DebPkg: strset.New("pkg:deb/debian/2"),
    98  				},
    99  				byPath: map[string]*strset.Set{
   100  					"/d/path":        strset.New("pkg:deb/debian/2"),
   101  					"/another/path2": strset.New("pkg:deb/debian/2"),
   102  				},
   103  			},
   104  		},
   105  		{
   106  			name: "delete multiple packages",
   107  			pkgs: []Package{
   108  				{
   109  					id:      "pkg:deb/debian/1",
   110  					Name:    "debian",
   111  					Version: "1",
   112  					Type:    DebPkg,
   113  					Locations: file.NewLocationSet(
   114  						file.NewVirtualLocation("/c/path", "/another/path1"),
   115  					),
   116  				},
   117  				{
   118  					id:      "pkg:deb/debian/2",
   119  					Name:    "debian",
   120  					Version: "2",
   121  					Type:    DebPkg,
   122  					Locations: file.NewLocationSet(
   123  						file.NewVirtualLocation("/d/path", "/another/path2"),
   124  					),
   125  				},
   126  				{
   127  					id:      "pkg:deb/debian/3",
   128  					Name:    "debian",
   129  					Version: "3",
   130  					Type:    DebPkg,
   131  					Locations: file.NewLocationSet(
   132  						file.NewVirtualLocation("/e/path", "/another/path3"),
   133  					),
   134  				},
   135  			},
   136  			deleteIDs: []artifact.ID{
   137  				artifact.ID("pkg:deb/debian/1"),
   138  				artifact.ID("pkg:deb/debian/3"),
   139  			},
   140  			expectedIndexes: expectedIndexes{
   141  				byType: map[Type]*strset.Set{
   142  					DebPkg: strset.New("pkg:deb/debian/2"),
   143  				},
   144  				byPath: map[string]*strset.Set{
   145  					"/d/path":        strset.New("pkg:deb/debian/2"),
   146  					"/another/path2": strset.New("pkg:deb/debian/2"),
   147  				},
   148  			},
   149  		},
   150  		{
   151  			name: "delete non-existent package",
   152  			pkgs: []Package{
   153  				{
   154  					id:      artifact.ID("pkg:deb/debian/1"),
   155  					Name:    "debian",
   156  					Version: "1",
   157  					Type:    DebPkg,
   158  					Locations: file.NewLocationSet(
   159  						file.NewVirtualLocation("/c/path", "/another/path1"),
   160  					),
   161  				},
   162  				{
   163  					id:      artifact.ID("pkg:deb/debian/2"),
   164  					Name:    "debian",
   165  					Version: "2",
   166  					Type:    DebPkg,
   167  					Locations: file.NewLocationSet(
   168  						file.NewVirtualLocation("/d/path", "/another/path2"),
   169  					),
   170  				},
   171  			},
   172  			deleteIDs: []artifact.ID{
   173  				artifact.ID("pkg:deb/debian/3"),
   174  			},
   175  			expectedIndexes: expectedIndexes{
   176  				byType: map[Type]*strset.Set{
   177  					DebPkg: strset.New("pkg:deb/debian/1", "pkg:deb/debian/2"),
   178  				},
   179  				byPath: map[string]*strset.Set{
   180  					"/c/path":        strset.New("pkg:deb/debian/1"),
   181  					"/another/path1": strset.New("pkg:deb/debian/1"),
   182  					"/d/path":        strset.New("pkg:deb/debian/2"),
   183  					"/another/path2": strset.New("pkg:deb/debian/2"),
   184  				},
   185  			},
   186  		},
   187  	}
   188  
   189  	for _, test := range tests {
   190  		t.Run(test.name, func(t *testing.T) {
   191  			c := NewCollection()
   192  			for _, p := range test.pkgs {
   193  				c.Add(p)
   194  			}
   195  
   196  			for _, id := range test.deleteIDs {
   197  				c.Delete(id)
   198  			}
   199  
   200  			assertIndexes(t, c, test.expectedIndexes)
   201  		})
   202  	}
   203  }
   204  
   205  func TestCatalogAddPopulatesIndex(t *testing.T) {
   206  
   207  	var pkgs = []Package{
   208  		{
   209  			Locations: file.NewLocationSet(
   210  				file.NewVirtualLocation("/a/path", "/another/path"),
   211  				file.NewVirtualLocation("/b/path", "/bee/path"),
   212  			),
   213  			Type: RpmPkg,
   214  		},
   215  		{
   216  			Locations: file.NewLocationSet(
   217  				file.NewVirtualLocation("/c/path", "/another/path"),
   218  				file.NewVirtualLocation("/d/path", "/another/path"),
   219  			),
   220  			Type: NpmPkg,
   221  		},
   222  	}
   223  
   224  	for i := range pkgs {
   225  		p := &pkgs[i]
   226  		p.SetID()
   227  	}
   228  
   229  	fixtureID := func(i int) string {
   230  		return string(pkgs[i].ID())
   231  	}
   232  
   233  	tests := []struct {
   234  		name            string
   235  		expectedIndexes expectedIndexes
   236  	}{
   237  		{
   238  			name: "vanilla-add",
   239  			expectedIndexes: expectedIndexes{
   240  				byType: map[Type]*strset.Set{
   241  					RpmPkg: strset.New(fixtureID(0)),
   242  					NpmPkg: strset.New(fixtureID(1)),
   243  				},
   244  				byPath: map[string]*strset.Set{
   245  					"/another/path": strset.New(fixtureID(0), fixtureID(1)),
   246  					"/a/path":       strset.New(fixtureID(0)),
   247  					"/b/path":       strset.New(fixtureID(0)),
   248  					"/bee/path":     strset.New(fixtureID(0)),
   249  					"/c/path":       strset.New(fixtureID(1)),
   250  					"/d/path":       strset.New(fixtureID(1)),
   251  				},
   252  			},
   253  		},
   254  	}
   255  
   256  	for _, test := range tests {
   257  		t.Run(test.name, func(t *testing.T) {
   258  			c := NewCollection(pkgs...)
   259  			assertIndexes(t, c, test.expectedIndexes)
   260  		})
   261  	}
   262  }
   263  
   264  func assertIndexes(t *testing.T, c *Collection, expectedIndexes expectedIndexes) {
   265  	// assert path index
   266  	assert.Len(t, c.idsByPath, len(expectedIndexes.byPath), "unexpected path index length")
   267  	for path, expectedIds := range expectedIndexes.byPath {
   268  		actualIds := strset.New()
   269  		for _, p := range c.PackagesByPath(path) {
   270  			actualIds.Add(string(p.ID()))
   271  		}
   272  
   273  		if !expectedIds.IsEqual(actualIds) {
   274  			t.Errorf("mismatched IDs for path=%q : %+v", path, strset.SymmetricDifference(actualIds, expectedIds))
   275  		}
   276  	}
   277  
   278  	// assert type index
   279  	assert.Len(t, c.idsByType, len(expectedIndexes.byType), "unexpected type index length")
   280  	for ty, expectedIds := range expectedIndexes.byType {
   281  		actualIds := strset.New()
   282  		for p := range c.Enumerate(ty) {
   283  			actualIds.Add(string(p.ID()))
   284  		}
   285  
   286  		if !expectedIds.IsEqual(actualIds) {
   287  			t.Errorf("mismatched IDs for type=%q : %+v", ty, strset.SymmetricDifference(actualIds, expectedIds))
   288  		}
   289  	}
   290  }
   291  
   292  func TestCatalog_PathIndexDeduplicatesRealVsVirtualPaths(t *testing.T) {
   293  	p1 := Package{
   294  		Locations: file.NewLocationSet(
   295  			file.NewVirtualLocation("/b/path", "/another/path"),
   296  			file.NewVirtualLocation("/b/path", "/b/path"),
   297  		),
   298  		Type: RpmPkg,
   299  		Name: "Package-1",
   300  	}
   301  
   302  	p2 := Package{
   303  		Locations: file.NewLocationSet(
   304  			file.NewVirtualLocation("/b/path", "/b/path"),
   305  		),
   306  		Type: RpmPkg,
   307  		Name: "Package-2",
   308  	}
   309  	p2Dup := Package{
   310  		Locations: file.NewLocationSet(
   311  			file.NewVirtualLocation("/b/path", "/another/path"),
   312  			file.NewVirtualLocation("/b/path", "/c/path/b/dup"),
   313  		),
   314  		Type: RpmPkg,
   315  		Name: "Package-2",
   316  	}
   317  
   318  	tests := []struct {
   319  		name  string
   320  		pkgs  []Package
   321  		paths []string
   322  	}{
   323  		{
   324  			name: "multiple locations with shared path",
   325  			pkgs: []Package{p1},
   326  			paths: []string{
   327  				"/b/path",
   328  				"/another/path",
   329  			},
   330  		},
   331  		{
   332  			name: "one location with shared path",
   333  			pkgs: []Package{p2},
   334  			paths: []string{
   335  				"/b/path",
   336  			},
   337  		},
   338  		{
   339  			name: "two instances with similar locations",
   340  			pkgs: []Package{p2, p2Dup},
   341  			paths: []string{
   342  				"/b/path",
   343  				"/another/path",
   344  				"/c/path/b/dup", // this updated the path index on merge
   345  			},
   346  		},
   347  	}
   348  
   349  	for _, test := range tests {
   350  		t.Run(test.name, func(t *testing.T) {
   351  			for _, path := range test.paths {
   352  				actualPackages := NewCollection(test.pkgs...).PackagesByPath(path)
   353  				require.Len(t, actualPackages, 1)
   354  			}
   355  		})
   356  	}
   357  
   358  }
   359  
   360  func TestCatalog_MergeRecords(t *testing.T) {
   361  	var tests = []struct {
   362  		name              string
   363  		pkgs              []Package
   364  		expectedLocations []file.Location
   365  		expectedCPECount  int
   366  	}{
   367  		{
   368  			name: "multiple Locations with shared path",
   369  			pkgs: []Package{
   370  				{
   371  					CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")},
   372  					Locations: file.NewLocationSet(
   373  						file.NewVirtualLocationFromCoordinates(
   374  							file.Coordinates{
   375  								RealPath:     "/b/path",
   376  								FileSystemID: "a",
   377  							},
   378  							"/another/path",
   379  						),
   380  					),
   381  					Type: RpmPkg,
   382  				},
   383  				{
   384  					CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")},
   385  					Locations: file.NewLocationSet(
   386  						file.NewVirtualLocationFromCoordinates(
   387  							file.Coordinates{
   388  								RealPath:     "/b/path",
   389  								FileSystemID: "b",
   390  							},
   391  							"/another/path",
   392  						),
   393  					),
   394  					Type: RpmPkg,
   395  				},
   396  			},
   397  			expectedLocations: []file.Location{
   398  				file.NewVirtualLocationFromCoordinates(
   399  					file.Coordinates{
   400  						RealPath:     "/b/path",
   401  						FileSystemID: "a",
   402  					},
   403  					"/another/path",
   404  				),
   405  				file.NewVirtualLocationFromCoordinates(
   406  					file.Coordinates{
   407  						RealPath:     "/b/path",
   408  						FileSystemID: "b",
   409  					},
   410  					"/another/path",
   411  				),
   412  			},
   413  			expectedCPECount: 2,
   414  		},
   415  	}
   416  
   417  	for _, tt := range tests {
   418  		t.Run(tt.name, func(t *testing.T) {
   419  			actual := NewCollection(tt.pkgs...).PackagesByPath("/b/path")
   420  			require.Len(t, actual, 1)
   421  			assert.Equal(t, tt.expectedLocations, actual[0].Locations.ToSlice())
   422  			require.Len(t, actual[0].CPEs, tt.expectedCPECount)
   423  		})
   424  	}
   425  }
   426  
   427  func TestCatalog_EnumerateNilCatalog(t *testing.T) {
   428  	var c *Collection
   429  	assert.Empty(t, c.Enumerate())
   430  }
   431  
   432  func Test_idOrderedSet_add(t *testing.T) {
   433  	tests := []struct {
   434  		name     string
   435  		input    []artifact.ID
   436  		expected []artifact.ID
   437  	}{
   438  		{
   439  			name: "elements deduplicated when added",
   440  			input: []artifact.ID{
   441  				"1", "2", "3", "4", "1", "2", "3", "4", "1", "2", "3", "4",
   442  			},
   443  			expected: []artifact.ID{
   444  				"1", "2", "3", "4",
   445  			},
   446  		},
   447  		{
   448  			name: "elements retain ordering when added",
   449  			input: []artifact.ID{
   450  				"4", "3", "2", "1",
   451  			},
   452  			expected: []artifact.ID{
   453  				"4", "3", "2", "1",
   454  			},
   455  		},
   456  	}
   457  	for _, tt := range tests {
   458  		t.Run(tt.name, func(t *testing.T) {
   459  			var s orderedIDSet
   460  			s.add(tt.input...)
   461  			assert.Equal(t, tt.expected, s.slice)
   462  		})
   463  	}
   464  }