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