github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/os/apk/apk.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 apk extracts packages from the APK database.
    16  package apk
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"path/filepath"
    22  
    23  	"github.com/google/osv-scalibr/extractor"
    24  	"github.com/google/osv-scalibr/extractor/filesystem"
    25  	"github.com/google/osv-scalibr/extractor/filesystem/os/apk/apkutil"
    26  	apkmeta "github.com/google/osv-scalibr/extractor/filesystem/os/apk/metadata"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/os/osrelease"
    28  	"github.com/google/osv-scalibr/inventory"
    29  	"github.com/google/osv-scalibr/log"
    30  	"github.com/google/osv-scalibr/plugin"
    31  	"github.com/google/osv-scalibr/purl"
    32  	"github.com/google/osv-scalibr/stats"
    33  )
    34  
    35  const (
    36  	// Name is the unique name of this extractor.
    37  	Name = "os/apk"
    38  )
    39  
    40  // Config is the configuration for the Extractor.
    41  type Config struct {
    42  	// Stats is a stats collector for reporting metrics.
    43  	Stats stats.Collector
    44  	// MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If
    45  	// `FileRequired` gets a bigger file, it will return false,
    46  	MaxFileSizeBytes int64
    47  }
    48  
    49  // DefaultConfig returns the default configuration for the extractor.
    50  func DefaultConfig() Config {
    51  	return Config{
    52  		MaxFileSizeBytes: 0,
    53  		Stats:            nil,
    54  	}
    55  }
    56  
    57  // Extractor extracts packages from the APK database.
    58  type Extractor struct {
    59  	stats            stats.Collector
    60  	maxFileSizeBytes int64
    61  }
    62  
    63  // New returns an APK extractor.
    64  //
    65  // For most use cases, initialize with:
    66  // ```
    67  // e := New(DefaultConfig())
    68  // ```
    69  func New(cfg Config) *Extractor {
    70  	return &Extractor{
    71  		stats:            cfg.Stats,
    72  		maxFileSizeBytes: cfg.MaxFileSizeBytes,
    73  	}
    74  }
    75  
    76  // NewDefault returns an extractor with the default config settings.
    77  func NewDefault() filesystem.Extractor { return New(DefaultConfig()) }
    78  
    79  // Name of the extractor.
    80  func (e Extractor) Name() string { return Name }
    81  
    82  // Version of the extractor.
    83  func (e Extractor) Version() int { return 0 }
    84  
    85  // Requirements of the extractor.
    86  func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
    87  
    88  // FileRequired returns true if the specified file matches apk status file pattern.
    89  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
    90  	// Should match the status file.
    91  	if filepath.ToSlash(api.Path()) != "lib/apk/db/installed" &&
    92  		filepath.ToSlash(api.Path()) != "var/lib/apk/db/installed" &&
    93  		// TODO(b/428271704): Remove once we handle symlinks properly.
    94  		filepath.ToSlash(api.Path()) != "usr/lib/apk/db/installed" {
    95  		return false
    96  	}
    97  
    98  	fileinfo, err := api.Stat()
    99  	if err != nil {
   100  		return false
   101  	}
   102  	if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes {
   103  		e.reportFileRequired(api.Path(), fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded)
   104  		return false
   105  	}
   106  
   107  	e.reportFileRequired(api.Path(), fileinfo.Size(), stats.FileRequiredResultOK)
   108  	return true
   109  }
   110  
   111  func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) {
   112  	if e.stats == nil {
   113  		return
   114  	}
   115  	e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{
   116  		Path:          path,
   117  		Result:        result,
   118  		FileSizeBytes: fileSizeBytes,
   119  	})
   120  }
   121  
   122  // Extract extracts packages from lib/apk/db/installed passed through the scan input.
   123  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
   124  	pkgs, err := e.extractFromInput(ctx, input)
   125  	if e.stats != nil {
   126  		var fileSizeBytes int64
   127  		if input.Info != nil {
   128  			fileSizeBytes = input.Info.Size()
   129  		}
   130  		e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{
   131  			Path:          input.Path,
   132  			Result:        filesystem.ExtractorErrorToFileExtractedResult(err),
   133  			FileSizeBytes: fileSizeBytes,
   134  		})
   135  	}
   136  	return inventory.Inventory{Packages: pkgs}, err
   137  }
   138  
   139  func (e Extractor) extractFromInput(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Package, error) {
   140  	m, err := osrelease.GetOSRelease(input.FS)
   141  	if err != nil {
   142  		log.Errorf("osrelease.ParseOsRelease(): %v", err)
   143  	}
   144  
   145  	scanner := apkutil.NewScanner(input.Reader)
   146  	packages := []*extractor.Package{}
   147  
   148  	for scanner.Scan() {
   149  		if err := ctx.Err(); err != nil {
   150  			return nil, fmt.Errorf("%s halted due to context error: %w", e.Name(), err)
   151  		}
   152  
   153  		record := scanner.Record()
   154  
   155  		var sourceCode *extractor.SourceCodeIdentifier
   156  		if commit, ok := record["c"]; ok {
   157  			sourceCode = &extractor.SourceCodeIdentifier{
   158  				Commit: commit,
   159  			}
   160  		}
   161  
   162  		var pkg = &extractor.Package{
   163  			Name:     record["P"],
   164  			Version:  record["V"],
   165  			PURLType: purl.TypeApk,
   166  			Metadata: &apkmeta.Metadata{
   167  				OSID:         m["ID"],
   168  				OSVersionID:  m["VERSION_ID"],
   169  				PackageName:  record["P"],
   170  				OriginName:   record["o"],
   171  				Architecture: record["A"],
   172  				Maintainer:   record["m"],
   173  			},
   174  			Licenses:   []string{record["L"]},
   175  			SourceCode: sourceCode,
   176  			Locations:  []string{input.Path},
   177  		}
   178  
   179  		if pkg.Name == "" || pkg.Version == "" {
   180  			log.Warnf("APK package name or version is empty (name: %q, version: %q)", pkg.Name, pkg.Version)
   181  			continue
   182  		}
   183  
   184  		packages = append(packages, pkg)
   185  	}
   186  
   187  	if err := scanner.Err(); err != nil {
   188  		return nil, fmt.Errorf("error while parsing apk status file: %w", err)
   189  	}
   190  
   191  	return packages, nil
   192  }