github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/common/cyclonedxhelpers/decoder_test.go (about)

     1  package cyclonedxhelpers
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"testing"
     8  
     9  	"github.com/CycloneDX/cyclonedx-go"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/anchore/syft/syft/artifact"
    14  	"github.com/anchore/syft/syft/pkg"
    15  	"github.com/anchore/syft/syft/sbom"
    16  )
    17  
    18  func Test_decode(t *testing.T) {
    19  	type expected struct {
    20  		os       string
    21  		pkg      string
    22  		ver      string
    23  		relation string
    24  		purl     string
    25  		cpe      string
    26  	}
    27  	tests := []struct {
    28  		name     string
    29  		input    cyclonedx.BOM
    30  		expected []expected
    31  	}{
    32  		{
    33  			name: "basic mapping from cyclonedx",
    34  			input: cyclonedx.BOM{
    35  				Metadata: nil,
    36  				Components: &[]cyclonedx.Component{
    37  					{
    38  						BOMRef:      "p1",
    39  						Type:        cyclonedx.ComponentTypeLibrary,
    40  						Name:        "package-1",
    41  						Version:     "1.0.1",
    42  						Description: "",
    43  						Hashes:      nil,
    44  						Licenses: &cyclonedx.Licenses{
    45  							{
    46  								License: &cyclonedx.License{
    47  									ID: "MIT",
    48  								},
    49  							},
    50  						},
    51  						CPE:        "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
    52  						PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
    53  						ExternalReferences: &[]cyclonedx.ExternalReference{
    54  							{
    55  								URL:     "",
    56  								Comment: "",
    57  								Hashes:  nil,
    58  								Type:    "",
    59  							},
    60  						},
    61  						Properties: &[]cyclonedx.Property{
    62  							{
    63  								Name:  "foundBy",
    64  								Value: "the-cataloger-1",
    65  							},
    66  							{
    67  								Name:  "language",
    68  								Value: "python",
    69  							},
    70  							{
    71  								Name:  "type",
    72  								Value: "python",
    73  							},
    74  							{
    75  								Name:  "metadataType",
    76  								Value: "PythonPackageMetadata",
    77  							},
    78  							{
    79  								Name:  "path",
    80  								Value: "/some/path/pkg1",
    81  							},
    82  						},
    83  						Components: nil,
    84  						Evidence:   nil,
    85  					},
    86  					{
    87  						BOMRef:  "p2",
    88  						Type:    cyclonedx.ComponentTypeLibrary,
    89  						Name:    "package-2",
    90  						Version: "2.0.2",
    91  						Hashes:  nil,
    92  						Licenses: &cyclonedx.Licenses{
    93  							{
    94  								License: &cyclonedx.License{
    95  									ID: "MIT",
    96  								},
    97  							},
    98  						},
    99  						CPE:        "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*",
   100  						PackageURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
   101  						Properties: &[]cyclonedx.Property{
   102  
   103  							{
   104  								Name:  "foundBy",
   105  								Value: "apkdb-cataloger",
   106  							},
   107  							{
   108  								Name:  "type",
   109  								Value: "apk",
   110  							},
   111  							{
   112  								Name:  "metadataType",
   113  								Value: "ApkMetadata",
   114  							},
   115  							{
   116  								Name:  "path",
   117  								Value: "/lib/apk/db/installed",
   118  							},
   119  							{
   120  								Name:  "layerID",
   121  								Value: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
   122  							},
   123  							{
   124  								Name:  "originPackage",
   125  								Value: "zlib",
   126  							},
   127  							{
   128  								Name:  "size",
   129  								Value: "51213",
   130  							},
   131  							{
   132  								Name:  "installedSize",
   133  								Value: "110592",
   134  							},
   135  							{
   136  								Name:  "pullDependencies",
   137  								Value: "so:libc.musl-x86_64.so.1",
   138  							},
   139  							{
   140  								Name:  "pullChecksum",
   141  								Value: "Q1uss4DfpvL16Nw2YUTwmzGBABz3Y=",
   142  							},
   143  							{
   144  								Name:  "gitCommitOfApkPort",
   145  								Value: "d2bfb22c8e8f67ad7d8d02704f35ec4d2a19f9b9",
   146  							},
   147  						},
   148  					},
   149  					{
   150  						Type:    cyclonedx.ComponentTypeOS,
   151  						Name:    "debian",
   152  						Version: "1.2.3",
   153  						Hashes:  nil,
   154  						Licenses: &cyclonedx.Licenses{
   155  							{
   156  								License: &cyclonedx.License{
   157  									ID: "MIT",
   158  								},
   159  							},
   160  						},
   161  						Properties: &[]cyclonedx.Property{
   162  							{
   163  								Name:  "prettyName",
   164  								Value: "debian",
   165  							},
   166  							{
   167  								Name:  "id",
   168  								Value: "debian",
   169  							},
   170  							{
   171  								Name:  "versionID",
   172  								Value: "1.2.3",
   173  							},
   174  						},
   175  						Components: nil,
   176  						Evidence:   nil,
   177  					},
   178  				},
   179  				Dependencies: &[]cyclonedx.Dependency{
   180  					{
   181  						Ref:          "p1",
   182  						Dependencies: &[]string{"p2"},
   183  					},
   184  				},
   185  			},
   186  			expected: []expected{
   187  				{
   188  					os:  "debian",
   189  					ver: "1.2.3",
   190  				},
   191  				{
   192  					pkg:  "package-1",
   193  					ver:  "1.0.1",
   194  					cpe:  "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
   195  					purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
   196  				},
   197  				{
   198  					pkg:      "package-2",
   199  					ver:      "2.0.2",
   200  					purl:     "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
   201  					relation: "package-1",
   202  				},
   203  			},
   204  		},
   205  	}
   206  	for _, test := range tests {
   207  		t.Run(test.name, func(t *testing.T) {
   208  			sbom, err := ToSyftModel(&test.input)
   209  			assert.NoError(t, err)
   210  
   211  		test:
   212  			for _, e := range test.expected {
   213  				if e.os != "" {
   214  					assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID)
   215  					assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID)
   216  				}
   217  				if e.pkg != "" {
   218  					for p := range sbom.Artifacts.Packages.Enumerate() {
   219  						if e.pkg != p.Name {
   220  							continue
   221  						}
   222  
   223  						assert.Equal(t, e.ver, p.Version)
   224  
   225  						if e.cpe != "" {
   226  							foundCPE := false
   227  							for _, c := range p.CPEs {
   228  								cstr := c.BindToFmtString()
   229  								if e.cpe == cstr {
   230  									foundCPE = true
   231  									break
   232  								}
   233  							}
   234  							if !foundCPE {
   235  								assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe))
   236  							}
   237  						}
   238  
   239  						if e.purl != "" {
   240  							assert.Equal(t, e.purl, p.PURL)
   241  						}
   242  
   243  						if e.relation != "" {
   244  							foundRelation := false
   245  							for _, r := range sbom.Relationships {
   246  								p := sbom.Artifacts.Packages.Package(r.To.ID())
   247  								if e.relation == p.Name {
   248  									foundRelation = true
   249  									break
   250  								}
   251  							}
   252  							if !foundRelation {
   253  								assert.Fail(t, fmt.Sprintf("relation not found: %s", e.relation))
   254  							}
   255  						}
   256  						continue test
   257  					}
   258  					assert.Fail(t, fmt.Sprintf("package should be present: %s", e.pkg))
   259  				}
   260  			}
   261  		})
   262  	}
   263  }
   264  
   265  func Test_relationshipDirection(t *testing.T) {
   266  	cyclonedx_bom := cyclonedx.BOM{Metadata: nil,
   267  		Components: &[]cyclonedx.Component{
   268  			{
   269  				BOMRef:     "p1",
   270  				Type:       cyclonedx.ComponentTypeLibrary,
   271  				Name:       "package-1",
   272  				Version:    "1.0.1",
   273  				PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1",
   274  			},
   275  			{
   276  				BOMRef:     "p2",
   277  				Type:       cyclonedx.ComponentTypeLibrary,
   278  				Name:       "package-2",
   279  				Version:    "2.0.2",
   280  				PackageURL: "pkg:some/package-2@2.0.2?arch=arm64&upstream=upstream1&distro=alpine-1",
   281  			},
   282  		},
   283  		Dependencies: &[]cyclonedx.Dependency{
   284  			{
   285  				Ref:          "p1",
   286  				Dependencies: &[]string{"p2"},
   287  			},
   288  		}}
   289  	sbom, err := ToSyftModel(&cyclonedx_bom)
   290  	assert.Nil(t, err)
   291  	assert.Len(t, sbom.Relationships, 1)
   292  	relationship := sbom.Relationships[0]
   293  
   294  	// check that p2 -- dependency of --> p1
   295  	// same as p1 -- depends on --> p2
   296  	assert.Equal(t, artifact.DependencyOfRelationship, relationship.Type)
   297  	assert.Equal(t, "package-2", packageNameFromIdentifier(sbom, relationship.From))
   298  	assert.Equal(t, "package-1", packageNameFromIdentifier(sbom, relationship.To))
   299  }
   300  
   301  func packageNameFromIdentifier(model *sbom.SBOM, identifier artifact.Identifiable) string {
   302  	return model.Artifacts.Packages.Package(identifier.ID()).Name
   303  }
   304  
   305  func Test_missingDataDecode(t *testing.T) {
   306  	bom := &cyclonedx.BOM{
   307  		Metadata:    nil,
   308  		Components:  &[]cyclonedx.Component{},
   309  		SpecVersion: cyclonedx.SpecVersion1_4,
   310  	}
   311  
   312  	_, err := ToSyftModel(bom)
   313  	assert.NoError(t, err)
   314  
   315  	bom.Metadata = &cyclonedx.Metadata{}
   316  
   317  	_, err = ToSyftModel(bom)
   318  	assert.NoError(t, err)
   319  
   320  	pkg := decodeComponent(&cyclonedx.Component{
   321  		Licenses: &cyclonedx.Licenses{
   322  			{
   323  				License: nil,
   324  			},
   325  		},
   326  	})
   327  	assert.Equal(t, pkg.Licenses.Empty(), true)
   328  }
   329  
   330  func Test_missingComponentsDecode(t *testing.T) {
   331  	bom := &cyclonedx.BOM{
   332  		SpecVersion: cyclonedx.SpecVersion1_4,
   333  	}
   334  	bomBytes, _ := json.Marshal(&bom)
   335  	decode := GetDecoder(cyclonedx.BOMFileFormatJSON)
   336  
   337  	_, err := decode(bytes.NewReader(bomBytes))
   338  
   339  	assert.NoError(t, err)
   340  }
   341  
   342  func Test_decodeDependencies(t *testing.T) {
   343  	c1 := cyclonedx.Component{
   344  		Name: "c1",
   345  	}
   346  
   347  	c2 := cyclonedx.Component{
   348  		Name: "c2",
   349  	}
   350  
   351  	c3 := cyclonedx.Component{
   352  		Name: "c3",
   353  	}
   354  
   355  	for _, c := range []*cyclonedx.Component{&c1, &c2, &c3} {
   356  		c.BOMRef = c.Name
   357  	}
   358  
   359  	setTypes := func(typ cyclonedx.ComponentType, components ...cyclonedx.Component) *[]cyclonedx.Component {
   360  		var out []cyclonedx.Component
   361  		for _, c := range components {
   362  			c.Type = typ
   363  			out = append(out, c)
   364  		}
   365  		return &out
   366  	}
   367  
   368  	tests := []struct {
   369  		name     string
   370  		sbom     cyclonedx.BOM
   371  		expected []string
   372  	}{
   373  		{
   374  			name: "dependencies decoded as dependencyOf relationships",
   375  			sbom: cyclonedx.BOM{
   376  				Components: setTypes(cyclonedx.ComponentTypeLibrary,
   377  					c1,
   378  					c2,
   379  					c3,
   380  				),
   381  				Dependencies: &[]cyclonedx.Dependency{
   382  					{
   383  						Ref: c1.BOMRef,
   384  						Dependencies: &[]string{
   385  							c2.BOMRef,
   386  							c3.BOMRef,
   387  						},
   388  					},
   389  				},
   390  			},
   391  			expected: []string{c2.Name, c3.Name},
   392  		},
   393  		{
   394  			name: "dependencies skipped with unhandled components",
   395  			sbom: cyclonedx.BOM{
   396  				Components: setTypes("",
   397  					c1,
   398  					c2,
   399  					c3,
   400  				),
   401  				Dependencies: &[]cyclonedx.Dependency{
   402  					{
   403  						Ref: c1.BOMRef,
   404  						Dependencies: &[]string{
   405  							c2.BOMRef,
   406  							c3.BOMRef,
   407  						},
   408  					},
   409  				},
   410  			},
   411  			expected: nil,
   412  		},
   413  	}
   414  
   415  	for _, test := range tests {
   416  		t.Run(test.name, func(t *testing.T) {
   417  			s, err := ToSyftModel(&test.sbom)
   418  			require.NoError(t, err)
   419  			require.NotNil(t, s)
   420  
   421  			var deps []string
   422  			if s != nil {
   423  				for _, r := range s.Relationships {
   424  					if r.Type != artifact.DependencyOfRelationship {
   425  						continue
   426  					}
   427  					if p, ok := r.To.(pkg.Package); !ok || p.Name != c1.Name {
   428  						continue
   429  					}
   430  					if p, ok := r.From.(pkg.Package); ok {
   431  						deps = append(deps, p.Name)
   432  					}
   433  				}
   434  			}
   435  			require.Equal(t, test.expected, deps)
   436  		})
   437  	}
   438  }