github.com/anchore/syft@v1.38.2/syft/pkg/collection_test.go (about)

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