github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/image/daemon/containerd.go (about) 1 package daemon 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "strings" 11 "time" 12 13 "github.com/containerd/containerd" 14 "github.com/containerd/containerd/content" 15 "github.com/containerd/containerd/images/archive" 16 "github.com/containerd/containerd/namespaces" 17 "github.com/containerd/containerd/platforms" 18 refdocker "github.com/containerd/containerd/reference/docker" 19 api "github.com/docker/docker/api/types" 20 "github.com/docker/docker/api/types/container" 21 "github.com/docker/go-connections/nat" 22 v1 "github.com/google/go-containerregistry/pkg/v1" 23 "github.com/opencontainers/go-digest" 24 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 25 "github.com/samber/lo" 26 "golang.org/x/xerrors" 27 28 "github.com/devseccon/trivy/pkg/fanal/types" 29 ) 30 31 const ( 32 defaultContainerdSocket = "/run/containerd/containerd.sock" 33 defaultContainerdNamespace = "default" 34 ) 35 36 type familiarNamed string 37 38 func (n familiarNamed) Name() string { 39 return strings.Split(string(n), ":")[0] 40 } 41 42 func (n familiarNamed) Tag() string { 43 s := strings.Split(string(n), ":") 44 if len(s) < 2 { 45 return "" 46 } 47 48 return s[1] 49 } 50 51 func (n familiarNamed) String() string { 52 return string(n) 53 } 54 55 func imageWriter(client *containerd.Client, img containerd.Image, platform types.Platform) imageSave { 56 return func(ctx context.Context, ref []string) (io.ReadCloser, error) { 57 if len(ref) < 1 { 58 return nil, xerrors.New("no image reference") 59 } 60 imgOpts := archive.WithImage(client.ImageService(), ref[0]) 61 manifestOpts := archive.WithManifest(img.Target()) 62 63 var platformMatchComparer platforms.MatchComparer 64 if platform.Platform == nil { 65 platformMatchComparer = platforms.DefaultStrict() 66 } else { 67 platformMatchComparer = img.Platform() 68 } 69 platOpts := archive.WithPlatform(platformMatchComparer) 70 pr, pw := io.Pipe() 71 go func() { 72 pw.CloseWithError(archive.Export(ctx, client.ContentStore(), pw, imgOpts, manifestOpts, platOpts)) 73 }() 74 return pr, nil 75 } 76 } 77 78 // ContainerdImage implements v1.Image 79 func ContainerdImage(ctx context.Context, imageName string, opts types.ImageOptions) (Image, func(), error) { 80 cleanup := func() {} 81 82 addr := os.Getenv("CONTAINERD_ADDRESS") 83 if addr == "" { 84 // TODO: support rootless 85 addr = defaultContainerdSocket 86 } 87 88 if _, err := os.Stat(addr); errors.Is(err, os.ErrNotExist) { 89 return nil, cleanup, xerrors.Errorf("containerd socket not found: %s", addr) 90 } 91 92 ref, searchFilters, err := parseReference(imageName) 93 if err != nil { 94 return nil, cleanup, err 95 } 96 97 var options []containerd.ClientOpt 98 if opts.RegistryOptions.Platform.Platform != nil { 99 ociPlatform, err := platforms.Parse(opts.RegistryOptions.Platform.String()) 100 if err != nil { 101 return nil, cleanup, err 102 } 103 104 options = append(options, containerd.WithDefaultPlatform(platforms.OnlyStrict(ociPlatform))) 105 } 106 107 client, err := containerd.New(addr, options...) 108 if err != nil { 109 return nil, cleanup, xerrors.Errorf("failed to initialize a containerd client: %w", err) 110 } 111 112 namespace := os.Getenv("CONTAINERD_NAMESPACE") 113 if namespace == "" { 114 namespace = defaultContainerdNamespace 115 } 116 117 ctx = namespaces.WithNamespace(ctx, namespace) 118 119 imgs, err := client.ListImages(ctx, searchFilters...) 120 if err != nil { 121 return nil, cleanup, xerrors.Errorf("failed to list images from containerd client: %w", err) 122 } 123 124 if len(imgs) < 1 { 125 return nil, cleanup, xerrors.Errorf("image not found in containerd store: %s", imageName) 126 } 127 128 img := imgs[0] 129 130 f, err := os.CreateTemp("", "fanal-containerd-*") 131 if err != nil { 132 return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err) 133 } 134 135 cleanup = func() { 136 _ = client.Close() 137 _ = f.Close() 138 _ = os.Remove(f.Name()) 139 } 140 141 insp, history, ref, err := inspect(ctx, img, ref) 142 if err != nil { 143 return nil, cleanup, xerrors.Errorf("inspect error: %w", err) 144 } 145 146 return &image{ 147 opener: imageOpener(ctx, ref.String(), f, imageWriter(client, img, opts.RegistryOptions.Platform)), 148 inspect: insp, 149 history: history, 150 }, cleanup, nil 151 } 152 153 func parseReference(imageName string) (refdocker.Reference, []string, error) { 154 ref, err := refdocker.ParseAnyReference(imageName) 155 if err != nil { 156 return nil, nil, xerrors.Errorf("parse error: %w", err) 157 } 158 159 d, isDigested := ref.(refdocker.Digested) 160 n, isNamed := ref.(refdocker.Named) 161 nt, isNamedAndTagged := ref.(refdocker.NamedTagged) 162 163 // a name plus a digest 164 // example: name@sha256:41adb3ef... 165 if isDigested && isNamed { 166 dgst := d.Digest() 167 // for the filters, each slice entry is logically or'd. each 168 // comma-separated filter is logically anded 169 return ref, []string{ 170 fmt.Sprintf(`name~="^%s(:|@).*",target.digest==%q`, n.Name(), dgst), 171 fmt.Sprintf(`name~="^%s(:|@).*",target.digest==%q`, refdocker.FamiliarName(n), dgst), 172 }, nil 173 } 174 175 // digested, but not named. i.e. a plain digest 176 // example: sha256:41adb3ef... 177 if isDigested { 178 return ref, []string{fmt.Sprintf(`target.digest==%q`, d.Digest())}, nil 179 } 180 181 // a name plus a tag 182 // example: name:tag 183 if isNamedAndTagged { 184 tag := nt.Tag() 185 return familiarNamed(imageName), []string{ 186 fmt.Sprintf(`name=="%s:%s"`, nt.Name(), tag), 187 fmt.Sprintf(`name=="%s:%s"`, refdocker.FamiliarName(nt), tag), 188 }, nil 189 } 190 191 return nil, nil, xerrors.Errorf("failed to parse image reference: %s", imageName) 192 } 193 194 // readImageConfig reads the config spec (`application/vnd.oci.image.config.v1+json`) for img.platform from content store. 195 // ported from https://github.com/containerd/nerdctl/blob/7dfbaa2122628921febeb097e7a8a86074dc931d/pkg/imgutil/imgutil.go#L377-L393 196 func readImageConfig(ctx context.Context, img containerd.Image) (ocispec.Image, ocispec.Descriptor, error) { 197 var config ocispec.Image 198 199 configDesc, err := img.Config(ctx) // aware of img.platform 200 if err != nil { 201 return config, configDesc, err 202 } 203 p, err := content.ReadBlob(ctx, img.ContentStore(), configDesc) 204 if err != nil { 205 return config, configDesc, err 206 } 207 if err = json.Unmarshal(p, &config); err != nil { 208 return config, configDesc, err 209 } 210 return config, configDesc, nil 211 } 212 213 // ported from https://github.com/containerd/nerdctl/blob/d110fea18018f13c3f798fa6565e482f3ff03591/pkg/inspecttypes/dockercompat/dockercompat.go#L279-L321 214 func inspect(ctx context.Context, img containerd.Image, ref refdocker.Reference) (api.ImageInspect, []v1.History, refdocker.Reference, error) { 215 if _, ok := ref.(refdocker.Digested); ok { 216 ref = familiarNamed(img.Name()) 217 } 218 219 var tag string 220 if tagged, ok := ref.(refdocker.Tagged); ok { 221 tag = tagged.Tag() 222 } 223 224 var repository string 225 if n, isNamed := ref.(refdocker.Named); isNamed { 226 repository = refdocker.FamiliarName(n) 227 } 228 229 imgConfig, imgConfigDesc, err := readImageConfig(ctx, img) 230 if err != nil { 231 return api.ImageInspect{}, nil, nil, err 232 } 233 234 var lastHistory ocispec.History 235 if len(imgConfig.History) > 0 { 236 lastHistory = imgConfig.History[len(imgConfig.History)-1] 237 } 238 239 var history []v1.History 240 for _, h := range imgConfig.History { 241 history = append(history, v1.History{ 242 Author: h.Author, 243 Created: v1.Time{Time: *h.Created}, 244 CreatedBy: h.CreatedBy, 245 Comment: h.Comment, 246 EmptyLayer: h.EmptyLayer, 247 }) 248 } 249 250 portSet := make(nat.PortSet) 251 for k := range imgConfig.Config.ExposedPorts { 252 portSet[nat.Port(k)] = struct{}{} 253 } 254 255 created := "" 256 if lastHistory.Created != nil { 257 created = lastHistory.Created.Format(time.RFC3339Nano) 258 } 259 260 return api.ImageInspect{ 261 ID: imgConfigDesc.Digest.String(), 262 RepoTags: []string{fmt.Sprintf("%s:%s", repository, tag)}, 263 RepoDigests: []string{fmt.Sprintf("%s@%s", repository, img.Target().Digest)}, 264 Comment: lastHistory.Comment, 265 Created: created, 266 Author: lastHistory.Author, 267 Config: &container.Config{ 268 User: imgConfig.Config.User, 269 ExposedPorts: portSet, 270 Env: imgConfig.Config.Env, 271 Cmd: imgConfig.Config.Cmd, 272 Volumes: imgConfig.Config.Volumes, 273 WorkingDir: imgConfig.Config.WorkingDir, 274 Entrypoint: imgConfig.Config.Entrypoint, 275 Labels: imgConfig.Config.Labels, 276 }, 277 Architecture: imgConfig.Architecture, 278 Os: imgConfig.OS, 279 RootFS: api.RootFS{ 280 Type: imgConfig.RootFS.Type, 281 Layers: lo.Map(imgConfig.RootFS.DiffIDs, func(d digest.Digest, _ int) string { 282 return d.String() 283 }), 284 }, 285 }, history, ref, nil 286 }