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  }