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  }