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

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/google/go-containerregistry/pkg/name"
     9  	v1 "github.com/google/go-containerregistry/pkg/v1"
    10  	multierror "github.com/hashicorp/go-multierror"
    11  	"golang.org/x/xerrors"
    12  
    13  	"github.com/devseccon/trivy/pkg/fanal/types"
    14  	"github.com/devseccon/trivy/pkg/log"
    15  )
    16  
    17  type imageSourceFunc func(ctx context.Context, imageName string, ref name.Reference, option types.ImageOptions) (types.Image, func(), error)
    18  
    19  var imageSourceFuncs = map[types.ImageSource]imageSourceFunc{
    20  	types.ContainerdImageSource: tryContainerdDaemon,
    21  	types.PodmanImageSource:     tryPodmanDaemon,
    22  	types.DockerImageSource:     tryDockerDaemon,
    23  	types.RemoteImageSource:     tryRemote,
    24  }
    25  
    26  func NewContainerImage(ctx context.Context, imageName string, opt types.ImageOptions) (types.Image, func(), error) {
    27  	if len(opt.ImageSources) == 0 {
    28  		return nil, func() {}, xerrors.New("no image sources supplied")
    29  	}
    30  
    31  	var errs error
    32  	var nameOpts []name.Option
    33  	if opt.RegistryOptions.Insecure {
    34  		nameOpts = append(nameOpts, name.Insecure)
    35  	}
    36  
    37  	ref, err := name.ParseReference(imageName, nameOpts...)
    38  	if err != nil {
    39  		return nil, func() {}, xerrors.Errorf("failed to parse the image name: %w", err)
    40  	}
    41  
    42  	for _, src := range opt.ImageSources {
    43  		trySrc, ok := imageSourceFuncs[src]
    44  		if !ok {
    45  			log.Logger.Warnf("Unknown image source: '%s'", src)
    46  			continue
    47  		}
    48  
    49  		img, cleanup, err := trySrc(ctx, imageName, ref, opt)
    50  		if err == nil {
    51  			// Return v1.Image if the image is found
    52  			return img, cleanup, nil
    53  		}
    54  		err = multierror.Prefix(err, fmt.Sprintf("%s error:", src))
    55  		errs = multierror.Append(errs, err)
    56  	}
    57  
    58  	return nil, func() {}, errs
    59  }
    60  
    61  func ID(img v1.Image) (string, error) {
    62  	h, err := img.ConfigName()
    63  	if err != nil {
    64  		return "", xerrors.Errorf("unable to get the image ID: %w", err)
    65  	}
    66  	return h.String(), nil
    67  }
    68  
    69  func LayerIDs(img v1.Image) ([]string, error) {
    70  	conf, err := img.ConfigFile()
    71  	if err != nil {
    72  		return nil, xerrors.Errorf("unable to get the config file: %w", err)
    73  	}
    74  
    75  	var layerIDs []string
    76  	for _, d := range conf.RootFS.DiffIDs {
    77  		layerIDs = append(layerIDs, d.String())
    78  	}
    79  	return layerIDs, nil
    80  }
    81  
    82  // GuessBaseImageIndex tries to guess index of base layer
    83  //
    84  // e.g. In the following example, we should detect layers in debian:8.
    85  //
    86  //	FROM debian:8
    87  //	RUN apt-get update
    88  //	COPY mysecret /
    89  //	ENTRYPOINT ["entrypoint.sh"]
    90  //	CMD ["somecmd"]
    91  //
    92  // debian:8 may be like
    93  //
    94  //	ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in /
    95  //	CMD ["/bin/sh"]
    96  //
    97  // In total, it would be like:
    98  //
    99  //	ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in /
   100  //	CMD ["/bin/sh"]              # empty layer (detected)
   101  //	RUN apt-get update
   102  //	COPY mysecret /
   103  //	ENTRYPOINT ["entrypoint.sh"] # empty layer (skipped)
   104  //	CMD ["somecmd"]              # empty layer (skipped)
   105  //
   106  // This method tries to detect CMD in the second line and assume the first line is a base layer.
   107  //  1. Iterate histories from the bottom.
   108  //  2. Skip all the empty layers at the bottom. In the above example, "entrypoint.sh" and "somecmd" will be skipped
   109  //  3. If it finds CMD, it assumes that it is the end of base layers.
   110  //  4. It gets all the layers as base layers above the CMD found in #3.
   111  func GuessBaseImageIndex(histories []v1.History) int {
   112  	baseImageIndex := -1
   113  	var foundNonEmpty bool
   114  	for i := len(histories) - 1; i >= 0; i-- {
   115  		h := histories[i]
   116  
   117  		// Skip the last CMD, ENTRYPOINT, etc.
   118  		if !foundNonEmpty {
   119  			if h.EmptyLayer {
   120  				continue
   121  			}
   122  			foundNonEmpty = true
   123  		}
   124  
   125  		if !h.EmptyLayer {
   126  			continue
   127  		}
   128  
   129  		// Detect CMD instruction in base image
   130  		if strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop)  CMD") ||
   131  			strings.HasPrefix(h.CreatedBy, "CMD") { // BuildKit
   132  			baseImageIndex = i
   133  			break
   134  		}
   135  	}
   136  	return baseImageIndex
   137  }