github.com/anchore/syft@v1.38.2/syft/format/internal/cyclonedxutil/helpers/component_test.go (about) 1 package helpers 2 3 import ( 4 "fmt" 5 "reflect" 6 "testing" 7 8 "github.com/CycloneDX/cyclonedx-go" 9 "github.com/stretchr/testify/assert" 10 11 "github.com/anchore/syft/syft/file" 12 "github.com/anchore/syft/syft/pkg" 13 ) 14 15 func Test_encodeComponentProperties(t *testing.T) { 16 epoch := 2 17 tests := []struct { 18 name string 19 input pkg.Package 20 expected []cyclonedx.Property 21 }{ 22 { 23 name: "no metadata", 24 input: pkg.Package{}, 25 expected: nil, 26 }, 27 { 28 name: "from apk", 29 input: pkg.Package{ 30 FoundBy: "cataloger", 31 Locations: file.NewLocationSet( 32 file.NewLocationFromCoordinates(file.Coordinates{RealPath: "test"}), 33 ), 34 Metadata: pkg.ApkDBEntry{ 35 Package: "libc-utils", 36 OriginPackage: "libc-dev", 37 Maintainer: "Natanael Copa <ncopa@alpinelinux.org>", 38 Version: "0.7.2-r0", 39 Architecture: "x86_64", 40 URL: "http://alpinelinux.org", 41 Description: "Meta package to pull in correct libc", 42 Size: 0, 43 InstalledSize: 4096, 44 Dependencies: []string{"musl-utils"}, 45 Provides: []string{"so:libc.so.1"}, 46 Checksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=", 47 GitCommit: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479", 48 Files: []pkg.ApkFileRecord{}, 49 }, 50 }, 51 expected: []cyclonedx.Property{ 52 {Name: "syft:package:foundBy", Value: "cataloger"}, 53 {Name: "syft:package:metadataType", Value: "apk-db-entry"}, 54 {Name: "syft:location:0:path", Value: "test"}, 55 {Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"}, 56 {Name: "syft:metadata:installedSize", Value: "4096"}, 57 {Name: "syft:metadata:originPackage", Value: "libc-dev"}, 58 {Name: "syft:metadata:provides:0", Value: "so:libc.so.1"}, 59 {Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="}, 60 {Name: "syft:metadata:pullDependencies:0", Value: "musl-utils"}, 61 {Name: "syft:metadata:size", Value: "0"}, 62 }, 63 }, 64 { 65 name: "from dpkg", 66 input: pkg.Package{ 67 Metadata: pkg.DpkgDBEntry{ 68 Package: "tzdata", 69 Version: "2020a-0+deb10u1", 70 Source: "tzdata-dev", 71 SourceVersion: "1.0", 72 Architecture: "all", 73 InstalledSize: 3036, 74 Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>", 75 Files: []pkg.DpkgFileRecord{}, 76 }, 77 }, 78 expected: []cyclonedx.Property{ 79 {Name: "syft:package:metadataType", Value: "dpkg-db-entry"}, 80 {Name: "syft:metadata:installedSize", Value: "3036"}, 81 {Name: "syft:metadata:source", Value: "tzdata-dev"}, 82 {Name: "syft:metadata:sourceVersion", Value: "1.0"}, 83 }, 84 }, 85 { 86 name: "from go bin", 87 input: pkg.Package{ 88 Name: "golang.org/x/net", 89 Version: "v0.0.0-20211006190231-62292e806868", 90 Language: pkg.Go, 91 Type: pkg.GoModulePkg, 92 Metadata: pkg.GolangBinaryBuildinfoEntry{ 93 GoCompiledVersion: "1.17", 94 Architecture: "amd64", 95 H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=", 96 }, 97 }, 98 expected: []cyclonedx.Property{ 99 {Name: "syft:package:language", Value: pkg.Go.String()}, 100 {Name: "syft:package:metadataType", Value: "go-module-buildinfo-entry"}, 101 {Name: "syft:package:type", Value: "go-module"}, 102 {Name: "syft:metadata:architecture", Value: "amd64"}, 103 {Name: "syft:metadata:goCompiledVersion", Value: "1.17"}, 104 {Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="}, 105 }, 106 }, 107 { 108 name: "from go mod", 109 input: pkg.Package{ 110 Name: "golang.org/x/net", 111 Version: "v0.0.0-20211006190231-62292e806868", 112 Language: pkg.Go, 113 Type: pkg.GoModulePkg, 114 Metadata: pkg.GolangModuleEntry{ 115 H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=", 116 }, 117 }, 118 expected: []cyclonedx.Property{ 119 {Name: "syft:package:language", Value: pkg.Go.String()}, 120 {Name: "syft:package:metadataType", Value: "go-module-entry"}, 121 {Name: "syft:package:type", Value: "go-module"}, 122 {Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="}, 123 }, 124 }, 125 { 126 name: "from rpm", 127 input: pkg.Package{ 128 Name: "dive", 129 Version: "0.9.2-1", 130 Type: pkg.RpmPkg, 131 Metadata: pkg.RpmDBEntry{ 132 Name: "dive", 133 Epoch: &epoch, 134 Arch: "x86_64", 135 Release: "1", 136 Version: "0.9.2", 137 SourceRpm: "dive-0.9.2-1.src.rpm", 138 Size: 12406784, 139 Vendor: "", 140 Files: []pkg.RpmFileRecord{}, 141 }, 142 }, 143 expected: []cyclonedx.Property{ 144 {Name: "syft:package:metadataType", Value: "rpm-db-entry"}, 145 {Name: "syft:package:type", Value: "rpm"}, 146 {Name: "syft:metadata:epoch", Value: "2"}, 147 {Name: "syft:metadata:release", Value: "1"}, 148 {Name: "syft:metadata:size", Value: "12406784"}, 149 {Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"}, 150 }, 151 }, 152 } 153 for _, test := range tests { 154 t.Run(test.name, func(t *testing.T) { 155 sbomSupplier := "" 156 c := EncodeComponent(test.input, sbomSupplier, file.LocationSorter(nil)) 157 if test.expected == nil { 158 if c.Properties != nil { 159 t.Fatalf("expected no properties, got: %+v", *c.Properties) 160 } 161 return 162 } 163 assert.ElementsMatch(t, test.expected, *c.Properties) 164 }) 165 } 166 } 167 168 func Test_encodeCompomentType(t *testing.T) { 169 tests := []struct { 170 name string 171 pkg pkg.Package 172 want cyclonedx.Component 173 }{ 174 { 175 name: "non-binary package", 176 pkg: pkg.Package{ 177 Name: "pkg1", 178 Version: "1.9.2", 179 Type: pkg.GoModulePkg, 180 }, 181 want: cyclonedx.Component{ 182 Name: "pkg1", 183 Version: "1.9.2", 184 Type: cyclonedx.ComponentTypeLibrary, 185 Properties: &[]cyclonedx.Property{ 186 { 187 Name: "syft:package:type", 188 Value: "go-module", 189 }, 190 }, 191 }, 192 }, 193 { 194 name: "non-binary package", 195 pkg: pkg.Package{ 196 Name: "pkg1", 197 Version: "3.1.2", 198 Type: pkg.BinaryPkg, 199 }, 200 want: cyclonedx.Component{ 201 Name: "pkg1", 202 Version: "3.1.2", 203 Type: cyclonedx.ComponentTypeApplication, 204 Properties: &[]cyclonedx.Property{ 205 { 206 Name: "syft:package:type", 207 Value: "binary", 208 }, 209 }, 210 }, 211 }, 212 } 213 for _, tt := range tests { 214 t.Run(tt.name, func(t *testing.T) { 215 tt.pkg.ID() 216 sbomSupplier := "" 217 p := EncodeComponent(tt.pkg, sbomSupplier, file.LocationSorter(nil)) 218 assert.Equal(t, tt.want, p) 219 }) 220 } 221 } 222 223 func Test_deriveBomRef(t *testing.T) { 224 pkgWithPurl := pkg.Package{ 225 Name: "django", 226 Version: "1.11.1", 227 PURL: "pkg:pypi/django@1.11.1", 228 } 229 pkgWithPurl.SetID() 230 231 pkgWithOutPurl := pkg.Package{ 232 Name: "django", 233 Version: "1.11.1", 234 PURL: "", 235 } 236 pkgWithOutPurl.SetID() 237 238 pkgWithBadPurl := pkg.Package{ 239 Name: "django", 240 Version: "1.11.1", 241 PURL: "pkg:pyjango@1.11.1", 242 } 243 pkgWithBadPurl.SetID() 244 245 tests := []struct { 246 name string 247 pkg pkg.Package 248 want string 249 }{ 250 { 251 name: "use pURL-id hybrid", 252 pkg: pkgWithPurl, 253 want: fmt.Sprintf("pkg:pypi/django@1.11.1?package-id=%s", pkgWithPurl.ID()), 254 }, 255 { 256 name: "fallback to ID when pURL is invalid", 257 pkg: pkgWithBadPurl, 258 want: string(pkgWithBadPurl.ID()), 259 }, 260 { 261 name: "fallback to ID when pURL is missing", 262 pkg: pkgWithOutPurl, 263 want: string(pkgWithOutPurl.ID()), 264 }, 265 } 266 for _, tt := range tests { 267 t.Run(tt.name, func(t *testing.T) { 268 tt.pkg.ID() 269 assert.Equal(t, tt.want, DeriveBomRef(tt.pkg)) 270 }) 271 } 272 } 273 274 func Test_decodeComponent(t *testing.T) { 275 tests := []struct { 276 name string 277 component cyclonedx.Component 278 wantLanguage pkg.Language 279 wantMetadata any 280 wantPURL string 281 }{ 282 { 283 name: "derive language from pURL if missing", 284 component: cyclonedx.Component{ 285 Name: "ch.qos.logback/logback-classic", 286 Version: "1.2.3", 287 PackageURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", 288 Type: "library", 289 BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", 290 }, 291 wantLanguage: pkg.Java, 292 wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", 293 }, 294 { 295 name: "derive language from bomref if missing", 296 component: cyclonedx.Component{ 297 Name: "ch.qos.logback/logback-classic", 298 Version: "1.2.3", 299 Type: "library", 300 BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", 301 }, 302 wantLanguage: pkg.Java, 303 wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3", 304 }, 305 { 306 name: "handle RpmdbMetadata type without properties", 307 component: cyclonedx.Component{ 308 Name: "acl", 309 Version: "2.2.53-1.el8", 310 PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", 311 Type: "library", 312 BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", 313 Properties: &[]cyclonedx.Property{ 314 { 315 Name: "syft:package:metadataType", 316 Value: "RpmdbMetadata", 317 }, 318 }, 319 }, 320 wantMetadata: pkg.RpmDBEntry{}, 321 wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", 322 }, 323 { 324 name: "handle RpmdbMetadata type with properties", 325 component: cyclonedx.Component{ 326 Name: "acl", 327 Version: "2.2.53-1.el8", 328 PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", 329 Type: "library", 330 BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", 331 Properties: &[]cyclonedx.Property{ 332 { 333 Name: "syft:package:metadataType", 334 Value: "RpmDBMetadata", 335 }, 336 { 337 Name: "syft:metadata:release", 338 Value: "some-release", 339 }, 340 }, 341 }, 342 wantMetadata: pkg.RpmDBEntry{ 343 Release: "some-release", 344 }, 345 wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8", 346 }, 347 { 348 name: "generate a purl from package type", 349 component: cyclonedx.Component{ 350 Name: "log4j", 351 Group: "org.apache.logging.log4j", 352 Version: "2.0.4", 353 Type: "library", 354 BOMRef: "log4j", 355 Properties: &[]cyclonedx.Property{ 356 { 357 Name: "syft:package:type", 358 Value: "java-archive", 359 }, 360 }, 361 }, 362 wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4", 363 }, 364 } 365 366 for _, tt := range tests { 367 t.Run(tt.name, func(t *testing.T) { 368 p := decodeComponent(&tt.component) 369 if tt.wantLanguage != "" { 370 assert.Equal(t, tt.wantLanguage, p.Language) 371 } 372 if tt.wantMetadata != nil { 373 assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata) 374 } 375 376 if tt.wantPURL != "" { 377 assert.Equal(t, tt.wantPURL, p.PURL, "purl should match") 378 } 379 380 if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" { 381 t.Fatal("this is a useless test, please remove it") 382 } 383 }) 384 } 385 } 386 387 func TestGetPURL(t *testing.T) { 388 tests := []struct { 389 name string 390 component *cyclonedx.Component 391 pkgType pkg.Type 392 expected string 393 }{ 394 { 395 name: "component with PackageURL", 396 component: &cyclonedx.Component{ 397 PackageURL: "pkg:npm/lodash@4.17.21", 398 Name: "lodash", 399 Version: "4.17.20", // different version to verify PackageURL is used 400 Group: "npm", 401 }, 402 pkgType: pkg.NpmPkg, 403 expected: "pkg:npm/lodash@4.17.21", 404 }, 405 { 406 name: "component with BOMRef as valid PURL", 407 component: &cyclonedx.Component{ 408 BOMRef: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", 409 Name: "commons-lang3", 410 Version: "3.11.0", // different version to verify BOMRef is used 411 Group: "org.apache.commons", 412 }, 413 pkgType: pkg.JavaPkg, 414 expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", 415 }, 416 { 417 name: "component with BOMRef not a valid PURL", 418 component: &cyclonedx.Component{ 419 BOMRef: "not-a-purl-ref", 420 Name: "commons-lang3", 421 Version: "3.12.0", 422 Group: "org.apache.commons", 423 }, 424 pkgType: pkg.JavaPkg, 425 expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", 426 }, 427 { 428 name: "component with unknown package type", 429 component: &cyclonedx.Component{ 430 Name: "some-component", 431 Version: "1.0.0", 432 Group: "org.example", 433 }, 434 pkgType: pkg.UnknownPkg, 435 expected: "", 436 }, 437 { 438 name: "component with empty package type", 439 component: &cyclonedx.Component{ 440 Name: "some-component", 441 Version: "1.0.0", 442 Group: "org.example", 443 }, 444 pkgType: "", 445 expected: "", 446 }, 447 { 448 name: "component with generic package type", 449 component: &cyclonedx.Component{ 450 Name: "some-component", 451 Version: "1.0.0", 452 Group: "org.example", 453 }, 454 pkgType: pkg.LinuxKernelModulePkg, 455 expected: "", 456 }, 457 { 458 name: "component with valid package type", 459 component: &cyclonedx.Component{ 460 Name: "react", 461 Version: "18.2.0", 462 Group: "facebook", 463 }, 464 pkgType: pkg.NpmPkg, 465 expected: "pkg:npm/facebook/react@18.2.0", 466 }, 467 { 468 name: "component with no group", 469 component: &cyclonedx.Component{ 470 Name: "express", 471 Version: "4.18.2", 472 }, 473 pkgType: pkg.NpmPkg, 474 expected: "pkg:npm/express@4.18.2", 475 }, 476 { 477 name: "component with no version", 478 component: &cyclonedx.Component{ 479 Name: "express", 480 Group: "npm", 481 }, 482 pkgType: pkg.NpmPkg, 483 expected: "pkg:npm/npm/express", 484 }, 485 } 486 487 for _, tt := range tests { 488 t.Run(tt.name, func(t *testing.T) { 489 result := getPURL(tt.component, tt.pkgType) 490 assert.Equal(t, tt.expected, result) 491 }) 492 } 493 }