github.com/anchore/syft@v1.38.2/syft/format/common/cyclonedxhelpers/to_format_model_test.go (about) 1 package cyclonedxhelpers 2 3 import ( 4 "fmt" 5 "testing" 6 7 "github.com/CycloneDX/cyclonedx-go" 8 "github.com/google/go-cmp/cmp" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 stfile "github.com/anchore/stereoscope/pkg/file" 13 "github.com/anchore/syft/syft/artifact" 14 "github.com/anchore/syft/syft/file" 15 "github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers" 16 "github.com/anchore/syft/syft/linux" 17 "github.com/anchore/syft/syft/pkg" 18 "github.com/anchore/syft/syft/sbom" 19 "github.com/anchore/syft/syft/source" 20 ) 21 22 func Test_formatCPE(t *testing.T) { 23 tests := []struct { 24 cpe string 25 expected string 26 }{ 27 { 28 cpe: "cpe:2.3:o:amazon:amazon_linux:2", 29 expected: "cpe:2.3:o:amazon:amazon_linux:2:*:*:*:*:*:*:*", 30 }, 31 { 32 cpe: "cpe:/o:opensuse:leap:15.2", 33 expected: "cpe:2.3:o:opensuse:leap:15.2:*:*:*:*:*:*:*", 34 }, 35 { 36 cpe: "invalid-cpe", 37 expected: "", 38 }, 39 } 40 41 for _, test := range tests { 42 t.Run(test.cpe, func(t *testing.T) { 43 out := formatCPE(test.cpe) 44 assert.Equal(t, test.expected, out) 45 }) 46 } 47 } 48 49 func Test_relationships(t *testing.T) { 50 p1 := pkg.Package{ 51 Name: "p1", 52 } 53 54 p2 := pkg.Package{ 55 Name: "p2", 56 } 57 58 p3 := pkg.Package{ 59 Name: "p3", 60 } 61 62 p4 := pkg.Package{ 63 Name: "p4", 64 } 65 66 for _, p := range []*pkg.Package{&p1, &p2, &p3, &p4} { 67 p.PURL = fmt.Sprintf("pkg:generic/%s@%s", p.Name, p.Name) 68 p.SetID() 69 } 70 71 tests := []struct { 72 name string 73 sbom sbom.SBOM 74 expected *[]cyclonedx.Dependency 75 }{ 76 { 77 name: "package dependencyOf relationships output as dependencies", 78 sbom: sbom.SBOM{ 79 Artifacts: sbom.Artifacts{ 80 Packages: pkg.NewCollection(p1, p2, p3, p4), 81 }, 82 Relationships: []artifact.Relationship{ 83 { 84 From: p2, 85 To: p1, 86 Type: artifact.DependencyOfRelationship, 87 }, 88 { 89 From: p3, 90 To: p1, 91 Type: artifact.DependencyOfRelationship, 92 }, 93 { 94 From: p4, 95 To: p2, 96 Type: artifact.DependencyOfRelationship, 97 }, 98 }, 99 }, 100 expected: &[]cyclonedx.Dependency{ 101 { 102 Ref: helpers.DeriveBomRef(p1), 103 Dependencies: &[]string{ 104 helpers.DeriveBomRef(p2), 105 helpers.DeriveBomRef(p3), 106 }, 107 }, 108 { 109 Ref: helpers.DeriveBomRef(p2), 110 Dependencies: &[]string{ 111 helpers.DeriveBomRef(p4), 112 }, 113 }, 114 }, 115 }, 116 { 117 name: "package contains relationships not output", 118 sbom: sbom.SBOM{ 119 Artifacts: sbom.Artifacts{ 120 Packages: pkg.NewCollection(p1, p2, p3), 121 }, 122 Relationships: []artifact.Relationship{ 123 { 124 From: p2, 125 To: p1, 126 Type: artifact.ContainsRelationship, 127 }, 128 { 129 From: p3, 130 To: p1, 131 Type: artifact.ContainsRelationship, 132 }, 133 }, 134 }, 135 expected: nil, 136 }, 137 } 138 139 for _, test := range tests { 140 t.Run(test.name, func(t *testing.T) { 141 cdx := ToFormatModel(test.sbom) 142 got := cdx.Dependencies 143 require.Equal(t, test.expected, got) 144 }) 145 } 146 } 147 148 func Test_FileComponents(t *testing.T) { 149 p1 := pkg.Package{ 150 Name: "p1", 151 } 152 tests := []struct { 153 name string 154 sbom sbom.SBOM 155 want []cyclonedx.Component 156 }{ 157 { 158 name: "sbom coordinates with file metadata are serialized to cdx along with packages", 159 sbom: sbom.SBOM{ 160 Artifacts: sbom.Artifacts{ 161 Packages: pkg.NewCollection(p1), 162 FileMetadata: map[file.Coordinates]file.Metadata{ 163 {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, 164 }, 165 FileDigests: map[file.Coordinates][]file.Digest{ 166 {RealPath: "/test"}: { 167 { 168 Algorithm: "sha256", 169 Value: "xyz12345", 170 }, 171 }, 172 }, 173 }, 174 }, 175 want: []cyclonedx.Component{ 176 { 177 BOMRef: "2a1fc74ade23e357", 178 Type: cyclonedx.ComponentTypeLibrary, 179 Name: "p1", 180 }, 181 { 182 BOMRef: "3f31cb2d98be6c1e", 183 Name: "/test", 184 Type: cyclonedx.ComponentTypeFile, 185 Hashes: &[]cyclonedx.Hash{ 186 {Algorithm: "SHA-256", Value: "xyz12345"}, 187 }, 188 }, 189 }, 190 }, 191 { 192 name: "sbom coordinates that don't contain metadata are not added to the final output", 193 sbom: sbom.SBOM{ 194 Artifacts: sbom.Artifacts{ 195 FileMetadata: map[file.Coordinates]file.Metadata{ 196 {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, 197 }, 198 FileDigests: map[file.Coordinates][]file.Digest{ 199 {RealPath: "/test"}: { 200 { 201 Algorithm: "sha256", 202 Value: "xyz12345", 203 }, 204 }, 205 {RealPath: "/test-2"}: { 206 { 207 Algorithm: "sha256", 208 Value: "xyz678910", 209 }, 210 }, 211 }, 212 }, 213 }, 214 want: []cyclonedx.Component{ 215 { 216 BOMRef: "3f31cb2d98be6c1e", 217 Name: "/test", 218 Type: cyclonedx.ComponentTypeFile, 219 Hashes: &[]cyclonedx.Hash{ 220 {Algorithm: "SHA-256", Value: "xyz12345"}, 221 }, 222 }, 223 }, 224 }, 225 { 226 name: "sbom coordinates that return hashes not covered by cdx only include valid digests", 227 sbom: sbom.SBOM{ 228 Artifacts: sbom.Artifacts{ 229 FileMetadata: map[file.Coordinates]file.Metadata{ 230 {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, 231 }, 232 FileDigests: map[file.Coordinates][]file.Digest{ 233 {RealPath: "/test"}: { 234 { 235 Algorithm: "xxh64", 236 Value: "xyz12345", 237 }, 238 { 239 Algorithm: "sha256", 240 Value: "xyz678910", 241 }, 242 }, 243 }, 244 }, 245 }, 246 want: []cyclonedx.Component{ 247 { 248 BOMRef: "3f31cb2d98be6c1e", 249 Name: "/test", 250 Type: cyclonedx.ComponentTypeFile, 251 Hashes: &[]cyclonedx.Hash{ 252 {Algorithm: "SHA-256", Value: "xyz678910"}, 253 }, 254 }, 255 }, 256 }, 257 { 258 name: "sbom coordinates who's metadata is directory or symlink are skipped", 259 sbom: sbom.SBOM{ 260 Artifacts: sbom.Artifacts{ 261 FileMetadata: map[file.Coordinates]file.Metadata{ 262 {RealPath: "/testdir"}: { 263 Path: "/testdir", 264 Type: stfile.TypeDirectory, 265 }, 266 {RealPath: "/testsym"}: { 267 Path: "/testsym", 268 Type: stfile.TypeSymLink, 269 }, 270 {RealPath: "/test"}: {Path: "/test", Type: stfile.TypeRegular}, 271 }, 272 FileDigests: map[file.Coordinates][]file.Digest{ 273 {RealPath: "/test"}: { 274 { 275 Algorithm: "sha256", 276 Value: "xyz12345", 277 }, 278 }, 279 }, 280 }, 281 }, 282 want: []cyclonedx.Component{ 283 { 284 BOMRef: "3f31cb2d98be6c1e", 285 Name: "/test", 286 Type: cyclonedx.ComponentTypeFile, 287 Hashes: &[]cyclonedx.Hash{ 288 {Algorithm: "SHA-256", Value: "xyz12345"}, 289 }, 290 }, 291 }, 292 }, 293 { 294 name: "sbom with no files serialized correctly", 295 sbom: sbom.SBOM{ 296 Artifacts: sbom.Artifacts{ 297 Packages: pkg.NewCollection(p1), 298 }, 299 }, 300 want: []cyclonedx.Component{ 301 { 302 BOMRef: "2a1fc74ade23e357", 303 Type: cyclonedx.ComponentTypeLibrary, 304 Name: "p1", 305 }, 306 }, 307 }, 308 } 309 for _, test := range tests { 310 t.Run(test.name, func(t *testing.T) { 311 cdx := ToFormatModel(test.sbom) 312 got := *cdx.Components 313 if diff := cmp.Diff(test.want, got); diff != "" { 314 t.Errorf("cdx file components mismatch (-want +got):\n%s", diff) 315 } 316 }) 317 } 318 } 319 320 func Test_toBomDescriptor(t *testing.T) { 321 type args struct { 322 name string 323 version string 324 srcMetadata source.Description 325 } 326 tests := []struct { 327 name string 328 args args 329 want *cyclonedx.Metadata 330 }{ 331 { 332 name: "with image labels source metadata", 333 args: args{ 334 name: "test-image", 335 version: "1.0.0", 336 srcMetadata: source.Description{ 337 Metadata: source.ImageMetadata{ 338 Labels: map[string]string{ 339 "key1": "value1", 340 }, 341 }, 342 }, 343 }, 344 want: &cyclonedx.Metadata{ 345 Timestamp: "", 346 Lifecycles: nil, 347 Tools: &cyclonedx.ToolsChoice{ 348 Components: &[]cyclonedx.Component{ 349 { 350 Type: cyclonedx.ComponentTypeApplication, 351 Author: "anchore", 352 Name: "test-image", 353 Version: "1.0.0", 354 }, 355 }, 356 }, 357 Authors: nil, 358 Component: &cyclonedx.Component{ 359 BOMRef: "", 360 MIMEType: "", 361 Type: "container", 362 Supplier: nil, 363 Author: "", 364 Publisher: "", 365 Group: "", 366 Name: "", 367 Version: "", 368 Description: "", 369 Scope: "", 370 Hashes: nil, 371 Licenses: nil, 372 Copyright: "", 373 CPE: "", 374 PackageURL: "", 375 SWID: nil, 376 Modified: nil, 377 Pedigree: nil, 378 ExternalReferences: nil, 379 Properties: nil, 380 Components: nil, 381 Evidence: nil, 382 ReleaseNotes: nil, 383 }, 384 Manufacture: nil, 385 Supplier: nil, 386 Licenses: nil, 387 Properties: &[]cyclonedx.Property{ 388 { 389 Name: "syft:image:labels:key1", 390 Value: "value1", 391 }, 392 }, 393 }, 394 }, 395 { 396 name: "with optional supplier is on the root component and bom metadata", 397 args: args{ 398 name: "test-image", 399 version: "1.0.0", 400 srcMetadata: source.Description{ 401 Name: "test-image", 402 Version: "1.0.0", 403 Supplier: "optional-supplier", 404 Metadata: source.ImageMetadata{}, 405 }, 406 }, 407 want: &cyclonedx.Metadata{ 408 Timestamp: "", 409 Lifecycles: nil, 410 Tools: &cyclonedx.ToolsChoice{ 411 Components: &[]cyclonedx.Component{ 412 { 413 Type: cyclonedx.ComponentTypeApplication, 414 Author: "anchore", 415 Name: "test-image", 416 Version: "1.0.0", 417 }, 418 }, 419 }, 420 Authors: nil, 421 Component: &cyclonedx.Component{ 422 BOMRef: "", 423 MIMEType: "", 424 Type: "container", 425 Supplier: &cyclonedx.OrganizationalEntity{ 426 Name: "optional-supplier", 427 }, 428 Author: "", 429 Publisher: "", 430 Group: "", 431 Name: "test-image", 432 Version: "1.0.0", 433 Description: "", 434 Scope: "", 435 Hashes: nil, 436 Licenses: nil, 437 Copyright: "", 438 CPE: "", 439 PackageURL: "", 440 SWID: nil, 441 Modified: nil, 442 Pedigree: nil, 443 ExternalReferences: nil, 444 Properties: nil, 445 Components: nil, 446 Evidence: nil, 447 ReleaseNotes: nil, 448 }, 449 Manufacture: nil, 450 Supplier: &cyclonedx.OrganizationalEntity{ 451 Name: "optional-supplier", 452 }, 453 Licenses: nil, 454 }, 455 }, 456 } 457 for _, tt := range tests { 458 t.Run(tt.name, func(t *testing.T) { 459 subject := toBomDescriptor(tt.args.name, tt.args.version, tt.args.srcMetadata) 460 461 require.NotEmpty(t, subject.Component.BOMRef) 462 subject.Timestamp = "" // not under test 463 464 require.NotNil(t, subject.Component) 465 require.NotEmpty(t, subject.Component.BOMRef) 466 subject.Component.BOMRef = "" // not under test 467 468 if d := cmp.Diff(tt.want, subject); d != "" { 469 t.Errorf("toBomDescriptor() mismatch (-want +got):\n%s", d) 470 } 471 }) 472 } 473 } 474 475 func Test_toBomProperties(t *testing.T) { 476 tests := []struct { 477 name string 478 srcMetadata source.Description 479 props *[]cyclonedx.Property 480 }{ 481 { 482 name: "ImageMetadata without labels", 483 srcMetadata: source.Description{ 484 Metadata: source.ImageMetadata{ 485 Labels: map[string]string{}, 486 }, 487 }, 488 props: nil, 489 }, 490 { 491 name: "ImageMetadata with labels", 492 srcMetadata: source.Description{ 493 Metadata: source.ImageMetadata{ 494 Labels: map[string]string{ 495 "label1": "value1", 496 "label2": "value2", 497 }, 498 }, 499 }, 500 props: &[]cyclonedx.Property{ 501 {Name: "syft:image:labels:label1", Value: "value1"}, 502 {Name: "syft:image:labels:label2", Value: "value2"}, 503 }, 504 }, 505 { 506 name: "not ImageMetadata", 507 srcMetadata: source.Description{ 508 Metadata: source.FileMetadata{}, 509 }, 510 props: nil, 511 }, 512 } 513 for _, test := range tests { 514 t.Run(test.name, func(t *testing.T) { 515 t.Parallel() 516 props := toBomProperties(test.srcMetadata) 517 require.Equal(t, test.props, props) 518 }) 519 } 520 } 521 522 func Test_toOsComponent(t *testing.T) { 523 tests := []struct { 524 name string 525 release linux.Release 526 expected cyclonedx.Component 527 }{ 528 { 529 name: "basic os component", 530 release: linux.Release{ 531 ID: "myLinux", 532 VersionID: "myVersion", 533 }, 534 expected: cyclonedx.Component{ 535 BOMRef: "os:myLinux@myVersion", 536 Type: cyclonedx.ComponentTypeOS, 537 Name: "myLinux", 538 Version: "myVersion", 539 SWID: &cyclonedx.SWID{ 540 TagID: "myLinux", 541 Name: "myLinux", 542 Version: "myVersion", 543 }, 544 Properties: &[]cyclonedx.Property{ 545 { 546 Name: "syft:distro:extendedSupport", 547 Value: "false", 548 }, 549 { 550 Name: "syft:distro:id", 551 Value: "myLinux", 552 }, 553 { 554 Name: "syft:distro:versionID", 555 Value: "myVersion", 556 }, 557 }, 558 }, 559 }, 560 } 561 562 for _, test := range tests { 563 t.Run(test.name, func(t *testing.T) { 564 gotSlice := toOSComponent(&test.release) 565 require.Len(t, gotSlice, 1) 566 got := gotSlice[0] 567 require.Equal(t, test.expected, got) 568 }) 569 } 570 } 571 572 func Test_toOSBomRef(t *testing.T) { 573 tests := []struct { 574 name string 575 osName string 576 osVersion string 577 expected string 578 }{ 579 { 580 name: "no name or version specified", 581 osName: "", 582 osVersion: "", 583 expected: "os:unknown", 584 }, 585 { 586 name: "no version specified", 587 osName: "my-name", 588 osVersion: "", 589 expected: "os:my-name", 590 }, 591 { 592 name: "no name specified", 593 osName: "", 594 osVersion: "my-version", 595 expected: "os:unknown", 596 }, 597 { 598 name: "both name and version specified", 599 osName: "my-name", 600 osVersion: "my-version", 601 expected: "os:my-name@my-version", 602 }, 603 } 604 for _, test := range tests { 605 t.Run(test.name, func(t *testing.T) { 606 got := toOSBomRef(test.osName, test.osVersion) 607 require.Equal(t, test.expected, got) 608 }) 609 } 610 }