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