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 }