github.com/google/osv-scalibr@v0.4.1/artifact/image/image.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 image provides functionality to scan a container image by layers for software 16 // inventory. 17 package image 18 19 import ( 20 "fmt" 21 "os" 22 "strings" 23 24 "github.com/google/go-containerregistry/pkg/name" 25 v1 "github.com/google/go-containerregistry/pkg/v1" 26 "github.com/google/go-containerregistry/pkg/v1/remote" 27 "github.com/google/osv-scalibr/artifact/image/require" 28 "github.com/google/osv-scalibr/artifact/image/unpack" 29 "github.com/opencontainers/go-digest" 30 31 scalibrfs "github.com/google/osv-scalibr/fs" 32 ) 33 34 // Layer is a filesystem derived from a container layer that can be scanned for software inventory. 35 // It also holds metadata about the container layer such as whether it is empty, its diffID, index, 36 // and command. 37 type Layer interface { 38 // FS outputs a filesystem that consist of the files found in the layer. This includes files that 39 // were added or modified. Whiteout files are also included in the filesystem if files or 40 // directories from previous layers were removed. 41 FS() scalibrfs.FS 42 // IsEmpty signifies whether the layer is empty. This should correspond with an empty filesystem 43 // produced by the FS method. 44 IsEmpty() bool 45 // DiffID is the hash of the uncompressed layer. Will be an empty string if the layer is empty. 46 DiffID() digest.Digest 47 // Command is the specific command that produced the layer. 48 Command() string 49 } 50 51 // ChainLayer is a filesystem derived from container layers that can be scanned for software 52 // inventory. It holds all the files found in layer 0, layer 1, ..., layer n (where n is the layer 53 // index). It also holds metadata about the latest container layer such as whether it is empty, its 54 // diffID, command, and index. 55 type ChainLayer interface { 56 // FS output an filesystem that consist of the files found in the layer n and all previous layers 57 // (layer 0, layer 1, ..., layer n). 58 FS() scalibrfs.FS 59 // Index is the index of the latest layer in the layer chain. 60 Index() int 61 // ChainID is the layer chain ID (sha256 hash) of the layer in the container image. 62 // https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid 63 ChainID() digest.Digest 64 // Layer is the latest layer in the layer chain. 65 Layer() Layer 66 } 67 68 // Image is a container image that can be scanned for software inventory. It is composed of a set of 69 // layers that can be scanned for software inventory. 70 type Image interface { 71 // Layers returns the layers of the image. 72 Layers() ([]Layer, error) 73 // ChainLayers returns the chain layers of the image. 74 ChainLayers() ([]ChainLayer, error) 75 // FS returns a SCALIBR compliant filesystem that represents the image. 76 FS() scalibrfs.FS 77 } 78 79 // V1ImageFromRemoteName creates a v1.Image from a remote container image name. 80 func V1ImageFromRemoteName(imageName string, imageOptions ...remote.Option) (v1.Image, error) { 81 imageName = strings.TrimPrefix(imageName, "https://") 82 var image v1.Image 83 if strings.Contains(imageName, "@") { 84 // Pull from a digest name. 85 ref, err := name.NewDigest(strings.TrimPrefix(imageName, "https://")) 86 if err != nil { 87 return nil, fmt.Errorf("unable to parse digest: %w", err) 88 } 89 descriptor, err := remote.Get(ref, imageOptions...) 90 if err != nil { 91 return nil, fmt.Errorf("couldn’t pull remote image %s: %w", ref, err) 92 } 93 image, err = descriptor.Image() 94 if err != nil { 95 return nil, fmt.Errorf("couldn’t parse image manifest %s: %w", ref, err) 96 } 97 } else { 98 // Pull from a tag. 99 tag, err := name.NewTag(strings.TrimPrefix(imageName, "https://")) 100 if err != nil { 101 return nil, fmt.Errorf("unable to parse image reference: %w", err) 102 } 103 image, err = remote.Image(tag, imageOptions...) 104 if err != nil { 105 return nil, fmt.Errorf("couldn’t pull remote image %s: %w", tag, err) 106 } 107 } 108 return image, nil 109 } 110 111 // NewFromRemoteName pulls a remote container and creates a 112 // SCALIBR filesystem for scanning it. 113 func NewFromRemoteName(imageName string, imageOptions ...remote.Option) (scalibrfs.FS, error) { 114 image, err := V1ImageFromRemoteName(imageName, imageOptions...) 115 if err != nil { 116 return nil, fmt.Errorf("failed to load image from remote name %q: %w", imageName, err) 117 } 118 return NewFromImage(image) 119 } 120 121 // NewFromImage creates a SCALIBR filesystem for scanning a container 122 // from its image descriptor. 123 func NewFromImage(image v1.Image) (scalibrfs.FS, error) { 124 outDir, err := os.MkdirTemp(os.TempDir(), "scalibr-container-") 125 if err != nil { 126 return nil, fmt.Errorf("couldn’t create tmp dir for image: %w", err) 127 } 128 // Squash the image's final layer into a directory. 129 cfg := &unpack.UnpackerConfig{ 130 SymlinkResolution: unpack.SymlinkRetain, 131 SymlinkErrStrategy: unpack.SymlinkErrLog, 132 MaxPass: unpack.DefaultMaxPass, 133 MaxFileBytes: unpack.DefaultMaxFileBytes, 134 Requirer: &require.FileRequirerAll{}, 135 } 136 unpacker, err := unpack.NewUnpacker(cfg) 137 if err != nil { 138 return nil, fmt.Errorf("failed to create image unpacker: %w", err) 139 } 140 if err = unpacker.UnpackSquashed(outDir, image); err != nil { 141 return nil, fmt.Errorf("failed to unpack image into directory %q: %w", outDir, err) 142 } 143 return scalibrfs.DirFS(outDir), nil 144 }