github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/artifact/image/remote_sbom_test.go (about)

     1  package image_test
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"net/url"
     8  	"os"
     9  	"testing"
    10  
    11  	v1 "github.com/google/go-containerregistry/pkg/v1"
    12  	fakei "github.com/google/go-containerregistry/pkg/v1/fake"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/devseccon/trivy/pkg/fanal/artifact"
    17  	image2 "github.com/devseccon/trivy/pkg/fanal/artifact/image"
    18  	"github.com/devseccon/trivy/pkg/fanal/cache"
    19  	"github.com/devseccon/trivy/pkg/fanal/types"
    20  	"github.com/devseccon/trivy/pkg/log"
    21  	"github.com/devseccon/trivy/pkg/rekortest"
    22  )
    23  
    24  func TestMain(m *testing.M) {
    25  	log.InitLogger(false, true)
    26  	os.Exit(m.Run())
    27  }
    28  
    29  type fakeImage struct {
    30  	name        string
    31  	repoDigests []string
    32  	*fakei.FakeImage
    33  	types.ImageExtension
    34  }
    35  
    36  func (f fakeImage) ID() (string, error) {
    37  	return "", nil
    38  }
    39  
    40  func (f fakeImage) Name() string {
    41  	return f.name
    42  }
    43  
    44  func (f fakeImage) RepoDigests() []string {
    45  	return f.repoDigests
    46  }
    47  
    48  func TestArtifact_InspectRekorAttestation(t *testing.T) {
    49  	type fields struct {
    50  		imageName   string
    51  		repoDigests []string
    52  	}
    53  	tests := []struct {
    54  		name                string
    55  		fields              fields
    56  		artifactOpt         artifact.Option
    57  		putBlobExpectations []cache.ArtifactCachePutBlobExpectation
    58  		want                types.ArtifactReference
    59  		wantErr             string
    60  	}{
    61  		{
    62  			name: "happy path",
    63  			fields: fields{
    64  				imageName: "test/image:10",
    65  				repoDigests: []string{
    66  					"test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
    67  				},
    68  			},
    69  			putBlobExpectations: []cache.ArtifactCachePutBlobExpectation{
    70  				{
    71  					Args: cache.ArtifactCachePutBlobArgs{
    72  						BlobID: "sha256:9c23872047046e145f49fb5533b63ace0cbf819f5b68e33f69f4e9bbab4c517e",
    73  						BlobInfo: types.BlobInfo{
    74  							SchemaVersion: types.BlobJSONSchemaVersion,
    75  							OS: types.OS{
    76  								Family: "alpine",
    77  								Name:   "3.16.2",
    78  							},
    79  							PackageInfos: []types.PackageInfo{
    80  								{
    81  									Packages: types.Packages{
    82  										{
    83  											Name:       "musl",
    84  											Version:    "1.2.3-r0",
    85  											SrcName:    "musl",
    86  											SrcVersion: "1.2.3-r0",
    87  											Licenses:   []string{"MIT"},
    88  											Ref:        "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2",
    89  											Layer: types.Layer{
    90  												DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7",
    91  											},
    92  										},
    93  									},
    94  								},
    95  							},
    96  						},
    97  					},
    98  					Returns: cache.ArtifactCachePutBlobReturns{},
    99  				},
   100  			},
   101  			artifactOpt: artifact.Option{
   102  				SBOMSources: []string{"rekor"},
   103  			},
   104  			want: types.ArtifactReference{
   105  				Name: "test/image:10",
   106  				Type: types.ArtifactCycloneDX,
   107  				ID:   "sha256:9c23872047046e145f49fb5533b63ace0cbf819f5b68e33f69f4e9bbab4c517e",
   108  				BlobIDs: []string{
   109  					"sha256:9c23872047046e145f49fb5533b63ace0cbf819f5b68e33f69f4e9bbab4c517e",
   110  				},
   111  			},
   112  		},
   113  		{
   114  			name: "error",
   115  			fields: fields{
   116  				imageName: "test/image:10",
   117  				repoDigests: []string{
   118  					"test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
   119  				},
   120  			},
   121  			artifactOpt: artifact.Option{
   122  				SBOMSources: []string{"rekor"},
   123  			},
   124  			wantErr: "remote SBOM fetching error",
   125  		},
   126  	}
   127  
   128  	require.NoError(t, log.InitLogger(false, true))
   129  	for _, tt := range tests {
   130  		t.Run(tt.name, func(t *testing.T) {
   131  			ts := rekortest.NewServer(t)
   132  			defer ts.Close()
   133  
   134  			// Set the testing URL
   135  			tt.artifactOpt.RekorURL = ts.URL()
   136  
   137  			mockCache := new(cache.MockArtifactCache)
   138  			mockCache.ApplyPutBlobExpectations(tt.putBlobExpectations)
   139  
   140  			fi := &fakei.FakeImage{}
   141  			fi.ConfigFileReturns(&v1.ConfigFile{}, nil)
   142  
   143  			img := &fakeImage{
   144  				name:        tt.fields.imageName,
   145  				repoDigests: tt.fields.repoDigests,
   146  				FakeImage:   fi,
   147  			}
   148  			a, err := image2.NewArtifact(img, mockCache, tt.artifactOpt)
   149  			require.NoError(t, err)
   150  
   151  			got, err := a.Inspect(context.Background())
   152  			if tt.wantErr != "" {
   153  				assert.ErrorContains(t, err, tt.wantErr)
   154  				return
   155  			}
   156  			require.NoError(t, err, tt.name)
   157  			got.CycloneDX = nil
   158  			assert.Equal(t, tt.want, got)
   159  		})
   160  	}
   161  }
   162  
   163  func TestArtifact_inspectOCIReferrerSBOM(t *testing.T) {
   164  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   165  		switch r.URL.Path {
   166  		case "/v2":
   167  			_, err := w.Write([]byte("ok"))
   168  			require.NoError(t, err)
   169  		case "/v2/test/image/referrers/sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02":
   170  			http.ServeFile(w, r, "testdata/index.json")
   171  		case "/v2/test/image/manifests/sha256:37c89af4907fa0af078aeba12d6f18dc0c63937c010030baaaa88e958f0719a5":
   172  			http.ServeFile(w, r, "testdata/manifest.json")
   173  		case "/v2/test/image/blobs/sha256:9e05dda2a2dcdd526c9204be8645ae48742861c27f093bf496a6397834acecf2":
   174  			http.ServeFile(w, r, "testdata/cyclonedx.json")
   175  		}
   176  		return
   177  	}))
   178  	defer ts.Close()
   179  
   180  	u, err := url.Parse(ts.URL)
   181  	require.NoError(t, err)
   182  	registry := u.Host
   183  
   184  	type fields struct {
   185  		imageName   string
   186  		repoDigests []string
   187  	}
   188  
   189  	tests := []struct {
   190  		name                string
   191  		fields              fields
   192  		artifactOpt         artifact.Option
   193  		putBlobExpectations []cache.ArtifactCachePutBlobExpectation
   194  		want                types.ArtifactReference
   195  		wantErr             string
   196  	}{
   197  		{
   198  			name: "happy path",
   199  			fields: fields{
   200  				imageName: registry + "/test/image:10",
   201  				repoDigests: []string{
   202  					registry + "/test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
   203  				},
   204  			},
   205  			artifactOpt: artifact.Option{
   206  				SBOMSources: []string{"oci"},
   207  			},
   208  			putBlobExpectations: []cache.ArtifactCachePutBlobExpectation{
   209  				{
   210  					Args: cache.ArtifactCachePutBlobArgs{
   211  						BlobID: "sha256:d07a1894bfd283b4ac26682ab48f12ad22cdc4fef9cf8b4c09056f631d3667a5",
   212  						BlobInfo: types.BlobInfo{
   213  							SchemaVersion: types.BlobJSONSchemaVersion,
   214  							Applications: []types.Application{
   215  								{
   216  									Type: types.GoBinary,
   217  									Libraries: types.Packages{
   218  										{
   219  											Name:    "github.com/opencontainers/go-digest",
   220  											Version: "v1.0.0",
   221  											Ref:     "pkg:golang/github.com/opencontainers/go-digest@v1.0.0",
   222  										},
   223  										{
   224  											Name:    "golang.org/x/sync",
   225  											Version: "v0.1.0",
   226  											Ref:     "pkg:golang/golang.org/x/sync@v0.1.0",
   227  										},
   228  									},
   229  								},
   230  							},
   231  						},
   232  					},
   233  				},
   234  			},
   235  			want: types.ArtifactReference{
   236  				Name: registry + "/test/image:10",
   237  				Type: types.ArtifactCycloneDX,
   238  				ID:   "sha256:d07a1894bfd283b4ac26682ab48f12ad22cdc4fef9cf8b4c09056f631d3667a5",
   239  				BlobIDs: []string{
   240  					"sha256:d07a1894bfd283b4ac26682ab48f12ad22cdc4fef9cf8b4c09056f631d3667a5",
   241  				},
   242  			},
   243  		},
   244  		{
   245  			name: "404",
   246  			fields: fields{
   247  				imageName: registry + "/test/image:unknown",
   248  				repoDigests: []string{
   249  					registry + "/test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
   250  				},
   251  			},
   252  			artifactOpt: artifact.Option{
   253  				SBOMSources: []string{"oci"},
   254  			},
   255  			wantErr: "unable to get manifest",
   256  		},
   257  	}
   258  
   259  	for _, tt := range tests {
   260  		t.Run(tt.name, func(t *testing.T) {
   261  			mockCache := new(cache.MockArtifactCache)
   262  			mockCache.ApplyPutBlobExpectations(tt.putBlobExpectations)
   263  
   264  			fi := &fakei.FakeImage{}
   265  			fi.ConfigFileReturns(&v1.ConfigFile{}, nil)
   266  
   267  			img := &fakeImage{
   268  				name:        tt.fields.imageName,
   269  				repoDigests: tt.fields.repoDigests,
   270  				FakeImage:   fi,
   271  			}
   272  			a, err := image2.NewArtifact(img, mockCache, tt.artifactOpt)
   273  			require.NoError(t, err)
   274  
   275  			got, err := a.Inspect(context.Background())
   276  			if tt.wantErr != "" {
   277  				assert.ErrorContains(t, err, tt.wantErr)
   278  				return
   279  			}
   280  
   281  			require.NoError(t, err, tt.name)
   282  			got.CycloneDX = nil
   283  			assert.Equal(t, tt.want, got)
   284  		})
   285  	}
   286  }