github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/flatpak/flatpak.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 flatpak extracts packages from flatpak metainfo files. 16 package flatpak 17 18 import ( 19 "context" 20 "encoding/xml" 21 "fmt" 22 "regexp" 23 "strings" 24 25 "github.com/google/osv-scalibr/extractor" 26 "github.com/google/osv-scalibr/extractor/filesystem" 27 flatpakmeta "github.com/google/osv-scalibr/extractor/filesystem/os/flatpak/metadata" 28 "github.com/google/osv-scalibr/extractor/filesystem/os/osrelease" 29 "github.com/google/osv-scalibr/inventory" 30 "github.com/google/osv-scalibr/log" 31 "github.com/google/osv-scalibr/plugin" 32 "github.com/google/osv-scalibr/purl" 33 "github.com/google/osv-scalibr/stats" 34 ) 35 36 const ( 37 // Name is the unique name of this extractor. 38 Name = "os/flatpak" 39 40 // defaultMaxFileSizeBytes is set to 0 since the xml file is per package and is usually small. 41 defaultMaxFileSizeBytes = 0 42 ) 43 44 // Metainfo is used to read the flatpak metainfo xml file. 45 type Metainfo struct { 46 ID string `xml:"id"` 47 Name []string `xml:"name"` 48 Developer string `xml:"developer_name"` 49 Releases struct { 50 Release []struct { 51 Version string `xml:"version,attr"` 52 ReleaseDate string `xml:"date,attr"` 53 } `xml:"release"` 54 } `xml:"releases"` 55 } 56 57 // Config is the configuration for the Extractor. 58 type Config struct { 59 // Stats is a stats collector for reporting metrics. 60 Stats stats.Collector 61 // MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If 62 // `FileRequired` gets a bigger file, it will return false, 63 MaxFileSizeBytes int64 64 } 65 66 // DefaultConfig returns the default configuration for the Flatpak extractor. 67 func DefaultConfig() Config { 68 return Config{ 69 Stats: nil, 70 MaxFileSizeBytes: defaultMaxFileSizeBytes, 71 } 72 } 73 74 // Extractor extracts Flatpak packages from *.metainfo.xml files. 75 type Extractor struct { 76 stats stats.Collector 77 maxFileSizeBytes int64 78 } 79 80 // New returns a Flatpak extractor. 81 // 82 // For most use cases, initialize with: 83 // ``` 84 // e := New(DefaultConfig()) 85 // ``` 86 func New(cfg Config) *Extractor { 87 return &Extractor{ 88 stats: cfg.Stats, 89 maxFileSizeBytes: cfg.MaxFileSizeBytes, 90 } 91 } 92 93 // NewDefault returns an extractor with the default config settings. 94 func NewDefault() filesystem.Extractor { return New(DefaultConfig()) } 95 96 // Config returns the configuration of the extractor. 97 func (e Extractor) Config() Config { 98 return Config{ 99 Stats: e.stats, 100 MaxFileSizeBytes: e.maxFileSizeBytes, 101 } 102 } 103 104 // Name of the extractor. 105 func (e Extractor) Name() string { return Name } 106 107 // Version of the extractor. 108 func (e Extractor) Version() int { return 0 } 109 110 // Requirements of the extractor. 111 func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } 112 113 // Should be metainfo.xml inside flatpak metainfo dir either globally or for a specific user. 114 var filePathRegex = regexp.MustCompile(`flatpak/app/.*/export/share/metainfo/.*metainfo.xml$`) 115 116 // FileRequired returns true if the specified file matches the metainfo xml file pattern. 117 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 118 path := api.Path() 119 if !strings.HasSuffix(path, "metainfo.xml") { 120 return false 121 } 122 123 if match := filePathRegex.FindString(path); match == "" { 124 return false 125 } 126 127 fileinfo, err := api.Stat() 128 if err != nil { 129 return false 130 } 131 if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes { 132 e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded) 133 return false 134 } 135 136 e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK) 137 return true 138 } 139 140 func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) { 141 if e.stats == nil { 142 return 143 } 144 e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{ 145 Path: path, 146 Result: result, 147 FileSizeBytes: fileSizeBytes, 148 }) 149 } 150 151 // Extract extracts packages from metainfo xml files passed through the scan input. 152 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 153 p, err := e.extractFromInput(input) 154 if e.stats != nil { 155 var fileSizeBytes int64 156 if input.Info != nil { 157 fileSizeBytes = input.Info.Size() 158 } 159 e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{ 160 Path: input.Path, 161 Result: filesystem.ExtractorErrorToFileExtractedResult(err), 162 FileSizeBytes: fileSizeBytes, 163 }) 164 } 165 if err != nil { 166 return inventory.Inventory{}, fmt.Errorf("flatpak.extract: %w", err) 167 } 168 if p == nil { 169 return inventory.Inventory{}, nil 170 } 171 return inventory.Inventory{Packages: []*extractor.Package{p}}, nil 172 } 173 174 func (e Extractor) extractFromInput(input *filesystem.ScanInput) (*extractor.Package, error) { 175 m, err := osrelease.GetOSRelease(input.FS) 176 if err != nil { 177 log.Errorf("osrelease.ParseOsRelease(): %v", err) 178 } 179 180 var f Metainfo 181 err = xml.NewDecoder(input.Reader).Decode(&f) 182 if err != nil { 183 return nil, fmt.Errorf("failed to xml decode: %w", err) 184 } 185 186 pkgName := "" 187 if len(f.Name) > 0 { 188 pkgName = f.Name[0] 189 } 190 191 pkgVersion := "" 192 if len(f.Releases.Release) > 0 { 193 pkgVersion = f.Releases.Release[0].Version // We only want the latest version. 194 } 195 if pkgVersion == "" { 196 return nil, fmt.Errorf("PackageVersion: %v does not exist", pkgVersion) 197 } 198 199 p := &extractor.Package{ 200 Name: f.ID, 201 Version: pkgVersion, 202 PURLType: purl.TypeFlatpak, 203 Metadata: &flatpakmeta.Metadata{ 204 PackageName: pkgName, 205 PackageID: f.ID, 206 PackageVersion: pkgVersion, 207 ReleaseDate: f.Releases.Release[0].ReleaseDate, 208 OSName: m["NAME"], 209 OSID: m["ID"], 210 OSVersionID: m["VERSION_ID"], 211 OSBuildID: m["BUILD_ID"], 212 Developer: f.Developer, 213 }, 214 Locations: []string{input.Path}, 215 } 216 217 return p, nil 218 }