github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/sbom/cyclonedx/unmarshal_test.go (about)

     1  package cyclonedx_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"os"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    12  	"github.com/devseccon/trivy/pkg/sbom/cyclonedx"
    13  	"github.com/devseccon/trivy/pkg/types"
    14  )
    15  
    16  func TestUnmarshaler_Unmarshal(t *testing.T) {
    17  	tests := []struct {
    18  		name      string
    19  		inputFile string
    20  		want      types.SBOM
    21  		wantErr   string
    22  	}{
    23  		{
    24  			name:      "happy path",
    25  			inputFile: "testdata/happy/bom.json",
    26  			want: types.SBOM{
    27  				OS: ftypes.OS{
    28  					Family: "alpine",
    29  					Name:   "3.16.0",
    30  				},
    31  				Packages: []ftypes.PackageInfo{
    32  					{
    33  						Packages: ftypes.Packages{
    34  							{
    35  								Name:       "musl",
    36  								Version:    "1.2.3-r0",
    37  								SrcName:    "musl",
    38  								SrcVersion: "1.2.3-r0",
    39  								Licenses:   []string{"MIT"},
    40  								Ref:        "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
    41  								Layer: ftypes.Layer{
    42  									DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
    43  								},
    44  							},
    45  						},
    46  					},
    47  				},
    48  				Applications: []ftypes.Application{
    49  					{
    50  						Type:     ftypes.Composer,
    51  						FilePath: "app/composer/composer.lock",
    52  						Libraries: ftypes.Packages{
    53  							{
    54  								Name:    "pear/log",
    55  								Version: "1.13.1",
    56  								Ref:     "pkg:composer/pear/log@1.13.1",
    57  								Layer: ftypes.Layer{
    58  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    59  								},
    60  							},
    61  							{
    62  
    63  								Name:    "pear/pear_exception",
    64  								Version: "v1.0.0",
    65  								Ref:     "pkg:composer/pear/pear_exception@v1.0.0",
    66  								Layer: ftypes.Layer{
    67  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    68  								},
    69  							},
    70  						},
    71  					},
    72  					{
    73  						Type:     ftypes.GoBinary,
    74  						FilePath: "app/gobinary/gobinary",
    75  						Libraries: ftypes.Packages{
    76  							{
    77  								Name:    "github.com/package-url/packageurl-go",
    78  								Version: "v0.1.1-0.20220203205134-d70459300c8a",
    79  								Ref:     "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
    80  								Layer: ftypes.Layer{
    81  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    82  								},
    83  							},
    84  						},
    85  					},
    86  					{
    87  						Type:     ftypes.Gradle,
    88  						FilePath: "app/gradle/target/gradle.lockfile",
    89  						Libraries: ftypes.Packages{
    90  							{
    91  								Name:    "com.example:example",
    92  								Ref:     "pkg:maven/com.example/example@0.0.1",
    93  								Version: "0.0.1",
    94  								Layer: ftypes.Layer{
    95  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    96  								},
    97  							},
    98  						},
    99  					},
   100  					{
   101  						Type: ftypes.Jar,
   102  						Libraries: ftypes.Packages{
   103  							{
   104  								Name:    "org.codehaus.mojo:child-project",
   105  								Ref:     "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar",
   106  								Version: "1.0",
   107  								Layer: ftypes.Layer{
   108  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
   109  								},
   110  								FilePath: "app/maven/target/child-project-1.0.jar",
   111  							},
   112  						},
   113  					},
   114  					{
   115  						Type:     ftypes.NodePkg,
   116  						FilePath: "",
   117  						Libraries: ftypes.Packages{
   118  							{
   119  								Name:     "bootstrap",
   120  								Version:  "5.0.2",
   121  								Ref:      "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json",
   122  								Licenses: []string{"MIT"},
   123  								Layer: ftypes.Layer{
   124  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
   125  								},
   126  								FilePath: "app/app/package.json",
   127  							},
   128  						},
   129  					},
   130  				},
   131  			},
   132  		},
   133  		{
   134  			name:      "happy path KBOM",
   135  			inputFile: "testdata/happy/kbom.json",
   136  			want: types.SBOM{
   137  				OS: ftypes.OS{
   138  					Family: "ubuntu",
   139  					Name:   "22.04.2",
   140  				},
   141  				Packages: []ftypes.PackageInfo{
   142  					{
   143  						FilePath: "",
   144  					},
   145  				},
   146  				Applications: []ftypes.Application{
   147  					{
   148  						Type: ftypes.GoBinary,
   149  						Libraries: ftypes.Packages{
   150  							{
   151  								Name:    "docker",
   152  								Version: "24.0.4",
   153  								Ref:     "pkg:golang/docker@24.0.4",
   154  							},
   155  						},
   156  					},
   157  					{
   158  						Type:     "golang",
   159  						FilePath: "node-core-components",
   160  					},
   161  					{
   162  						Type: ftypes.K8sUpstream,
   163  						Libraries: ftypes.Packages{
   164  							{
   165  								Name:    "k8s.io/apiserver",
   166  								Version: "1.27.4",
   167  								Ref:     "pkg:k8s/k8s.io%2Fapiserver@1.27.4",
   168  							},
   169  							{
   170  								Name:    "k8s.io/controller-manager",
   171  								Version: "1.27.4",
   172  								Ref:     "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.4",
   173  							},
   174  							{
   175  								Name:    "k8s.io/kube-proxy",
   176  								Version: "1.27.4",
   177  								Ref:     "pkg:k8s/k8s.io%2Fkube-proxy@1.27.4",
   178  							},
   179  							{
   180  								Name:    "k8s.io/kube-scheduler",
   181  								Version: "1.27.4",
   182  								Ref:     "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.4",
   183  							},
   184  							{
   185  								Name:    "k8s.io/kubelet",
   186  								Version: "1.27.4",
   187  								Ref:     "pkg:k8s/k8s.io%2Fkubelet@1.27.4",
   188  							},
   189  							{
   190  								Name:    "k8s.io/kubernetes",
   191  								Version: "1.27.4",
   192  								Ref:     "pkg:k8s/k8s.io%2Fkubernetes@1.27.4",
   193  							},
   194  						},
   195  					},
   196  				},
   197  			},
   198  		},
   199  		{
   200  			name:      "happy path with infinity loop",
   201  			inputFile: "testdata/happy/infinite-loop-bom.json",
   202  			want: types.SBOM{
   203  				OS: ftypes.OS{
   204  					Family: "ubuntu",
   205  					Name:   "22.04",
   206  				},
   207  				Packages: []ftypes.PackageInfo{
   208  					{
   209  						Packages: ftypes.Packages{
   210  							{
   211  								ID:         "libc6@2.35-0ubuntu3.1",
   212  								Name:       "libc6",
   213  								Version:    "2.35-0ubuntu3.1",
   214  								SrcName:    "glibc",
   215  								SrcVersion: "2.35",
   216  								SrcRelease: "0ubuntu3.1",
   217  								Licenses: []string{
   218  									"LGPL-2.1",
   219  									"GPL-2.0",
   220  									"GFDL-1.3",
   221  								},
   222  								Ref: "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04",
   223  								Layer: ftypes.Layer{
   224  									Digest: "sha256:74ac377868f863e123f24c409f79709f7563fa464557c36a09cf6f85c8b92b7f",
   225  									DiffID: "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466",
   226  								},
   227  							},
   228  							{
   229  								ID:         "libcrypt1@1:4.4.27-1",
   230  								Name:       "libcrypt1",
   231  								Version:    "4.4.27-1",
   232  								Epoch:      1,
   233  								SrcName:    "libxcrypt",
   234  								SrcVersion: "4.4.27",
   235  								SrcRelease: "1",
   236  								SrcEpoch:   1,
   237  								Ref:        "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1&distro=ubuntu-22.04",
   238  								Layer: ftypes.Layer{
   239  									Digest: "sha256:74ac377868f863e123f24c409f79709f7563fa464557c36a09cf6f85c8b92b7f",
   240  									DiffID: "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466",
   241  								},
   242  							},
   243  						},
   244  					},
   245  				},
   246  			},
   247  		},
   248  		{
   249  			name:      "happy path for third party sbom",
   250  			inputFile: "testdata/happy/third-party-bom.json",
   251  			want: types.SBOM{
   252  				OS: ftypes.OS{
   253  					Family: "alpine",
   254  					Name:   "3.16.0",
   255  				},
   256  				Packages: []ftypes.PackageInfo{
   257  					{
   258  						Packages: ftypes.Packages{
   259  							{
   260  								Name:       "musl",
   261  								Version:    "1.2.3-r0",
   262  								SrcName:    "musl",
   263  								SrcVersion: "1.2.3-r0",
   264  								Licenses:   []string{"MIT"},
   265  								Ref:        "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
   266  							},
   267  						},
   268  					},
   269  				},
   270  				Applications: []ftypes.Application{
   271  					{
   272  						Type:     "composer",
   273  						FilePath: "",
   274  						Libraries: ftypes.Packages{
   275  							{
   276  								Name:    "pear/log",
   277  								Version: "1.13.1",
   278  								Ref:     "pkg:composer/pear/log@1.13.1",
   279  							},
   280  							{
   281  
   282  								Name:    "pear/pear_exception",
   283  								Version: "v1.0.0",
   284  								Ref:     "pkg:composer/pear/pear_exception@v1.0.0",
   285  							},
   286  						},
   287  					},
   288  				},
   289  			},
   290  		},
   291  		{
   292  			name:      "happy path for third party sbom, no operation-system component",
   293  			inputFile: "testdata/happy/third-party-bom-no-os.json",
   294  			want: types.SBOM{
   295  				Applications: []ftypes.Application{
   296  					{
   297  						Type:     "composer",
   298  						FilePath: "",
   299  						Libraries: ftypes.Packages{
   300  							{
   301  								Name:    "pear/log",
   302  								Version: "1.13.1",
   303  								Ref:     "pkg:composer/pear/log@1.13.1",
   304  							},
   305  						},
   306  					},
   307  				},
   308  			},
   309  		},
   310  		{
   311  			name:      "happy path for unrelated bom",
   312  			inputFile: "testdata/happy/unrelated-bom.json",
   313  			want: types.SBOM{
   314  				Applications: []ftypes.Application{
   315  					{
   316  						Type:     "composer",
   317  						FilePath: "",
   318  						Libraries: ftypes.Packages{
   319  							{
   320  								Name:    "pear/log",
   321  								Version: "1.13.1",
   322  								Ref:     "pkg:composer/pear/log@1.13.1",
   323  							},
   324  							{
   325  
   326  								Name:    "pear/pear_exception",
   327  								Version: "v1.0.0",
   328  								Ref:     "pkg:composer/pear/pear_exception@v1.0.0",
   329  							},
   330  						},
   331  					},
   332  				},
   333  			},
   334  		},
   335  		{
   336  			name:      "happy path for independent library bom",
   337  			inputFile: "testdata/happy/independent-library-bom.json",
   338  			want: types.SBOM{
   339  				Applications: []ftypes.Application{
   340  					{
   341  						Type:     "composer",
   342  						FilePath: "",
   343  						Libraries: ftypes.Packages{
   344  							{
   345  								Name:    "pear/core",
   346  								Version: "1.13.1",
   347  								Ref:     "pkg:composer/pear/core@1.13.1",
   348  							},
   349  							{
   350  								Name:    "pear/log",
   351  								Version: "1.13.1",
   352  								Ref:     "pkg:composer/pear/log@1.13.1",
   353  							},
   354  							{
   355  
   356  								Name:    "pear/pear_exception",
   357  								Version: "v1.0.0",
   358  								Ref:     "pkg:composer/pear/pear_exception@v1.0.0",
   359  							},
   360  						},
   361  					},
   362  				},
   363  			},
   364  		},
   365  		{
   366  			name:      "happy path for jar where name is GroupID and ArtifactID",
   367  			inputFile: "testdata/happy/group-in-name.json",
   368  			want: types.SBOM{
   369  				Applications: []ftypes.Application{
   370  					{
   371  						Type: "jar",
   372  						Libraries: ftypes.Packages{
   373  							{
   374  								Name:     "org.springframework:spring-web",
   375  								Version:  "5.3.22",
   376  								Ref:      "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar",
   377  								FilePath: "spring-web-5.3.22.jar",
   378  							},
   379  						},
   380  					},
   381  				},
   382  			},
   383  		},
   384  		{
   385  			name:      "happy path only os component",
   386  			inputFile: "testdata/happy/os-only-bom.json",
   387  			want: types.SBOM{
   388  				OS: ftypes.OS{
   389  					Family: "alpine",
   390  					Name:   "3.16.0",
   391  				},
   392  				Packages: []ftypes.PackageInfo{
   393  					{},
   394  				},
   395  			},
   396  		},
   397  		{
   398  			name:      "happy path empty component",
   399  			inputFile: "testdata/happy/empty-bom.json",
   400  			want:      types.SBOM{},
   401  		},
   402  		{
   403  			name:      "happy path empty metadata component",
   404  			inputFile: "testdata/happy/empty-metadata-component-bom.json",
   405  			want:      types.SBOM{},
   406  		},
   407  		{
   408  			name:      "sad path invalid purl",
   409  			inputFile: "testdata/sad/invalid-purl.json",
   410  			wantErr:   "failed to parse purl",
   411  		},
   412  	}
   413  
   414  	for _, tt := range tests {
   415  		t.Run(tt.name, func(t *testing.T) {
   416  			f, err := os.Open(tt.inputFile)
   417  			require.NoError(t, err)
   418  			defer f.Close()
   419  
   420  			var cdx cyclonedx.BOM
   421  			err = json.NewDecoder(f).Decode(&cdx)
   422  			if tt.wantErr != "" {
   423  				require.Error(t, err)
   424  				assert.Contains(t, err.Error(), tt.wantErr)
   425  				return
   426  			}
   427  			require.NoError(t, err)
   428  
   429  			// Not compare the CycloneDX field
   430  			got := *cdx.SBOM
   431  			got.CycloneDX = nil
   432  
   433  			assert.Equal(t, tt.want, got)
   434  		})
   435  	}
   436  }