github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/package_test.go (about)

     1  package pkg
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/google/go-cmp/cmp"
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/anchore/syft/syft/cpe"
    11  	"github.com/anchore/syft/syft/file"
    12  )
    13  
    14  func TestIDUniqueness(t *testing.T) {
    15  	originalLocation := file.NewVirtualLocationFromCoordinates(
    16  		file.Coordinates{
    17  			RealPath:     "39.0742° N, 21.8243° E",
    18  			FileSystemID: "Earth",
    19  		},
    20  		"/Ancient-Greece",
    21  	)
    22  
    23  	originalPkg := Package{
    24  		Name:    "pi",
    25  		Version: "3.14",
    26  		FoundBy: "Archimedes",
    27  		Locations: file.NewLocationSet(
    28  			originalLocation,
    29  		),
    30  		Licenses: NewLicenseSet(
    31  			NewLicense("MIT"),
    32  			NewLicense("cc0-1.0"),
    33  		),
    34  		Language: "math",
    35  		Type:     PythonPkg,
    36  		CPEs: []cpe.CPE{
    37  			cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
    38  		},
    39  		PURL: "pkg:pypi/pi@3.14",
    40  		Metadata: PythonPackage{
    41  			Name:                 "pi",
    42  			Version:              "3.14",
    43  			Author:               "Archimedes",
    44  			AuthorEmail:          "Archimedes@circles.io",
    45  			Platform:             "universe",
    46  			SitePackagesRootPath: "Pi",
    47  		},
    48  	}
    49  
    50  	// this is a set of differential tests, ensuring that select mutations are reflected in the fingerprint (or not)
    51  	tests := []struct {
    52  		name                 string
    53  		transform            func(pkg Package) Package
    54  		expectedIDComparison assert.ComparisonAssertionFunc
    55  	}{
    56  		{
    57  			name: "go case (no transform)",
    58  			transform: func(pkg Package) Package {
    59  				// do nothing!
    60  				return pkg
    61  			},
    62  			expectedIDComparison: assert.Equal,
    63  		},
    64  		{
    65  			name: "same metadata is ignored",
    66  			transform: func(pkg Package) Package {
    67  				// note: this is the same as the original values, just a new allocation
    68  				pkg.Metadata = PythonPackage{
    69  					Name:                 "pi",
    70  					Version:              "3.14",
    71  					Author:               "Archimedes",
    72  					AuthorEmail:          "Archimedes@circles.io",
    73  					Platform:             "universe",
    74  					SitePackagesRootPath: "Pi",
    75  				}
    76  				return pkg
    77  			},
    78  			expectedIDComparison: assert.Equal,
    79  		},
    80  		{
    81  			name: "licenses order is ignored",
    82  			transform: func(pkg Package) Package {
    83  				// note: same as the original package, only a different order
    84  				pkg.Licenses = NewLicenseSet(
    85  					NewLicense("cc0-1.0"),
    86  					NewLicense("MIT"),
    87  				)
    88  				return pkg
    89  			},
    90  			expectedIDComparison: assert.Equal,
    91  		},
    92  		{
    93  			name: "name is reflected",
    94  			transform: func(pkg Package) Package {
    95  				pkg.Name = "new!"
    96  				return pkg
    97  			},
    98  			expectedIDComparison: assert.NotEqual,
    99  		},
   100  		{
   101  			name: "location is reflected",
   102  			transform: func(pkg Package) Package {
   103  				locations := file.NewLocationSet(pkg.Locations.ToSlice()...)
   104  				locations.Add(file.NewLocation("/somewhere/new"))
   105  				pkg.Locations = locations
   106  				return pkg
   107  			},
   108  			expectedIDComparison: assert.NotEqual,
   109  		},
   110  		{
   111  			name: "licenses is reflected",
   112  			transform: func(pkg Package) Package {
   113  				pkg.Licenses = NewLicenseSet(NewLicense("new!"))
   114  				return pkg
   115  			},
   116  			expectedIDComparison: assert.NotEqual,
   117  		},
   118  		{
   119  			name: "same path for different filesystem is NOT reflected",
   120  			transform: func(pkg Package) Package {
   121  				newLocation := originalLocation
   122  				newLocation.FileSystemID = "Mars"
   123  
   124  				pkg.Locations = file.NewLocationSet(newLocation)
   125  				return pkg
   126  			},
   127  			expectedIDComparison: assert.Equal,
   128  		},
   129  		{
   130  			name: "multiple equivalent paths for different filesystem is NOT reflected",
   131  			transform: func(pkg Package) Package {
   132  				newLocation := originalLocation
   133  				newLocation.FileSystemID = "Mars"
   134  
   135  				locations := file.NewLocationSet(pkg.Locations.ToSlice()...)
   136  				locations.Add(newLocation, originalLocation)
   137  
   138  				pkg.Locations = locations
   139  				return pkg
   140  			},
   141  			expectedIDComparison: assert.Equal,
   142  		},
   143  		{
   144  			name: "version is reflected",
   145  			transform: func(pkg Package) Package {
   146  				pkg.Version = "new!"
   147  				return pkg
   148  			},
   149  			expectedIDComparison: assert.NotEqual,
   150  		},
   151  		{
   152  			name: "type is reflected",
   153  			transform: func(pkg Package) Package {
   154  				pkg.Type = RustPkg
   155  				return pkg
   156  			},
   157  			expectedIDComparison: assert.NotEqual,
   158  		},
   159  		{
   160  			name: "CPEs is ignored",
   161  			transform: func(pkg Package) Package {
   162  				pkg.CPEs = []cpe.CPE{}
   163  				return pkg
   164  			},
   165  			expectedIDComparison: assert.Equal,
   166  		},
   167  		{
   168  			name: "pURL is ignored",
   169  			transform: func(pkg Package) Package {
   170  				pkg.PURL = "new!"
   171  				return pkg
   172  			},
   173  			expectedIDComparison: assert.Equal,
   174  		},
   175  		{
   176  			name: "language is NOT reflected",
   177  			transform: func(pkg Package) Package {
   178  				pkg.Language = Rust
   179  				return pkg
   180  			},
   181  			expectedIDComparison: assert.Equal,
   182  		},
   183  		{
   184  			name: "metadata mutation is reflected",
   185  			transform: func(pkg Package) Package {
   186  				metadata := pkg.Metadata.(PythonPackage)
   187  				metadata.Name = "new!"
   188  				pkg.Metadata = metadata
   189  				return pkg
   190  			},
   191  			expectedIDComparison: assert.NotEqual,
   192  		},
   193  		{
   194  			name: "new metadata is reflected",
   195  			transform: func(pkg Package) Package {
   196  				pkg.Metadata = PythonPackage{
   197  					Name: "new!",
   198  				}
   199  				return pkg
   200  			},
   201  			expectedIDComparison: assert.NotEqual,
   202  		},
   203  		{
   204  			name: "nil metadata is reflected",
   205  			transform: func(pkg Package) Package {
   206  				pkg.Metadata = nil
   207  				return pkg
   208  			},
   209  			expectedIDComparison: assert.NotEqual,
   210  		},
   211  	}
   212  
   213  	for _, test := range tests {
   214  		t.Run(test.name, func(t *testing.T) {
   215  			originalPkg.SetID()
   216  			transformedPkg := test.transform(originalPkg)
   217  			transformedPkg.SetID()
   218  
   219  			originalFingerprint := originalPkg.ID()
   220  			assert.NotEmpty(t, originalFingerprint)
   221  			transformedFingerprint := transformedPkg.ID()
   222  			assert.NotEmpty(t, transformedFingerprint)
   223  
   224  			test.expectedIDComparison(t, originalFingerprint, transformedFingerprint)
   225  		})
   226  	}
   227  }
   228  
   229  func TestPackage_Merge(t *testing.T) {
   230  	originalLocation := file.NewVirtualLocationFromCoordinates(
   231  		file.Coordinates{
   232  			RealPath:     "39.0742° N, 21.8243° E",
   233  			FileSystemID: "Earth",
   234  		},
   235  		"/Ancient-Greece",
   236  	)
   237  
   238  	similarLocation := originalLocation
   239  	similarLocation.FileSystemID = "Mars"
   240  
   241  	tests := []struct {
   242  		name     string
   243  		subject  Package
   244  		other    Package
   245  		expected *Package
   246  	}{
   247  		{
   248  			name: "merge two packages (different cpes + locations)",
   249  			subject: Package{
   250  				Name:    "pi",
   251  				Version: "3.14",
   252  				FoundBy: "Archimedes",
   253  				Locations: file.NewLocationSet(
   254  					originalLocation,
   255  				),
   256  				Language: "math",
   257  				Type:     PythonPkg,
   258  				CPEs: []cpe.CPE{
   259  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   260  				},
   261  				PURL: "pkg:pypi/pi@3.14",
   262  				Metadata: PythonPackage{
   263  					Name:                 "pi",
   264  					Version:              "3.14",
   265  					Author:               "Archimedes",
   266  					AuthorEmail:          "Archimedes@circles.io",
   267  					Platform:             "universe",
   268  					SitePackagesRootPath: "Pi",
   269  				},
   270  			},
   271  			other: Package{
   272  				Name:    "pi",
   273  				Version: "3.14",
   274  				FoundBy: "Archimedes",
   275  				Locations: file.NewLocationSet(
   276  					similarLocation, // NOTE: difference; we have a different layer but the same path
   277  				),
   278  				Language: "math",
   279  				Type:     PythonPkg,
   280  				CPEs: []cpe.CPE{
   281  					cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: difference
   282  				},
   283  				PURL: "pkg:pypi/pi@3.14",
   284  				Metadata: PythonPackage{
   285  					Name:                 "pi",
   286  					Version:              "3.14",
   287  					Author:               "Archimedes",
   288  					AuthorEmail:          "Archimedes@circles.io",
   289  					Platform:             "universe",
   290  					SitePackagesRootPath: "Pi",
   291  				},
   292  			},
   293  			expected: &Package{
   294  				Name:    "pi",
   295  				Version: "3.14",
   296  				FoundBy: "Archimedes",
   297  				Locations: file.NewLocationSet(
   298  					originalLocation,
   299  					similarLocation, // NOTE: merge!
   300  				),
   301  				Language: "math",
   302  				Type:     PythonPkg,
   303  				CPEs: []cpe.CPE{
   304  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   305  					cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: merge!
   306  				},
   307  				PURL: "pkg:pypi/pi@3.14",
   308  				Metadata: PythonPackage{
   309  					Name:                 "pi",
   310  					Version:              "3.14",
   311  					Author:               "Archimedes",
   312  					AuthorEmail:          "Archimedes@circles.io",
   313  					Platform:             "universe",
   314  					SitePackagesRootPath: "Pi",
   315  				},
   316  			},
   317  		},
   318  		{
   319  			name: "error when there are different IDs",
   320  			subject: Package{
   321  				Name:    "pi",
   322  				Version: "3.14",
   323  				FoundBy: "Archimedes",
   324  				Locations: file.NewLocationSet(
   325  					originalLocation,
   326  				),
   327  				Language: "math",
   328  				Type:     PythonPkg,
   329  				CPEs: []cpe.CPE{
   330  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   331  				},
   332  				PURL: "pkg:pypi/pi@3.14",
   333  				Metadata: PythonPackage{
   334  					Name:                 "pi",
   335  					Version:              "3.14",
   336  					Author:               "Archimedes",
   337  					AuthorEmail:          "Archimedes@circles.io",
   338  					Platform:             "universe",
   339  					SitePackagesRootPath: "Pi",
   340  				},
   341  			},
   342  			other: Package{
   343  				Name:    "pi-DIFFERENT", // difference
   344  				Version: "3.14",
   345  				FoundBy: "Archimedes",
   346  				Locations: file.NewLocationSet(
   347  					originalLocation,
   348  				),
   349  				Language: "math",
   350  				Type:     PythonPkg,
   351  				CPEs: []cpe.CPE{
   352  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   353  				},
   354  				PURL: "pkg:pypi/pi@3.14",
   355  				Metadata: PythonPackage{
   356  					Name:                 "pi",
   357  					Version:              "3.14",
   358  					Author:               "Archimedes",
   359  					AuthorEmail:          "Archimedes@circles.io",
   360  					Platform:             "universe",
   361  					SitePackagesRootPath: "Pi",
   362  				},
   363  			},
   364  		},
   365  	}
   366  	for _, tt := range tests {
   367  		t.Run(tt.name, func(t *testing.T) {
   368  			tt.subject.SetID()
   369  			tt.other.SetID()
   370  
   371  			err := tt.subject.merge(tt.other)
   372  			if tt.expected == nil {
   373  				require.Error(t, err)
   374  				return
   375  			}
   376  			require.NoError(t, err)
   377  
   378  			tt.expected.SetID()
   379  			require.Equal(t, tt.expected.id, tt.subject.id)
   380  
   381  			if diff := cmp.Diff(*tt.expected, tt.subject,
   382  				cmp.AllowUnexported(Package{}),
   383  				cmp.Comparer(
   384  					func(x, y file.LocationSet) bool {
   385  						xs := x.ToSlice()
   386  						ys := y.ToSlice()
   387  
   388  						if len(xs) != len(ys) {
   389  							return false
   390  						}
   391  						for i, xe := range xs {
   392  							ye := ys[i]
   393  							if !locationComparer(xe, ye) {
   394  								return false
   395  							}
   396  						}
   397  
   398  						return true
   399  					},
   400  				),
   401  				cmp.Comparer(
   402  					func(x, y LicenseSet) bool {
   403  						xs := x.ToSlice()
   404  						ys := y.ToSlice()
   405  
   406  						if len(xs) != len(ys) {
   407  							return false
   408  						}
   409  						for i, xe := range xs {
   410  							ye := ys[i]
   411  							if !licenseComparer(xe, ye) {
   412  								return false
   413  							}
   414  						}
   415  
   416  						return true
   417  					},
   418  				),
   419  				cmp.Comparer(locationComparer),
   420  			); diff != "" {
   421  				t.Errorf("unexpected result from parsing (-expected +actual)\n%s", diff)
   422  			}
   423  		})
   424  	}
   425  }
   426  
   427  func licenseComparer(x, y License) bool {
   428  	return cmp.Equal(x, y, cmp.Comparer(locationComparer))
   429  }
   430  
   431  func locationComparer(x, y file.Location) bool {
   432  	return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
   433  }
   434  
   435  func TestIsValid(t *testing.T) {
   436  	cases := []struct {
   437  		name  string
   438  		given *Package
   439  		want  bool
   440  	}{
   441  		{
   442  			name:  "nil",
   443  			given: nil,
   444  			want:  false,
   445  		},
   446  		{
   447  			name:  "has-name",
   448  			given: &Package{Name: "paul"},
   449  			want:  true,
   450  		},
   451  		{
   452  			name:  "has-no-name",
   453  			given: &Package{},
   454  			want:  false,
   455  		},
   456  	}
   457  
   458  	for _, c := range cases {
   459  		require.Equal(t, c.want, IsValid(c.given), "when package: %s", c.name)
   460  	}
   461  }