github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/oci/artifact.go (about) 1 package oci 2 3 import ( 4 "context" 5 "io" 6 "os" 7 "path/filepath" 8 "sync" 9 10 "github.com/cheggaaa/pb/v3" 11 "github.com/google/go-containerregistry/pkg/name" 12 v1 "github.com/google/go-containerregistry/pkg/v1" 13 "golang.org/x/xerrors" 14 15 "github.com/devseccon/trivy/pkg/downloader" 16 "github.com/devseccon/trivy/pkg/fanal/types" 17 "github.com/devseccon/trivy/pkg/remote" 18 ) 19 20 const ( 21 // Artifact types 22 CycloneDXArtifactType = "application/vnd.cyclonedx+json" 23 SPDXArtifactType = "application/spdx+json" 24 25 // Media types 26 OCIImageManifest = "application/vnd.oci.image.manifest.v1+json" 27 28 // Annotations 29 titleAnnotation = "org.opencontainers.image.title" 30 ) 31 32 var SupportedSBOMArtifactTypes = []string{ 33 CycloneDXArtifactType, 34 SPDXArtifactType, 35 } 36 37 // Option is a functional option 38 type Option func(*Artifact) 39 40 // WithImage takes an OCI v1 Image 41 func WithImage(img v1.Image) Option { 42 return func(a *Artifact) { 43 a.image = img 44 } 45 } 46 47 // Artifact is used to download artifacts such as vulnerability database and policies from OCI registries. 48 type Artifact struct { 49 m sync.Mutex 50 repository string 51 quiet bool 52 53 // For OCI registries 54 types.RegistryOptions 55 56 image v1.Image // For testing 57 } 58 59 // NewArtifact returns a new artifact 60 func NewArtifact(repo string, quiet bool, registryOpt types.RegistryOptions, opts ...Option) (*Artifact, error) { 61 art := &Artifact{ 62 repository: repo, 63 quiet: quiet, 64 RegistryOptions: registryOpt, 65 } 66 67 for _, o := range opts { 68 o(art) 69 } 70 return art, nil 71 } 72 73 func (a *Artifact) populate(ctx context.Context, opt types.RegistryOptions) error { 74 if a.image != nil { 75 return nil 76 } 77 78 a.m.Lock() 79 defer a.m.Unlock() 80 81 var nameOpts []name.Option 82 if opt.Insecure { 83 nameOpts = append(nameOpts, name.Insecure) 84 } 85 86 ref, err := name.ParseReference(a.repository, nameOpts...) 87 if err != nil { 88 return xerrors.Errorf("repository name error (%s): %w", a.repository, err) 89 } 90 91 a.image, err = remote.Image(ctx, ref, opt) 92 if err != nil { 93 return xerrors.Errorf("OCI repository error: %w", err) 94 } 95 return nil 96 } 97 98 type DownloadOption struct { 99 MediaType string // Accept any media type if not specified 100 Filename string // Use the annotation if not specified 101 } 102 103 func (a *Artifact) Download(ctx context.Context, dir string, opt DownloadOption) error { 104 if err := a.populate(ctx, a.RegistryOptions); err != nil { 105 return err 106 } 107 108 layers, err := a.image.Layers() 109 if err != nil { 110 return xerrors.Errorf("OCI layer error: %w", err) 111 } 112 113 manifest, err := a.image.Manifest() 114 if err != nil { 115 return xerrors.Errorf("OCI manifest error: %w", err) 116 } 117 118 // A single layer is only supported now. 119 if len(layers) != 1 || len(manifest.Layers) != 1 { 120 return xerrors.Errorf("OCI artifact must be a single layer") 121 } 122 123 // Take the first layer 124 layer := layers[0] 125 126 // Take the file name of the first layer if not specified 127 fileName := opt.Filename 128 if fileName == "" { 129 if v, ok := manifest.Layers[0].Annotations[titleAnnotation]; !ok { 130 return xerrors.Errorf("annotation %s is missing", titleAnnotation) 131 } else { 132 fileName = v 133 } 134 } 135 136 layerMediaType, err := layer.MediaType() 137 if err != nil { 138 return xerrors.Errorf("media type error: %w", err) 139 } else if opt.MediaType != "" && opt.MediaType != string(layerMediaType) { 140 return xerrors.Errorf("unacceptable media type: %s", string(layerMediaType)) 141 } 142 143 if err = a.download(ctx, layer, fileName, dir); err != nil { 144 return xerrors.Errorf("oci download error: %w", err) 145 } 146 147 return nil 148 } 149 150 func (a *Artifact) download(ctx context.Context, layer v1.Layer, fileName, dir string) error { 151 size, err := layer.Size() 152 if err != nil { 153 return xerrors.Errorf("size error: %w", err) 154 } 155 156 rc, err := layer.Compressed() 157 if err != nil { 158 return xerrors.Errorf("failed to fetch the layer: %w", err) 159 } 160 defer rc.Close() 161 162 // Show progress bar 163 bar := pb.Full.Start64(size) 164 if a.quiet { 165 bar.SetWriter(io.Discard) 166 } 167 pr := bar.NewProxyReader(rc) 168 defer bar.Finish() 169 170 // https://github.com/hashicorp/go-getter/issues/326 171 tempDir, err := os.MkdirTemp("", "trivy") 172 if err != nil { 173 return xerrors.Errorf("failed to create a temp dir: %w", err) 174 } 175 176 f, err := os.Create(filepath.Join(tempDir, fileName)) 177 if err != nil { 178 return xerrors.Errorf("failed to create a temp file: %w", err) 179 } 180 defer func() { 181 _ = f.Close() 182 _ = os.RemoveAll(tempDir) 183 }() 184 185 // Download the layer content into a temporal file 186 if _, err = io.Copy(f, pr); err != nil { 187 return xerrors.Errorf("copy error: %w", err) 188 } 189 190 // Decompress the downloaded file if it is compressed and copy it into the dst 191 if err = downloader.Download(ctx, f.Name(), dir, dir); err != nil { 192 return xerrors.Errorf("download error: %w", err) 193 } 194 195 return nil 196 } 197 198 func (a *Artifact) Digest(ctx context.Context) (string, error) { 199 if err := a.populate(ctx, a.RegistryOptions); err != nil { 200 return "", err 201 } 202 203 digest, err := a.image.Digest() 204 if err != nil { 205 return "", xerrors.Errorf("digest error: %w", err) 206 } 207 return digest.String(), nil 208 }