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 }