github.com/google/osv-scalibr@v0.4.1/enricher/baseimage/baseimage_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package baseimage_test
    16  
    17  import (
    18  	"errors"
    19  	"testing"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/google/go-cmp/cmp/cmpopts"
    23  	"github.com/google/osv-scalibr/enricher/baseimage"
    24  	"github.com/google/osv-scalibr/extractor"
    25  	"github.com/google/osv-scalibr/inventory"
    26  	"github.com/google/osv-scalibr/plugin"
    27  	"github.com/mohae/deepcopy"
    28  	"github.com/opencontainers/go-digest"
    29  	"github.com/opencontainers/image-spec/identity"
    30  	"google.golang.org/protobuf/testing/protocmp"
    31  )
    32  
    33  func TestNew(t *testing.T) {
    34  	tests := []struct {
    35  		name    string
    36  		cfg     *baseimage.Config
    37  		wantErr error
    38  	}{
    39  		{
    40  			name:    "nil config",
    41  			wantErr: cmpopts.AnyError,
    42  		},
    43  		{
    44  			name:    "nil client",
    45  			cfg:     &baseimage.Config{},
    46  			wantErr: cmpopts.AnyError,
    47  		},
    48  		{
    49  			name: "valid_config",
    50  			cfg: &baseimage.Config{
    51  				Client: mustNewClientFake(t, &config{}),
    52  			},
    53  		},
    54  	}
    55  
    56  	for _, tc := range tests {
    57  		t.Run(tc.name, func(t *testing.T) {
    58  			got, err := baseimage.New(tc.cfg)
    59  			if !cmp.Equal(err, tc.wantErr, cmpopts.EquateErrors()) {
    60  				t.Errorf("New(%v) returned an unexpected error: %v", tc.cfg, err)
    61  			}
    62  			if err != nil && got == nil {
    63  				return
    64  			}
    65  			opts := []cmp.Option{
    66  				cmp.AllowUnexported(clientFake{}),
    67  			}
    68  			if diff := cmp.Diff(tc.cfg, got.Config(), opts...); diff != "" {
    69  				t.Errorf("New(%v) returned an unexpected diff (-want +got): %v", tc.cfg, diff)
    70  			}
    71  		})
    72  	}
    73  }
    74  
    75  func TestVersion(t *testing.T) {
    76  	e := baseimage.Enricher{}
    77  	if e.Version() != baseimage.Version {
    78  		t.Errorf("Version() = %q, want %q", e.Version(), baseimage.Version)
    79  	}
    80  }
    81  
    82  func TestRequirements(t *testing.T) {
    83  	e := &baseimage.Enricher{}
    84  	got := e.Requirements()
    85  	want := &plugin.Capabilities{Network: plugin.NetworkOnline}
    86  	opts := []cmp.Option{
    87  		protocmp.Transform(),
    88  	}
    89  	if diff := cmp.Diff(want, got, opts...); diff != "" {
    90  		t.Errorf("Requirements() returned diff (-want +got):\n%s", diff)
    91  	}
    92  }
    93  
    94  func TestRequiredPlugins(t *testing.T) {
    95  	e := &baseimage.Enricher{}
    96  	got := e.RequiredPlugins()
    97  	want := []string{}
    98  	opts := []cmp.Option{
    99  		protocmp.Transform(),
   100  	}
   101  	if diff := cmp.Diff(want, got, opts...); diff != "" {
   102  		t.Errorf("RequiredPlugins() returned diff (-want +got):\n%s", diff)
   103  	}
   104  }
   105  
   106  func TestEnrich(t *testing.T) {
   107  	// Test layer metadata.
   108  	// lm1: in base image alpine.
   109  	// lm2: in base image nginx, but not an edge layer of the base image.
   110  	// lm3: in base image nginx.
   111  	lm1DiffID := digest.FromString("alpine")
   112  	lm2DiffID := digest.FromString("nginxnonedge")
   113  	lm3DiffID := digest.FromString("nginx")
   114  
   115  	lm1ChainID := lm1DiffID.String()
   116  	lm12ChainID := identity.ChainID([]digest.Digest{lm1DiffID, lm2DiffID}).String()
   117  	lm123ChainID := identity.ChainID([]digest.Digest{lm1DiffID, lm2DiffID, lm3DiffID}).String()
   118  
   119  	lm1 := &extractor.LayerMetadata{
   120  		DiffID: lm1DiffID,
   121  	}
   122  	lm1Enriched := &extractor.LayerMetadata{
   123  		DiffID:         lm1DiffID,
   124  		BaseImageIndex: 2,
   125  	}
   126  	lm1EnrichedNoOtherBaseImages := &extractor.LayerMetadata{
   127  		DiffID:         lm1DiffID,
   128  		BaseImageIndex: 1,
   129  	}
   130  	lm2 := &extractor.LayerMetadata{
   131  		DiffID: lm2DiffID,
   132  	}
   133  	lm2Enriched := &extractor.LayerMetadata{
   134  		DiffID:         lm2DiffID,
   135  		BaseImageIndex: 1,
   136  	}
   137  	lm3 := &extractor.LayerMetadata{
   138  		DiffID: lm3DiffID,
   139  	}
   140  	lm3Enriched := &extractor.LayerMetadata{
   141  		DiffID:         lm3DiffID,
   142  		BaseImageIndex: 1,
   143  	}
   144  	clientErr := errors.New("client error")
   145  	lmErrDiffID := digest.FromString("clienterror")
   146  	lmErr := &extractor.LayerMetadata{
   147  		DiffID: lmErrDiffID,
   148  	}
   149  
   150  	lm12ErrChainID := identity.ChainID([]digest.Digest{lm1DiffID, lm2DiffID, lmErrDiffID}).String()
   151  	lmErr2ChainID := identity.ChainID([]digest.Digest{lmErrDiffID, lm2DiffID}).String()
   152  	lmErr23ChainID := identity.ChainID([]digest.Digest{lmErrDiffID, lm2DiffID, lm3DiffID}).String()
   153  
   154  	tests := []struct {
   155  		name    string
   156  		cfg     *baseimage.Config
   157  		inv     *inventory.Inventory
   158  		want    *inventory.Inventory
   159  		wantErr error
   160  	}{
   161  		{
   162  			name: "no_image_metadata_to_enrich",
   163  			cfg: &baseimage.Config{
   164  				Client: mustNewClientFake(t, &config{}),
   165  			},
   166  			inv:  &inventory.Inventory{},
   167  			want: &inventory.Inventory{},
   168  		},
   169  		{
   170  			name: "enrich_layers",
   171  			cfg: &baseimage.Config{
   172  				Client: mustNewClientFake(t, &config{ReqRespErrs: []reqRespErr{
   173  					{
   174  						req:  &baseimage.Request{ChainID: lm123ChainID},
   175  						resp: &baseimage.Response{Results: []*baseimage.Result{&baseimage.Result{"nginx"}}},
   176  					},
   177  					{
   178  						req: &baseimage.Request{ChainID: lm12ChainID},
   179  					},
   180  					{
   181  						req:  &baseimage.Request{ChainID: lm1ChainID},
   182  						resp: &baseimage.Response{Results: []*baseimage.Result{&baseimage.Result{"alpine"}}},
   183  					},
   184  				}}),
   185  			},
   186  			inv: &inventory.Inventory{
   187  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   188  					{LayerMetadata: []*extractor.LayerMetadata{lm1, lm2, lm3}},
   189  				},
   190  			},
   191  			want: &inventory.Inventory{
   192  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   193  					{
   194  						LayerMetadata: []*extractor.LayerMetadata{lm1Enriched, lm2Enriched, lm3Enriched},
   195  						BaseImages: [][]*extractor.BaseImageDetails{
   196  							[]*extractor.BaseImageDetails{},
   197  							[]*extractor.BaseImageDetails{
   198  								&extractor.BaseImageDetails{
   199  									Repository: "nginx",
   200  									Registry:   "docker.io",
   201  									ChainID:    digest.Digest(lm123ChainID),
   202  									Plugin:     "baseimage",
   203  								},
   204  							},
   205  							[]*extractor.BaseImageDetails{
   206  								&extractor.BaseImageDetails{
   207  									Repository: "alpine",
   208  									Registry:   "docker.io",
   209  									ChainID:    digest.Digest(lm1ChainID),
   210  									Plugin:     "baseimage",
   211  								},
   212  							},
   213  						},
   214  					},
   215  				},
   216  			},
   217  		},
   218  		{
   219  			name: "same_layer_chainID_in_different_images,_should_use_cache",
   220  			cfg: &baseimage.Config{
   221  				Client: mustNewClientFake(t, &config{ReqRespErrs: []reqRespErr{
   222  					{
   223  						req:  &baseimage.Request{ChainID: lm1ChainID},
   224  						resp: &baseimage.Response{Results: []*baseimage.Result{&baseimage.Result{"alpine"}}},
   225  					},
   226  				}}),
   227  			},
   228  			inv: &inventory.Inventory{
   229  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   230  					{LayerMetadata: []*extractor.LayerMetadata{lm1}},
   231  					{LayerMetadata: []*extractor.LayerMetadata{lm1}},
   232  				},
   233  			},
   234  			want: &inventory.Inventory{
   235  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   236  					{
   237  						LayerMetadata: []*extractor.LayerMetadata{lm1EnrichedNoOtherBaseImages},
   238  						BaseImages: [][]*extractor.BaseImageDetails{
   239  							[]*extractor.BaseImageDetails{},
   240  							[]*extractor.BaseImageDetails{
   241  								&extractor.BaseImageDetails{
   242  									Repository: "alpine",
   243  									Registry:   "docker.io",
   244  									ChainID:    digest.Digest(lm1ChainID),
   245  									Plugin:     "baseimage",
   246  								},
   247  							},
   248  						},
   249  					},
   250  					{
   251  						LayerMetadata: []*extractor.LayerMetadata{lm1EnrichedNoOtherBaseImages},
   252  						BaseImages: [][]*extractor.BaseImageDetails{
   253  							[]*extractor.BaseImageDetails{},
   254  							[]*extractor.BaseImageDetails{
   255  								&extractor.BaseImageDetails{
   256  									Repository: "alpine",
   257  									Registry:   "docker.io",
   258  									ChainID:    digest.Digest(lm1ChainID),
   259  									Plugin:     "baseimage",
   260  								},
   261  							},
   262  						},
   263  					},
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name: "client_error_on_last_layer",
   269  			cfg: &baseimage.Config{
   270  				Client: mustNewClientFake(t, &config{ReqRespErrs: []reqRespErr{
   271  					{
   272  						req: &baseimage.Request{ChainID: lm12ErrChainID},
   273  						err: clientErr,
   274  					},
   275  					{
   276  						req: &baseimage.Request{ChainID: lm12ChainID},
   277  					},
   278  					{
   279  						req:  &baseimage.Request{ChainID: lm1ChainID},
   280  						resp: &baseimage.Response{Results: []*baseimage.Result{&baseimage.Result{"alpine"}}},
   281  					},
   282  				}}),
   283  			},
   284  			inv: &inventory.Inventory{
   285  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   286  					{LayerMetadata: []*extractor.LayerMetadata{lm1, lm2, lmErr}},
   287  				},
   288  			},
   289  			want: &inventory.Inventory{
   290  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   291  					{
   292  						// lm1 is enriched with the base image alpine.
   293  						// lm2 is not enriched because the layer above it lmErr does not get enriched.
   294  						// lmErr is not enriched because the client returns an error.
   295  						LayerMetadata: []*extractor.LayerMetadata{lm1, lm2, lmErr},
   296  						BaseImages: [][]*extractor.BaseImageDetails{
   297  							[]*extractor.BaseImageDetails{},
   298  						},
   299  					},
   300  				},
   301  			},
   302  			wantErr: clientErr,
   303  		},
   304  		{
   305  			name: "client_error_on_first_layer",
   306  			cfg: &baseimage.Config{
   307  				Client: mustNewClientFake(t, &config{ReqRespErrs: []reqRespErr{
   308  					{
   309  						req:  &baseimage.Request{ChainID: lmErr23ChainID},
   310  						resp: &baseimage.Response{Results: []*baseimage.Result{&baseimage.Result{"nginx"}}},
   311  					},
   312  					{
   313  						req: &baseimage.Request{ChainID: lmErr2ChainID},
   314  					},
   315  					{
   316  						req: &baseimage.Request{ChainID: lmErrDiffID.String()},
   317  						err: clientErr,
   318  					},
   319  				}}),
   320  			},
   321  			inv: &inventory.Inventory{
   322  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   323  					{LayerMetadata: []*extractor.LayerMetadata{lmErr, lm2, lm3}},
   324  				},
   325  			},
   326  			want: &inventory.Inventory{
   327  				ContainerImageMetadata: []*extractor.ContainerImageMetadata{
   328  					{
   329  						// Nothing is enriched because one of the layer requests failed, everything is cancelled
   330  						LayerMetadata: []*extractor.LayerMetadata{lmErr, lm2, lm3},
   331  						BaseImages: [][]*extractor.BaseImageDetails{
   332  							[]*extractor.BaseImageDetails{},
   333  						},
   334  					},
   335  				},
   336  			},
   337  			wantErr: clientErr,
   338  		},
   339  	}
   340  
   341  	for _, tc := range tests {
   342  		t.Run(tc.name, func(t *testing.T) {
   343  			e := mustNew(t, tc.cfg)
   344  			inv := deepcopy.Copy(tc.inv).(*inventory.Inventory)
   345  			if err := e.Enrich(t.Context(), nil, inv); !cmp.Equal(err, tc.wantErr, cmpopts.EquateErrors()) {
   346  				t.Errorf("Enrich(%v) returned error: %v, want error: %v\n", tc.inv, err, tc.wantErr)
   347  			}
   348  			opts := []cmp.Option{
   349  				protocmp.Transform(),
   350  				cmpopts.IgnoreFields(extractor.LayerMetadata{}, "ParentContainer"),
   351  			}
   352  			if diff := cmp.Diff(tc.want, inv, opts...); diff != "" {
   353  				t.Errorf("Enrich(%v) returned diff (-want +got):\n%s\n", tc.inv, diff)
   354  			}
   355  		})
   356  	}
   357  }
   358  
   359  func mustNew(t *testing.T, cfg *baseimage.Config) *baseimage.Enricher {
   360  	t.Helper()
   361  	e, err := baseimage.New(cfg)
   362  	if err != nil {
   363  		t.Fatalf("Failed to create base image enricher: %v", err)
   364  	}
   365  	return e
   366  }