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  }