github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/image/daemon/image.go (about)

     1  package daemon
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	dimage "github.com/docker/docker/api/types/image"
    14  	v1 "github.com/google/go-containerregistry/pkg/v1"
    15  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    16  	"github.com/samber/lo"
    17  	"golang.org/x/xerrors"
    18  
    19  	"github.com/devseccon/trivy/pkg/log"
    20  )
    21  
    22  type Image interface {
    23  	v1.Image
    24  	RepoTags() []string
    25  	RepoDigests() []string
    26  }
    27  
    28  var mu sync.Mutex
    29  
    30  type opener func() (v1.Image, error)
    31  
    32  type imageSave func(context.Context, []string) (io.ReadCloser, error)
    33  
    34  func imageOpener(ctx context.Context, ref string, f *os.File, imageSave imageSave) opener {
    35  	return func() (v1.Image, error) {
    36  		// Store the tarball in local filesystem and return a new reader into the bytes each time we need to access something.
    37  		rc, err := imageSave(ctx, []string{ref})
    38  		if err != nil {
    39  			return nil, xerrors.Errorf("unable to export the image: %w", err)
    40  		}
    41  		defer rc.Close()
    42  
    43  		if _, err = io.Copy(f, rc); err != nil {
    44  			return nil, xerrors.Errorf("failed to copy the image: %w", err)
    45  		}
    46  		defer f.Close()
    47  
    48  		img, err := tarball.ImageFromPath(f.Name(), nil)
    49  		if err != nil {
    50  			return nil, xerrors.Errorf("failed to initialize the struct from the temporary file: %w", err)
    51  		}
    52  
    53  		return img, nil
    54  	}
    55  }
    56  
    57  // image is a wrapper for github.com/google/go-containerregistry/pkg/v1/daemon.Image
    58  // daemon.Image loads the entire image into the memory at first,
    59  // but it doesn't need to load it if the information is already in the cache,
    60  // To avoid entire loading, this wrapper uses ImageInspectWithRaw and checks image ID and layer IDs.
    61  type image struct {
    62  	v1.Image
    63  	opener  opener
    64  	inspect types.ImageInspect
    65  	history []v1.History
    66  }
    67  
    68  // populateImage initializes an "image" struct.
    69  // This method is called by some goroutines at the same time.
    70  // To prevent multiple heavy initializations, the lock is necessary.
    71  func (img *image) populateImage() (err error) {
    72  	mu.Lock()
    73  	defer mu.Unlock()
    74  
    75  	// img.Image is already initialized, so we don't have to do it again.
    76  	if img.Image != nil {
    77  		return nil
    78  	}
    79  
    80  	img.Image, err = img.opener()
    81  	if err != nil {
    82  		return xerrors.Errorf("unable to open: %w", err)
    83  	}
    84  
    85  	return nil
    86  }
    87  
    88  func (img *image) ConfigName() (v1.Hash, error) {
    89  	return v1.NewHash(img.inspect.ID)
    90  }
    91  
    92  func (img *image) ConfigFile() (*v1.ConfigFile, error) {
    93  	if len(img.inspect.RootFS.Layers) == 0 {
    94  		// Podman doesn't return RootFS...
    95  		return img.configFile()
    96  	}
    97  
    98  	nonEmptyLayerCount := lo.CountBy(img.history, func(history v1.History) bool {
    99  		return !history.EmptyLayer
   100  	})
   101  
   102  	if len(img.inspect.RootFS.Layers) != nonEmptyLayerCount {
   103  		// In cases where empty layers are not correctly determined from the history API.
   104  		// There are some edge cases where we cannot guess empty layers well.
   105  		return img.configFile()
   106  	}
   107  
   108  	diffIDs, err := img.diffIDs()
   109  	if err != nil {
   110  		return nil, xerrors.Errorf("unable to get diff IDs: %w", err)
   111  	}
   112  
   113  	created, err := time.Parse(time.RFC3339Nano, img.inspect.Created)
   114  	if err != nil {
   115  		return nil, xerrors.Errorf("failed parsing created %s: %w", img.inspect.Created, err)
   116  	}
   117  
   118  	return &v1.ConfigFile{
   119  		Architecture:  img.inspect.Architecture,
   120  		Author:        img.inspect.Author,
   121  		Container:     img.inspect.Container,
   122  		Created:       v1.Time{Time: created},
   123  		DockerVersion: img.inspect.DockerVersion,
   124  		Config:        img.imageConfig(img.inspect.Config),
   125  		History:       img.history,
   126  		OS:            img.inspect.Os,
   127  		RootFS: v1.RootFS{
   128  			Type:    img.inspect.RootFS.Type,
   129  			DiffIDs: diffIDs,
   130  		},
   131  	}, nil
   132  }
   133  
   134  func (img *image) configFile() (*v1.ConfigFile, error) {
   135  	log.Logger.Debug("Saving the container image to a local file to obtain the image config...")
   136  
   137  	// Need to fall back into expensive operations like "docker save"
   138  	// because the config file cannot be generated properly from container engine API for some reason.
   139  	if err := img.populateImage(); err != nil {
   140  		return nil, xerrors.Errorf("unable to populate: %w", err)
   141  	}
   142  	return img.Image.ConfigFile()
   143  }
   144  
   145  func (img *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
   146  	if err := img.populateImage(); err != nil {
   147  		return nil, xerrors.Errorf("unable to populate: %w", err)
   148  	}
   149  	return img.Image.LayerByDiffID(h)
   150  }
   151  
   152  func (img *image) RawConfigFile() ([]byte, error) {
   153  	if err := img.populateImage(); err != nil {
   154  		return nil, xerrors.Errorf("unable to populate: %w", err)
   155  	}
   156  	return img.Image.RawConfigFile()
   157  }
   158  
   159  func (img *image) RepoTags() []string {
   160  	return img.inspect.RepoTags
   161  }
   162  
   163  func (img *image) RepoDigests() []string {
   164  	return img.inspect.RepoDigests
   165  }
   166  
   167  func (img *image) diffIDs() ([]v1.Hash, error) {
   168  	var diffIDs []v1.Hash
   169  	for _, l := range img.inspect.RootFS.Layers {
   170  		h, err := v1.NewHash(l)
   171  		if err != nil {
   172  			return nil, xerrors.Errorf("invalid hash %s: %w", l, err)
   173  		}
   174  		diffIDs = append(diffIDs, h)
   175  	}
   176  	return diffIDs, nil
   177  }
   178  
   179  func (img *image) imageConfig(config *container.Config) v1.Config {
   180  	if config == nil {
   181  		return v1.Config{}
   182  	}
   183  
   184  	c := v1.Config{
   185  		AttachStderr:    config.AttachStderr,
   186  		AttachStdin:     config.AttachStdin,
   187  		AttachStdout:    config.AttachStdout,
   188  		Cmd:             config.Cmd,
   189  		Domainname:      config.Domainname,
   190  		Entrypoint:      config.Entrypoint,
   191  		Env:             config.Env,
   192  		Hostname:        config.Hostname,
   193  		Image:           config.Image,
   194  		Labels:          config.Labels,
   195  		OnBuild:         config.OnBuild,
   196  		OpenStdin:       config.OpenStdin,
   197  		StdinOnce:       config.StdinOnce,
   198  		Tty:             config.Tty,
   199  		User:            config.User,
   200  		Volumes:         config.Volumes,
   201  		WorkingDir:      config.WorkingDir,
   202  		ArgsEscaped:     config.ArgsEscaped,
   203  		NetworkDisabled: config.NetworkDisabled,
   204  		MacAddress:      config.MacAddress,
   205  		StopSignal:      config.StopSignal,
   206  		Shell:           config.Shell,
   207  	}
   208  
   209  	if config.Healthcheck != nil {
   210  		c.Healthcheck = &v1.HealthConfig{
   211  			Test:        config.Healthcheck.Test,
   212  			Interval:    config.Healthcheck.Interval,
   213  			Timeout:     config.Healthcheck.Timeout,
   214  			StartPeriod: config.Healthcheck.StartPeriod,
   215  			Retries:     config.Healthcheck.Retries,
   216  		}
   217  	}
   218  
   219  	if len(config.ExposedPorts) > 0 {
   220  		c.ExposedPorts = make(map[string]struct{})
   221  		for port := range c.ExposedPorts {
   222  			c.ExposedPorts[port] = struct{}{}
   223  		}
   224  	}
   225  
   226  	return c
   227  }
   228  
   229  func configHistory(dhistory []dimage.HistoryResponseItem) []v1.History {
   230  	// Fill only required metadata
   231  	var history []v1.History
   232  
   233  	for i := len(dhistory) - 1; i >= 0; i-- {
   234  		h := dhistory[i]
   235  		history = append(history, v1.History{
   236  			Created: v1.Time{
   237  				Time: time.Unix(h.Created, 0).UTC(),
   238  			},
   239  			CreatedBy:  h.CreatedBy,
   240  			Comment:    h.Comment,
   241  			EmptyLayer: emptyLayer(h),
   242  		})
   243  	}
   244  	return history
   245  }
   246  
   247  // emptyLayer tries to determine if the layer is empty from the history API, but may return a wrong result.
   248  // The non-empty layers will be compared to diffIDs later so that results can be validated.
   249  func emptyLayer(history dimage.HistoryResponseItem) bool {
   250  	if history.Size != 0 {
   251  		return false
   252  	}
   253  	createdBy := strings.TrimSpace(strings.TrimLeft(history.CreatedBy, "/bin/sh -c #(nop)"))
   254  	// This logic is taken from https://github.com/moby/buildkit/blob/2942d13ff489a2a49082c99e6104517e357e53ad/frontend/dockerfile/dockerfile2llb/convert.go
   255  	if strings.HasPrefix(createdBy, "ENV") ||
   256  		strings.HasPrefix(createdBy, "MAINTAINER") ||
   257  		strings.HasPrefix(createdBy, "LABEL") ||
   258  		strings.HasPrefix(createdBy, "CMD") ||
   259  		strings.HasPrefix(createdBy, "ENTRYPOINT") ||
   260  		strings.HasPrefix(createdBy, "HEALTHCHECK") ||
   261  		strings.HasPrefix(createdBy, "EXPOSE") ||
   262  		strings.HasPrefix(createdBy, "USER") ||
   263  		strings.HasPrefix(createdBy, "VOLUME") ||
   264  		strings.HasPrefix(createdBy, "STOPSIGNAL") ||
   265  		strings.HasPrefix(createdBy, "SHELL") ||
   266  		strings.HasPrefix(createdBy, "ARG") {
   267  		return true
   268  	}
   269  	// buildkit layers with "WORKDIR /" command are empty,
   270  	if strings.HasPrefix(history.Comment, "buildkit.dockerfile") {
   271  		if createdBy == "WORKDIR /" {
   272  			return true
   273  		}
   274  	} else if strings.HasPrefix(createdBy, "WORKDIR") { // layers build with docker and podman, WORKDIR command is always empty layer.
   275  		return true
   276  	}
   277  	// The following instructions could reach here:
   278  	//     - "ADD"
   279  	//     - "COPY"
   280  	//     - "RUN"
   281  	//         - "RUN" may not include even 'RUN' prefix
   282  	//            e.g. '/bin/sh -c mkdir test '
   283  	//     - "WORKDIR", which doesn't meet the above conditions
   284  	return false
   285  }