github.com/anchore/syft@v1.38.2/syft/source/stereoscopesource/image_source.go (about)

     1  package stereoscopesource
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/bmatcuk/doublestar/v4"
     7  	"github.com/distribution/reference"
     8  	"github.com/opencontainers/go-digest"
     9  
    10  	"github.com/anchore/stereoscope/pkg/image"
    11  	"github.com/anchore/syft/internal/log"
    12  	"github.com/anchore/syft/syft/artifact"
    13  	"github.com/anchore/syft/syft/file"
    14  	"github.com/anchore/syft/syft/internal/fileresolver"
    15  	"github.com/anchore/syft/syft/source"
    16  	"github.com/anchore/syft/syft/source/internal"
    17  )
    18  
    19  var _ source.Source = (*stereoscopeImageSource)(nil)
    20  
    21  type ImageConfig struct {
    22  	Reference       string
    23  	Platform        *image.Platform
    24  	RegistryOptions *image.RegistryOptions
    25  	Exclude         source.ExcludeConfig
    26  	Alias           source.Alias
    27  }
    28  
    29  type stereoscopeImageSource struct {
    30  	id       artifact.ID
    31  	config   ImageConfig
    32  	image    *image.Image
    33  	metadata source.ImageMetadata
    34  }
    35  
    36  func New(img *image.Image, cfg ImageConfig) source.Source {
    37  	metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
    38  	return &stereoscopeImageSource{
    39  		id:       deriveIDFromStereoscopeImage(cfg.Alias, metadata),
    40  		config:   cfg,
    41  		image:    img,
    42  		metadata: metadata,
    43  	}
    44  }
    45  
    46  func (s stereoscopeImageSource) ID() artifact.ID {
    47  	return s.id
    48  }
    49  
    50  func (s stereoscopeImageSource) Describe() source.Description {
    51  	a := s.config.Alias
    52  
    53  	name := a.Name
    54  	supplier := a.Supplier
    55  	nameIfUnset := func(n string) {
    56  		if name != "" {
    57  			return
    58  		}
    59  		name = n
    60  	}
    61  
    62  	version := a.Version
    63  	versionIfUnset := func(v string) {
    64  		if version != "" && version != "latest" {
    65  			return
    66  		}
    67  		version = v
    68  	}
    69  
    70  	ref, err := reference.Parse(s.metadata.UserInput)
    71  	if err != nil {
    72  		log.Debugf("unable to parse image ref: %s", s.config.Reference)
    73  	} else {
    74  		if ref, ok := ref.(reference.Named); ok {
    75  			nameIfUnset(ref.Name())
    76  		}
    77  
    78  		if ref, ok := ref.(reference.NamedTagged); ok {
    79  			versionIfUnset(ref.Tag())
    80  		}
    81  
    82  		if ref, ok := ref.(reference.Digested); ok {
    83  			versionIfUnset(ref.Digest().String())
    84  		}
    85  	}
    86  
    87  	nameIfUnset(s.metadata.UserInput)
    88  	versionIfUnset(s.metadata.ManifestDigest)
    89  
    90  	return source.Description{
    91  		ID:       string(s.id),
    92  		Name:     name,
    93  		Version:  version,
    94  		Supplier: supplier,
    95  		Metadata: s.metadata,
    96  	}
    97  }
    98  
    99  func (s stereoscopeImageSource) FileResolver(scope source.Scope) (file.Resolver, error) {
   100  	var res file.Resolver
   101  	var err error
   102  
   103  	switch scope {
   104  	case source.SquashedScope:
   105  		res, err = fileresolver.NewFromContainerImageSquash(s.image)
   106  	case source.AllLayersScope:
   107  		res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
   108  	case source.DeepSquashedScope:
   109  		res, err = fileresolver.NewFromContainerImageDeepSquash(s.image)
   110  	default:
   111  		return nil, fmt.Errorf("bad image scope provided: %+v", scope)
   112  	}
   113  
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// image tree contains all paths, so we filter out the excluded entries afterward
   119  	if len(s.config.Exclude.Paths) > 0 {
   120  		res = fileresolver.NewExcludingDecorator(res, getImageExclusionFunction(s.config.Exclude.Paths))
   121  	}
   122  
   123  	return res, nil
   124  }
   125  
   126  func (s stereoscopeImageSource) Close() error {
   127  	if s.image == nil {
   128  		return nil
   129  	}
   130  	return s.image.Cleanup()
   131  }
   132  
   133  func imageMetadataFromStereoscopeImage(img *image.Image, reference string) source.ImageMetadata {
   134  	tags := make([]string, len(img.Metadata.Tags))
   135  	for idx, tag := range img.Metadata.Tags {
   136  		tags[idx] = tag.String()
   137  	}
   138  
   139  	layers := make([]source.LayerMetadata, len(img.Layers))
   140  	for idx, l := range img.Layers {
   141  		layers[idx] = source.LayerMetadata{
   142  			MediaType: string(l.Metadata.MediaType),
   143  			Digest:    l.Metadata.Digest,
   144  			Size:      l.Metadata.Size,
   145  		}
   146  	}
   147  
   148  	return source.ImageMetadata{
   149  		ID:             img.Metadata.ID,
   150  		UserInput:      reference,
   151  		ManifestDigest: img.Metadata.ManifestDigest,
   152  		Size:           img.Metadata.Size,
   153  		MediaType:      string(img.Metadata.MediaType),
   154  		Tags:           tags,
   155  		Layers:         layers,
   156  		RawConfig:      img.Metadata.RawConfig,
   157  		RawManifest:    img.Metadata.RawManifest,
   158  		RepoDigests:    img.Metadata.RepoDigests,
   159  		Architecture:   img.Metadata.Architecture,
   160  		Variant:        img.Metadata.Variant,
   161  		OS:             img.Metadata.OS,
   162  		Labels:         img.Metadata.Config.Config.Labels,
   163  	}
   164  }
   165  
   166  // deriveIDFromStereoscopeImage derives an artifact ID from the given image metadata. The order of data precedence is:
   167  //  1. prefer a digest of the raw container image manifest
   168  //  2. if no manifest digest is available, calculate a chain ID from the image layer metadata
   169  //  3. if no layer metadata is available, use the user input string
   170  //
   171  // in all cases, if an alias is provided, it is additionally considered in the ID calculation. This allows for the
   172  // same image to be scanned multiple times with different aliases and be considered logically different.
   173  func deriveIDFromStereoscopeImage(alias source.Alias, metadata source.ImageMetadata) artifact.ID {
   174  	var input string
   175  
   176  	if len(metadata.RawManifest) > 0 {
   177  		input = digest.Canonical.FromBytes(metadata.RawManifest).String()
   178  	} else {
   179  		// calculate chain ID for image sources where manifestDigest is not available
   180  		// https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid
   181  		input = calculateChainID(metadata.Layers)
   182  		if input == "" {
   183  			// TODO what happens here if image has no layers?
   184  			// is this case possible?
   185  			input = digest.Canonical.FromString(metadata.UserInput).String()
   186  		}
   187  	}
   188  
   189  	if !alias.IsEmpty() {
   190  		// if the user provided an alias, we want to consider that in the artifact ID. This way if the user
   191  		// scans the same item but is considered to be logically different, then ID will express that.
   192  		aliasStr := fmt.Sprintf(":%s@%s", alias.Name, alias.Version)
   193  		input = digest.Canonical.FromString(input + aliasStr).String()
   194  	}
   195  
   196  	return internal.ArtifactIDFromDigest(input)
   197  }
   198  
   199  func calculateChainID(lm []source.LayerMetadata) string {
   200  	if len(lm) < 1 {
   201  		return ""
   202  	}
   203  
   204  	// DiffID(L0) = digest of layer 0
   205  	// https://github.com/anchore/stereoscope/blob/1b1b744a919964f38d14e1416fb3f25221b761ce/pkg/image/layer_metadata.go#L19-L32
   206  	chainID := lm[0].Digest
   207  	id := chain(chainID, lm[1:])
   208  
   209  	return id
   210  }
   211  
   212  func chain(chainID string, layers []source.LayerMetadata) string {
   213  	if len(layers) < 1 {
   214  		return chainID
   215  	}
   216  
   217  	chainID = digest.Canonical.FromString(layers[0].Digest + " " + chainID).String()
   218  	return chain(chainID, layers[1:])
   219  }
   220  
   221  func getImageExclusionFunction(exclusions []string) func(string) bool {
   222  	if len(exclusions) == 0 {
   223  		return nil
   224  	}
   225  	// add subpath exclusions
   226  	for _, exclusion := range exclusions {
   227  		exclusions = append(exclusions, exclusion+"/**")
   228  	}
   229  	return func(path string) bool {
   230  		for _, exclusion := range exclusions {
   231  			matches, err := doublestar.Match(exclusion, path)
   232  			if err != nil {
   233  				return false
   234  			}
   235  			if matches {
   236  				return true
   237  			}
   238  		}
   239  		return false
   240  	}
   241  }