github.com/anchore/syft@v1.38.2/syft/source/snapsource/snap_source.go (about)

     1  package snapsource
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/OneOfOne/xxhash"
    14  	diskFile "github.com/diskfs/go-diskfs/backend/file"
    15  	"github.com/diskfs/go-diskfs/filesystem"
    16  	"github.com/diskfs/go-diskfs/filesystem/squashfs"
    17  	"github.com/hashicorp/go-cleanhttp"
    18  	"github.com/opencontainers/go-digest"
    19  	"github.com/spf13/afero"
    20  
    21  	"github.com/anchore/clio"
    22  	"github.com/anchore/go-homedir"
    23  	stereoFile "github.com/anchore/stereoscope/pkg/file"
    24  	"github.com/anchore/stereoscope/pkg/filetree"
    25  	"github.com/anchore/stereoscope/pkg/image"
    26  	"github.com/anchore/syft/internal/bus"
    27  	intFile "github.com/anchore/syft/internal/file"
    28  	"github.com/anchore/syft/internal/log"
    29  	"github.com/anchore/syft/syft/artifact"
    30  	"github.com/anchore/syft/syft/event/monitor"
    31  	"github.com/anchore/syft/syft/file"
    32  	"github.com/anchore/syft/syft/internal/fileresolver"
    33  	"github.com/anchore/syft/syft/source"
    34  	"github.com/anchore/syft/syft/source/internal"
    35  )
    36  
    37  var _ source.Source = (*snapSource)(nil)
    38  
    39  type Config struct {
    40  	ID clio.Identification
    41  
    42  	Request          string
    43  	Platform         *image.Platform
    44  	Exclude          source.ExcludeConfig
    45  	DigestAlgorithms []crypto.Hash
    46  	Alias            source.Alias
    47  
    48  	fs afero.Fs
    49  }
    50  
    51  type snapSource struct {
    52  	id               artifact.ID
    53  	config           Config
    54  	resolver         file.Resolver
    55  	mutex            *sync.Mutex
    56  	manifest         snapManifest
    57  	digests          []file.Digest
    58  	fs               filesystem.FileSystem
    59  	squashfsPath     string
    60  	squashFileCloser func() error
    61  	closer           func() error
    62  }
    63  
    64  func NewFromLocal(cfg Config) (source.Source, error) {
    65  	f, err := getLocalSnapFile(&cfg)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return newFromPath(cfg, f)
    70  }
    71  
    72  func getLocalSnapFile(cfg *Config) (*snapFile, error) {
    73  	expandedPath, err := homedir.Expand(cfg.Request)
    74  	if err != nil {
    75  		return nil, fmt.Errorf("unable to expand path %q: %w", cfg.Request, err)
    76  	}
    77  	cfg.Request = filepath.Clean(expandedPath)
    78  
    79  	if cfg.fs == nil {
    80  		cfg.fs = afero.NewOsFs()
    81  	}
    82  
    83  	if !fileExists(cfg.fs, cfg.Request) {
    84  		return nil, fmt.Errorf("snap file %q does not exist", cfg.Request)
    85  	}
    86  
    87  	log.WithFields("path", cfg.Request).Debug("snap is a local file")
    88  
    89  	return newSnapFromFile(context.Background(), cfg.fs, *cfg)
    90  }
    91  
    92  func NewFromRemote(cfg Config) (source.Source, error) {
    93  	expandedPath, err := homedir.Expand(cfg.Request)
    94  	if err != nil {
    95  		return nil, fmt.Errorf("unable to expand path %q: %w", cfg.Request, err)
    96  	}
    97  	cfg.Request = filepath.Clean(expandedPath)
    98  
    99  	if cfg.fs == nil {
   100  		cfg.fs = afero.NewOsFs()
   101  	}
   102  
   103  	client := intFile.NewGetter(cfg.ID, cleanhttp.DefaultClient())
   104  	f, err := getRemoteSnapFile(context.Background(), cfg.fs, client, cfg)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	return newFromPath(cfg, f)
   110  }
   111  
   112  func newFromPath(cfg Config, f *snapFile) (source.Source, error) {
   113  	s := &snapSource{
   114  		id:           deriveID(cfg.Request, cfg.Alias.Name, cfg.Alias.Version, f.Digests),
   115  		config:       cfg,
   116  		mutex:        &sync.Mutex{},
   117  		digests:      f.Digests,
   118  		squashfsPath: f.Path,
   119  		closer:       f.Cleanup,
   120  	}
   121  
   122  	return s, s.extractManifest()
   123  }
   124  
   125  func (s *snapSource) extractManifest() error {
   126  	r, err := s.FileResolver(source.SquashedScope)
   127  	if err != nil {
   128  		return fmt.Errorf("unable to create snap file resolver: %w", err)
   129  	}
   130  
   131  	manifest, err := parseManifest(r)
   132  	if err != nil {
   133  		return fmt.Errorf("unable to parse snap manifest file: %w", err)
   134  	}
   135  
   136  	if manifest != nil {
   137  		s.manifest = *manifest
   138  	}
   139  	return nil
   140  }
   141  
   142  func (s snapSource) ID() artifact.ID {
   143  	return s.id
   144  }
   145  
   146  func (s snapSource) NameVersion() (string, string) {
   147  	name := s.manifest.Name
   148  	version := s.manifest.Version
   149  	if !s.config.Alias.IsEmpty() {
   150  		a := s.config.Alias
   151  		if a.Name != "" {
   152  			name = a.Name
   153  		}
   154  
   155  		if a.Version != "" {
   156  			version = a.Version
   157  		}
   158  	}
   159  	return name, version
   160  }
   161  
   162  func (s snapSource) Describe() source.Description {
   163  	name, version := s.NameVersion()
   164  	return source.Description{
   165  		ID:      string(s.id),
   166  		Name:    name,
   167  		Version: version,
   168  		Metadata: source.SnapMetadata{
   169  			Summary:       s.manifest.Summary,
   170  			Base:          s.manifest.Base,
   171  			Grade:         s.manifest.Grade,
   172  			Confinement:   s.manifest.Confinement,
   173  			Architectures: s.manifest.Architectures,
   174  			Digests:       s.digests,
   175  		},
   176  	}
   177  }
   178  
   179  func (s *snapSource) Close() error {
   180  	if s.squashFileCloser != nil {
   181  		if err := s.squashFileCloser(); err != nil {
   182  			return fmt.Errorf("unable to close snap resolver: %w", err)
   183  		}
   184  		s.squashFileCloser = nil
   185  	}
   186  	s.resolver = nil
   187  	if s.fs != nil {
   188  		if err := s.fs.Close(); err != nil {
   189  			return fmt.Errorf("unable to close snap squashfs: %w", err)
   190  		}
   191  	}
   192  	if s.closer != nil {
   193  		if err := s.closer(); err != nil {
   194  			return fmt.Errorf("unable to close snap source: %w", err)
   195  		}
   196  	}
   197  	return nil
   198  }
   199  
   200  func (s *snapSource) FileResolver(_ source.Scope) (file.Resolver, error) {
   201  	s.mutex.Lock()
   202  	defer s.mutex.Unlock()
   203  
   204  	if s.resolver != nil {
   205  		return s.resolver, nil
   206  	}
   207  
   208  	log.Debugf("parsing squashfs file: %s", s.squashfsPath)
   209  
   210  	f, err := os.Open(s.squashfsPath)
   211  	if err != nil {
   212  		return nil, fmt.Errorf("unable to open squashfs file: %w", err)
   213  	}
   214  
   215  	s.squashFileCloser = func() error {
   216  		if err := f.Close(); err != nil {
   217  			return fmt.Errorf("unable to close squashfs file: %w", err)
   218  		}
   219  		return nil
   220  	}
   221  
   222  	fileMeta, err := f.Stat()
   223  	if err != nil {
   224  		return nil, fmt.Errorf("unable to stat squashfs file: %w", err)
   225  	}
   226  
   227  	size := fileMeta.Size()
   228  
   229  	fileCatalog := image.NewFileCatalog()
   230  
   231  	prog := bus.StartIndexingFiles(filepath.Base(s.squashfsPath))
   232  
   233  	b := diskFile.New(f, true)
   234  	fs, err := squashfs.Read(b, fileMeta.Size(), 0, 0)
   235  	if err != nil {
   236  		err := fmt.Errorf("unable to open squashfs file: %w", err)
   237  		prog.SetError(err)
   238  		return nil, err
   239  	}
   240  
   241  	tree := filetree.New()
   242  	if err := intFile.WalkDiskDir(fs, "/", squashfsVisitor(tree, fileCatalog, &size, prog)); err != nil {
   243  		err := fmt.Errorf("failed to walk squashfs file=%q: %w", s.squashfsPath, err)
   244  		prog.SetError(err)
   245  		return nil, err
   246  	}
   247  
   248  	prog.SetCompleted()
   249  
   250  	s.resolver = &fileresolver.FiletreeResolver{
   251  		Chroot:        fileresolver.ChrootContext{},
   252  		Tree:          tree,
   253  		Index:         fileCatalog.Index,
   254  		SearchContext: filetree.NewSearchContext(tree, fileCatalog.Index),
   255  		Opener: func(ref stereoFile.Reference) (io.ReadCloser, error) {
   256  			return fileCatalog.Open(ref)
   257  		},
   258  	}
   259  
   260  	s.fs = fs
   261  
   262  	return s.resolver, nil
   263  }
   264  
   265  type linker interface {
   266  	Readlink() (string, error)
   267  }
   268  
   269  func squashfsVisitor(ft filetree.Writer, fileCatalog *image.FileCatalog, size *int64, prog *monitor.TaskProgress) intFile.WalkDiskDirFunc {
   270  	builder := filetree.NewBuilder(ft, fileCatalog.Index)
   271  
   272  	return func(fsys filesystem.FileSystem, path string, d os.FileInfo, walkErr error) error {
   273  		if walkErr != nil {
   274  			log.WithFields("error", walkErr, "path", path).Trace("unable to walk squash file path")
   275  			return walkErr
   276  		}
   277  
   278  		prog.AtomicStage.Set(path)
   279  
   280  		var f filesystem.File
   281  		var mimeType string
   282  		var err error
   283  
   284  		if !d.IsDir() {
   285  			f, err = fsys.OpenFile(path, os.O_RDONLY)
   286  			if err != nil {
   287  				log.WithFields("error", err, "path", path).Trace("unable to open squash file path")
   288  			} else {
   289  				defer f.Close()
   290  				mimeType = stereoFile.MIMEType(f)
   291  			}
   292  		}
   293  
   294  		var ty stereoFile.Type
   295  		var linkPath string
   296  		switch {
   297  		case d.IsDir():
   298  			// in some implementations, the mode does not indicate a directory, so we check the FileInfo type explicitly
   299  			ty = stereoFile.TypeDirectory
   300  		default:
   301  			ty = stereoFile.TypeFromMode(d.Mode())
   302  			if ty == stereoFile.TypeSymLink && f != nil {
   303  				if l, ok := f.(linker); ok {
   304  					linkPath, _ = l.Readlink()
   305  				}
   306  			}
   307  		}
   308  
   309  		metadata := stereoFile.Metadata{
   310  			FileInfo:        d,
   311  			Path:            path,
   312  			LinkDestination: linkPath,
   313  			Type:            ty,
   314  			MIMEType:        mimeType,
   315  		}
   316  
   317  		fileReference, err := builder.Add(metadata)
   318  		if err != nil {
   319  			return err
   320  		}
   321  
   322  		if fileReference == nil {
   323  			return nil
   324  		}
   325  
   326  		if size != nil {
   327  			*(size) += metadata.Size()
   328  		}
   329  		fileCatalog.AssociateOpener(*fileReference, func() (io.ReadCloser, error) {
   330  			return fsys.OpenFile(path, os.O_RDONLY)
   331  		})
   332  
   333  		prog.Increment()
   334  		return nil
   335  	}
   336  }
   337  
   338  func isSquashFSFile(mimeType, path string) bool {
   339  	if mimeType == "application/vnd.squashfs" || mimeType == "application/x-squashfs" {
   340  		return true
   341  	}
   342  
   343  	ext := filepath.Ext(path)
   344  	return ext == ".snap" || ext == ".squashfs"
   345  }
   346  
   347  func deriveID(path, name, version string, digests []file.Digest) artifact.ID {
   348  	var xxhDigest string
   349  	for _, d := range digests {
   350  		if strings.ToLower(strings.ReplaceAll(d.Algorithm, "-", "")) == "xxh64" {
   351  			xxhDigest = d.Value
   352  			break
   353  		}
   354  	}
   355  
   356  	if xxhDigest == "" {
   357  		xxhDigest = digestOfFileContents(path)
   358  	}
   359  
   360  	info := fmt.Sprintf("%s:%s@%s", xxhDigest, name, version)
   361  	return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String())
   362  }
   363  
   364  // return the xxhash64 of the file contents, or the xxhash64 of the path if the file cannot be read
   365  func digestOfFileContents(path string) string {
   366  	f, err := os.Open(path)
   367  	if err != nil {
   368  		return digestOfReader(strings.NewReader(path))
   369  	}
   370  	defer f.Close()
   371  	return digestOfReader(f)
   372  }
   373  
   374  func digestOfReader(r io.Reader) string {
   375  	hasher := xxhash.New64()
   376  	_, _ = io.Copy(hasher, r)
   377  	return fmt.Sprintf("%x", hasher.Sum(nil))
   378  }