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

     1  package spdx_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"os"
     6  	"sort"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    13  	"github.com/devseccon/trivy/pkg/sbom/spdx"
    14  	"github.com/devseccon/trivy/pkg/types"
    15  )
    16  
    17  func TestUnmarshaler_Unmarshal(t *testing.T) {
    18  	tests := []struct {
    19  		name      string
    20  		inputFile string
    21  		want      types.SBOM
    22  		wantErr   string
    23  	}{
    24  		{
    25  			name:      "happy path",
    26  			inputFile: "testdata/happy/bom.json",
    27  			want: types.SBOM{
    28  				OS: ftypes.OS{
    29  					Family: "alpine",
    30  					Name:   "3.16.0",
    31  				},
    32  				Packages: []ftypes.PackageInfo{
    33  					{
    34  						Packages: ftypes.Packages{
    35  							{
    36  								Name:       "musl",
    37  								Version:    "1.2.3-r0",
    38  								SrcName:    "musl",
    39  								SrcVersion: "1.2.3-r0",
    40  								Licenses:   []string{"MIT"},
    41  								Ref:        "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
    42  								Layer: ftypes.Layer{
    43  									DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
    44  								},
    45  							},
    46  						},
    47  					},
    48  				},
    49  				Applications: []ftypes.Application{
    50  					{
    51  						Type:     "composer",
    52  						FilePath: "app/composer/composer.lock",
    53  						Libraries: ftypes.Packages{
    54  							{
    55  								Name:    "pear/log",
    56  								Version: "1.13.1",
    57  								Ref:     "pkg:composer/pear/log@1.13.1",
    58  								Layer: ftypes.Layer{
    59  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    60  								},
    61  							},
    62  							{
    63  
    64  								Name:    "pear/pear_exception",
    65  								Version: "v1.0.0",
    66  								Ref:     "pkg:composer/pear/pear_exception@v1.0.0",
    67  								Layer: ftypes.Layer{
    68  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    69  								},
    70  							},
    71  						},
    72  					},
    73  					{
    74  						Type:     "gobinary",
    75  						FilePath: "app/gobinary/gobinary",
    76  						Libraries: ftypes.Packages{
    77  							{
    78  								Name:    "github.com/package-url/packageurl-go",
    79  								Version: "v0.1.1-0.20220203205134-d70459300c8a",
    80  								Ref:     "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
    81  								Layer: ftypes.Layer{
    82  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    83  								},
    84  							},
    85  						},
    86  					},
    87  					{
    88  						Type: "jar",
    89  						Libraries: ftypes.Packages{
    90  							{
    91  								Name:    "org.codehaus.mojo:child-project",
    92  								Ref:     "pkg:maven/org.codehaus.mojo/child-project@1.0",
    93  								Version: "1.0",
    94  								Layer: ftypes.Layer{
    95  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
    96  								},
    97  							},
    98  						},
    99  					},
   100  					{
   101  						Type: "node-pkg",
   102  						Libraries: ftypes.Packages{
   103  							{
   104  								Name:     "bootstrap",
   105  								Version:  "5.0.2",
   106  								Ref:      "pkg:npm/bootstrap@5.0.2",
   107  								Licenses: []string{"MIT"},
   108  								Layer: ftypes.Layer{
   109  									DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
   110  								},
   111  							},
   112  						},
   113  					},
   114  				},
   115  			},
   116  		},
   117  		{
   118  			name:      "happy path for bom with hasFiles field",
   119  			inputFile: "testdata/happy/with-hasfiles-bom.json",
   120  			want: types.SBOM{
   121  				Applications: []ftypes.Application{
   122  					{
   123  						Type: "node-pkg",
   124  						Libraries: ftypes.Packages{
   125  							{
   126  								ID:       "yargs-parser@21.1.1",
   127  								Name:     "yargs-parser",
   128  								Version:  "21.1.1",
   129  								Licenses: []string{"ISC"},
   130  								Ref:      "pkg:npm/yargs-parser@21.1.1",
   131  								FilePath: "node_modules/yargs-parser/package.json",
   132  							},
   133  						},
   134  					},
   135  				},
   136  			},
   137  		},
   138  		{
   139  			name:      "happy path for bom with hasFiles field",
   140  			inputFile: "testdata/happy/with-hasfiles-bom.json",
   141  			want: types.SBOM{
   142  				Applications: []ftypes.Application{
   143  					{
   144  						Type: "node-pkg",
   145  						Libraries: ftypes.Packages{
   146  							{
   147  								ID:       "yargs-parser@21.1.1",
   148  								Name:     "yargs-parser",
   149  								Version:  "21.1.1",
   150  								Licenses: []string{"ISC"},
   151  								Ref:      "pkg:npm/yargs-parser@21.1.1",
   152  								FilePath: "node_modules/yargs-parser/package.json",
   153  							},
   154  						},
   155  					},
   156  				},
   157  			},
   158  		},
   159  		{
   160  			name:      "happy path for bom files in relationships",
   161  			inputFile: "testdata/happy/with-files-in-relationships-bom.json",
   162  			want: types.SBOM{
   163  				Applications: []ftypes.Application{
   164  					{
   165  						Type: "node-pkg",
   166  						Libraries: ftypes.Packages{
   167  							{
   168  								ID:       "yargs-parser@21.1.1",
   169  								Name:     "yargs-parser",
   170  								Version:  "21.1.1",
   171  								Licenses: []string{"ISC"},
   172  								Ref:      "pkg:npm/yargs-parser@21.1.1",
   173  								FilePath: "node_modules/yargs-parser/package.json",
   174  							},
   175  						},
   176  					},
   177  				},
   178  			},
   179  		},
   180  		{
   181  			name:      "happy path for unrelated bom",
   182  			inputFile: "testdata/happy/unrelated-bom.json",
   183  			want: types.SBOM{
   184  				Applications: []ftypes.Application{
   185  					{
   186  						Type:     "composer",
   187  						FilePath: "app/composer/composer.lock",
   188  						Libraries: ftypes.Packages{
   189  							{
   190  								Name:    "pear/log",
   191  								Version: "1.13.1",
   192  								Ref:     "pkg:composer/pear/log@1.13.1",
   193  							},
   194  							{
   195  
   196  								Name:    "pear/pear_exception",
   197  								Version: "v1.0.0",
   198  								Ref:     "pkg:composer/pear/pear_exception@v1.0.0",
   199  							},
   200  						},
   201  					},
   202  				},
   203  			},
   204  		},
   205  		{
   206  			name:      "happy path with no relationship",
   207  			inputFile: "testdata/happy/no-relationship.json",
   208  			want: types.SBOM{
   209  				Applications: []ftypes.Application{
   210  					{
   211  						Type: ftypes.Jar,
   212  						Libraries: ftypes.Packages{
   213  							{
   214  								FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar",
   215  								Name:     "co.elastic.apm:apm-agent",
   216  								Version:  "1.36.0",
   217  								Ref:      "pkg:maven/co.elastic.apm/apm-agent@1.36.0",
   218  							},
   219  							{
   220  								FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar",
   221  								Name:     "co.elastic.apm:apm-agent-cached-lookup-key",
   222  								Version:  "1.36.0",
   223  								Ref:      "pkg:maven/co.elastic.apm/apm-agent-cached-lookup-key@1.36.0",
   224  							},
   225  						},
   226  					},
   227  				},
   228  			},
   229  		},
   230  		{
   231  			name:      "happy path only os component",
   232  			inputFile: "testdata/happy/os-only-bom.json",
   233  			want: types.SBOM{
   234  				OS: ftypes.OS{
   235  					Family: "alpine",
   236  					Name:   "3.16.0",
   237  				},
   238  			},
   239  		},
   240  		{
   241  			name:      "happy path empty component",
   242  			inputFile: "testdata/happy/empty-bom.json",
   243  			want:      types.SBOM{},
   244  		},
   245  		{
   246  			name:      "sad path invalid purl",
   247  			inputFile: "testdata/sad/invalid-source-info.json",
   248  			wantErr:   "failed to parse source info:",
   249  		},
   250  	}
   251  
   252  	for _, tt := range tests {
   253  		t.Run(tt.name, func(t *testing.T) {
   254  			f, err := os.Open(tt.inputFile)
   255  			require.NoError(t, err)
   256  			defer f.Close()
   257  
   258  			v := &spdx.SPDX{SBOM: &types.SBOM{}}
   259  			err = json.NewDecoder(f).Decode(v)
   260  			if tt.wantErr != "" {
   261  				require.Error(t, err)
   262  				assert.Contains(t, err.Error(), tt.wantErr)
   263  				return
   264  			}
   265  
   266  			// Not compare the SPDX field
   267  			v.SPDX = nil
   268  
   269  			sort.Slice(v.Applications, func(i, j int) bool {
   270  				return v.Applications[i].Type < v.Applications[j].Type
   271  			})
   272  			require.NoError(t, err)
   273  			assert.Equal(t, tt.want, *v.SBOM)
   274  		})
   275  	}
   276  }