github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/snap/snap.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package snap extracts snap packages
    16  package snap
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"regexp"
    23  	"strings"
    24  
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/os/osrelease"
    29  	snapmeta "github.com/google/osv-scalibr/extractor/filesystem/os/snap/metadata"
    30  	"github.com/google/osv-scalibr/inventory"
    31  	"github.com/google/osv-scalibr/log"
    32  	"github.com/google/osv-scalibr/plugin"
    33  	"github.com/google/osv-scalibr/purl"
    34  	"github.com/google/osv-scalibr/stats"
    35  	"gopkg.in/yaml.v3"
    36  )
    37  
    38  const (
    39  	// Name is the unique name of this extractor.
    40  	Name = "os/snap"
    41  
    42  	// defaultMaxFileSizeBytes is the maximum file size an extractor will unmarshal.
    43  	// If Extract gets a bigger file, it will return an error.
    44  	defaultMaxFileSizeBytes = 1 * units.MiB
    45  )
    46  
    47  type snap struct {
    48  	Name          string   `yaml:"name"`
    49  	Version       string   `yaml:"version"`
    50  	Grade         string   `yaml:"grade"`
    51  	Type          string   `yaml:"type"`
    52  	Architectures []string `yaml:"architectures"`
    53  }
    54  
    55  // Config is the configuration for the Extractor.
    56  type Config struct {
    57  	// Stats is a stats collector for reporting metrics.
    58  	Stats stats.Collector
    59  	// MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If
    60  	// `FileRequired` gets a bigger file, it will return false,
    61  	MaxFileSizeBytes int64
    62  }
    63  
    64  // DefaultConfig returns the default configuration for the extractor.
    65  func DefaultConfig() Config {
    66  	return Config{
    67  		MaxFileSizeBytes: defaultMaxFileSizeBytes,
    68  		Stats:            nil,
    69  	}
    70  }
    71  
    72  // Extractor extracts snap apps.
    73  type Extractor struct {
    74  	stats            stats.Collector
    75  	maxFileSizeBytes int64
    76  }
    77  
    78  // New returns a SNAP extractor.
    79  //
    80  // For most use cases, initialize with:
    81  // ```
    82  // e := New(DefaultConfig())
    83  // ```
    84  func New(cfg Config) *Extractor {
    85  	return &Extractor{
    86  		stats:            cfg.Stats,
    87  		maxFileSizeBytes: cfg.MaxFileSizeBytes,
    88  	}
    89  }
    90  
    91  // NewDefault returns an extractor with the default config settings.
    92  func NewDefault() filesystem.Extractor { return New(DefaultConfig()) }
    93  
    94  // Name of the extractor.
    95  func (e Extractor) Name() string { return Name }
    96  
    97  // Version of the extractor.
    98  func (e Extractor) Version() int { return 0 }
    99  
   100  // Requirements of the extractor.
   101  func (e Extractor) Requirements() *plugin.Capabilities {
   102  	return &plugin.Capabilities{OS: plugin.OSLinux}
   103  }
   104  
   105  // the yaml file is found in snap/<app>/<revision>/meta/snap.yaml
   106  var filePathRegex = regexp.MustCompile(`^snap/[^/]*/[^/]*/meta/snap.yaml$`)
   107  
   108  // FileRequired returns true if the specified file matches snap.yaml file pattern.
   109  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
   110  	path := api.Path()
   111  	if !strings.HasSuffix(path, "snap.yaml") {
   112  		return false
   113  	}
   114  
   115  	if match := filePathRegex.FindString(path); match == "" {
   116  		return false
   117  	}
   118  
   119  	fileinfo, err := api.Stat()
   120  	if err != nil {
   121  		return false
   122  	}
   123  	if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes {
   124  		e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded)
   125  		return false
   126  	}
   127  
   128  	e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK)
   129  	return true
   130  }
   131  
   132  func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) {
   133  	if e.stats == nil {
   134  		return
   135  	}
   136  	e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{
   137  		Path:          path,
   138  		Result:        result,
   139  		FileSizeBytes: fileSizeBytes,
   140  	})
   141  }
   142  
   143  // Extract extracts snap info from snap.yaml file passed through the scan input.
   144  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
   145  	pkgs, err := e.extractFromInput(input)
   146  	if e.stats != nil {
   147  		var fileSizeBytes int64
   148  		if input.Info != nil {
   149  			fileSizeBytes = input.Info.Size()
   150  		}
   151  		e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{
   152  			Path:          input.Path,
   153  			Result:        filesystem.ExtractorErrorToFileExtractedResult(err),
   154  			FileSizeBytes: fileSizeBytes,
   155  		})
   156  	}
   157  	return inventory.Inventory{Packages: pkgs}, err
   158  }
   159  
   160  func (e Extractor) extractFromInput(input *filesystem.ScanInput) ([]*extractor.Package, error) {
   161  	m, err := osrelease.GetOSRelease(input.FS)
   162  	if err != nil {
   163  		log.Errorf("osrelease.ParseOsRelease(): %v", err)
   164  	}
   165  
   166  	snap := snap{}
   167  	dec := yaml.NewDecoder(input.Reader)
   168  	if err := dec.Decode(&snap); err != nil {
   169  		return nil, fmt.Errorf("failed to yaml decode: %w", err)
   170  	}
   171  
   172  	if snap.Name == "" {
   173  		return nil, errors.New("missing snap name")
   174  	}
   175  
   176  	if snap.Version == "" {
   177  		return nil, errors.New("missing snap version")
   178  	}
   179  
   180  	pkg := &extractor.Package{
   181  		Name:     snap.Name,
   182  		Version:  snap.Version,
   183  		PURLType: purl.TypeSnap,
   184  		Metadata: &snapmeta.Metadata{
   185  			Name:              snap.Name,
   186  			Version:           snap.Version,
   187  			Grade:             snap.Grade,
   188  			Type:              snap.Type,
   189  			Architectures:     snap.Architectures,
   190  			OSID:              m["ID"],
   191  			OSVersionCodename: m["VERSION_CODENAME"],
   192  			OSVersionID:       m["VERSION_ID"],
   193  		},
   194  		Locations: []string{input.Path},
   195  	}
   196  	return []*extractor.Package{pkg}, nil
   197  }