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 }