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  }