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

     1  package snapsource
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/spf13/afero"
    13  
    14  	stereoFile "github.com/anchore/stereoscope/pkg/file"
    15  	"github.com/anchore/syft/internal/bus"
    16  	intFile "github.com/anchore/syft/internal/file"
    17  	"github.com/anchore/syft/internal/log"
    18  	"github.com/anchore/syft/syft/event/monitor"
    19  	"github.com/anchore/syft/syft/file"
    20  )
    21  
    22  type snapFile struct {
    23  	Path     string
    24  	Digests  []file.Digest
    25  	MimeType string
    26  	Cleanup  func() error
    27  }
    28  
    29  type remoteSnap struct {
    30  	snapIdentity
    31  	URL string
    32  }
    33  
    34  type snapIdentity struct {
    35  	Name         string
    36  	Channel      string
    37  	Architecture string
    38  }
    39  
    40  func (s snapIdentity) String() string {
    41  	parts := []string{s.Name}
    42  
    43  	if s.Channel != "" {
    44  		parts = append(parts, fmt.Sprintf("@%s", s.Channel))
    45  	}
    46  
    47  	if s.Architecture != "" {
    48  		parts = append(parts, fmt.Sprintf(" (%s)", s.Architecture))
    49  	}
    50  
    51  	return strings.Join(parts, "")
    52  }
    53  
    54  func getRemoteSnapFile(ctx context.Context, fs afero.Fs, getter intFile.Getter, cfg Config) (*snapFile, error) {
    55  	if cfg.Request == "" {
    56  		return nil, fmt.Errorf("invalid request: %q", cfg.Request)
    57  	}
    58  
    59  	var architecture string
    60  	if cfg.Platform != nil {
    61  		architecture = cfg.Platform.Architecture
    62  	}
    63  
    64  	info, err := resolveRemoteSnap(cfg.Request, architecture)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	return newSnapFileFromRemote(ctx, fs, cfg, getter, info)
    70  }
    71  
    72  func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (*snapFile, error) {
    73  	t, err := afero.TempDir(fs, "", "syft-snap-")
    74  	if err != nil {
    75  		return nil, fmt.Errorf("failed to create temp directory: %w", err)
    76  	}
    77  
    78  	snapFilePath := path.Join(t, path.Base(info.URL))
    79  	err = downloadSnap(getter, info, snapFilePath)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("failed to download snap file: %w", err)
    82  	}
    83  
    84  	closer := func() error {
    85  		return fs.RemoveAll(t)
    86  	}
    87  
    88  	mimeType, digests, err := getSnapFileInfo(ctx, fs, snapFilePath, cfg.DigestAlgorithms)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	return &snapFile{
    94  		Path:     snapFilePath,
    95  		Digests:  digests,
    96  		MimeType: mimeType,
    97  		Cleanup:  closer,
    98  	}, nil
    99  }
   100  
   101  func newSnapFromFile(ctx context.Context, fs afero.Fs, cfg Config) (*snapFile, error) {
   102  	var architecture string
   103  	if cfg.Platform != nil {
   104  		architecture = cfg.Platform.Architecture
   105  	}
   106  
   107  	if architecture != "" {
   108  		return nil, fmt.Errorf("architecture cannot be specified for local snap files: %q", cfg.Request)
   109  	}
   110  
   111  	absPath, err := filepath.Abs(cfg.Request)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("unable to get absolute path of snap: %w", err)
   114  	}
   115  
   116  	mimeType, digests, err := getSnapFileInfo(ctx, fs, absPath, cfg.DigestAlgorithms)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	return &snapFile{
   122  		Path:     absPath,
   123  		Digests:  digests,
   124  		MimeType: mimeType,
   125  		// note: we have no closer since this is the user's file (never delete it)
   126  	}, nil
   127  }
   128  
   129  func getSnapFileInfo(ctx context.Context, fs afero.Fs, path string, hashes []crypto.Hash) (string, []file.Digest, error) {
   130  	fileMeta, err := fs.Stat(path)
   131  	if err != nil {
   132  		return "", nil, fmt.Errorf("unable to stat path=%q: %w", path, err)
   133  	}
   134  
   135  	if fileMeta.IsDir() {
   136  		return "", nil, fmt.Errorf("given path is a directory, not a snap file: %q", path)
   137  	}
   138  
   139  	fh, err := fs.Open(path)
   140  	if err != nil {
   141  		return "", nil, fmt.Errorf("unable to open file=%q: %w", path, err)
   142  	}
   143  	defer fh.Close()
   144  
   145  	mimeType := stereoFile.MIMEType(fh)
   146  	if !isSquashFSFile(mimeType, path) {
   147  		return "", nil, fmt.Errorf("not a valid squashfs/snap file: %q (mime-type=%q)", path, mimeType)
   148  	}
   149  
   150  	var digests []file.Digest
   151  	if len(hashes) > 0 {
   152  		if _, err := fh.Seek(0, 0); err != nil {
   153  			return "", nil, fmt.Errorf("unable to reset file position: %w", err)
   154  		}
   155  
   156  		digests, err = intFile.NewDigestsFromFile(ctx, fh, hashes)
   157  		if err != nil {
   158  			return "", nil, fmt.Errorf("unable to calculate digests for file=%q: %w", path, err)
   159  		}
   160  	}
   161  
   162  	return mimeType, digests, nil
   163  }
   164  
   165  // resolveRemoteSnap parses a snap request and returns the appropriate path or URL
   166  // The request can be:
   167  // - A snap name (e.g., "etcd")
   168  // - A snap name with channel (e.g., "etcd@beta" or "etcd@2.3/stable")
   169  func resolveRemoteSnap(request, architecture string) (*remoteSnap, error) {
   170  	if architecture == "" {
   171  		architecture = defaultArchitecture
   172  	}
   173  
   174  	snapName, channel := parseSnapRequest(request)
   175  
   176  	id := snapIdentity{
   177  		Name:         snapName,
   178  		Channel:      channel,
   179  		Architecture: architecture,
   180  	}
   181  
   182  	client := newSnapcraftClient()
   183  
   184  	downloadURL, err := client.GetSnapDownloadURL(id)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	log.WithFields("url", downloadURL, "name", snapName, "channel", channel, "architecture", architecture).Debugf("snap resolved")
   190  
   191  	return &remoteSnap{
   192  		snapIdentity: id,
   193  		URL:          downloadURL,
   194  	}, nil
   195  }
   196  
   197  // parseSnapRequest parses a snap request into name and channel
   198  // Examples:
   199  // - "etcd" -> name="etcd", channel="stable" (default)
   200  // - "etcd@beta" -> name="etcd", channel="beta"
   201  // - "etcd@2.3/stable" -> name="etcd", channel="2.3/stable"
   202  func parseSnapRequest(request string) (name, channel string) {
   203  	parts := strings.SplitN(request, "@", 2)
   204  	name = parts[0]
   205  
   206  	if len(parts) == 2 {
   207  		channel = parts[1]
   208  	}
   209  
   210  	if channel == "" {
   211  		channel = defaultChannel
   212  	}
   213  
   214  	return name, channel
   215  }
   216  
   217  func downloadSnap(getter intFile.Getter, info *remoteSnap, dest string) error {
   218  	log.WithFields("url", info.URL, "destination", dest).Debug("downloading snap file")
   219  
   220  	prog := bus.StartPullSourceTask(monitor.GenericTask{
   221  		Title: monitor.Title{
   222  			Default:      "Download snap",
   223  			WhileRunning: "Downloading snap",
   224  			OnSuccess:    "Downloaded snap",
   225  		},
   226  		HideOnSuccess:      false,
   227  		HideStageOnSuccess: true,
   228  		ID:                 "",
   229  		ParentID:           "",
   230  		Context:            info.String(),
   231  	}, -1, "")
   232  
   233  	if err := getter.GetFile(dest, info.URL, prog.Manual); err != nil {
   234  		prog.SetError(err)
   235  		return fmt.Errorf("failed to download snap file at %q: %w", info.URL, err)
   236  	}
   237  
   238  	prog.SetCompleted()
   239  	return nil
   240  }
   241  
   242  // fileExists checks if a file exists and is not a directory
   243  func fileExists(fs afero.Fs, path string) bool {
   244  	info, err := fs.Stat(path)
   245  	if os.IsNotExist(err) {
   246  		return false
   247  	}
   248  	return err == nil && !info.IsDir()
   249  }