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 }