github.com/google/osv-scalibr@v0.4.1/annotator/osduplicate/rpm/rpm_linux.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  //go:build linux
    16  
    17  package rpm
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io/fs"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"slices"
    28  	"strings"
    29  
    30  	rpmdb "github.com/erikvarga/go-rpmdb/pkg"
    31  	"github.com/google/osv-scalibr/annotator"
    32  	"github.com/google/osv-scalibr/annotator/osduplicate"
    33  	"github.com/google/osv-scalibr/extractor"
    34  	scalibrfs "github.com/google/osv-scalibr/fs"
    35  	"github.com/google/osv-scalibr/inventory"
    36  	"github.com/google/osv-scalibr/inventory/vex"
    37  	"github.com/google/osv-scalibr/log"
    38  
    39  	// SQLite driver needed for parsing rpmdb.sqlite files.
    40  	_ "modernc.org/sqlite"
    41  )
    42  
    43  var (
    44  	// Directories and files where RPM descriptor packages can be found.
    45  	rpmDirectories = []string{
    46  		"usr/lib/sysimage/rpm/",
    47  		"var/lib/rpm/",
    48  		"usr/share/rpm/",
    49  	}
    50  	rpmFilenames = []string{
    51  		// Berkley DB (old format)
    52  		"Packages",
    53  		// NDB (very rare alternative to sqlite)
    54  		"Packages.db",
    55  		// SQLite3 (new format)
    56  		"rpmdb.sqlite",
    57  	}
    58  )
    59  
    60  // Annotate adds annotations to language packages that have already been found in RPM OS packages.
    61  func (a *Annotator) Annotate(ctx context.Context, input *annotator.ScanInput, results *inventory.Inventory) error {
    62  	locationToPKGs := osduplicate.BuildLocationToPKGsMap(results, input.ScanRoot)
    63  
    64  	errs := []error{}
    65  	for _, dir := range rpmDirectories {
    66  		for _, file := range rpmFilenames {
    67  			// Return if canceled or exceeding deadline.
    68  			if err := ctx.Err(); err != nil {
    69  				errs = append(errs, fmt.Errorf("%s halted at %q because of context error: %w", a.Name(), input.ScanRoot.Path, err))
    70  				break
    71  			}
    72  
    73  			dbPath := path.Join(dir, file)
    74  			// Skip files that don't exist.
    75  			if _, err := fs.Stat(input.ScanRoot.FS, dbPath); err != nil {
    76  				continue
    77  			}
    78  
    79  			if err := a.annotatePackagesInRPMDB(ctx, input.ScanRoot, dbPath, locationToPKGs); err != nil {
    80  				return err
    81  			}
    82  		}
    83  	}
    84  
    85  	return errors.Join(errs...)
    86  }
    87  
    88  func (a *Annotator) annotatePackagesInRPMDB(ctx context.Context, root *scalibrfs.ScanRoot, dbPath string, locationToPKGs map[string][]*extractor.Package) error {
    89  	realDBPath, err := scalibrfs.GetRealPath(root, dbPath, nil)
    90  	if err != nil {
    91  		return fmt.Errorf("GetRealPath(%v, %v): %w", root, dbPath, err)
    92  	}
    93  	if root.IsVirtual() {
    94  		// The file got copied to a temporary dir, remove it at the end.
    95  		defer func() {
    96  			dir := filepath.Dir(realDBPath)
    97  			if err := os.RemoveAll(dir); err != nil {
    98  				log.Errorf("os.RemoveAll(%q): %v", dir, err)
    99  			}
   100  		}()
   101  	}
   102  
   103  	db, err := rpmdb.Open(realDBPath)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	defer db.Close()
   108  
   109  	var pkgs []*rpmdb.PackageInfo
   110  	if a.Timeout == 0 {
   111  		pkgs, err = db.ListPackages()
   112  		if err != nil {
   113  			return err
   114  		}
   115  	} else {
   116  		ctx, cancelFunc := context.WithTimeout(ctx, a.Timeout)
   117  		defer cancelFunc()
   118  
   119  		pkgs, err = db.ListPackagesWithContext(ctx)
   120  		if err != nil {
   121  			return err
   122  		}
   123  	}
   124  
   125  	for _, pkg := range pkgs {
   126  		for i, base := range pkg.BaseNames {
   127  			if len(pkg.DirIndexes) <= i {
   128  				return fmt.Errorf("malformed RPM directory index: want %d entries, got %d", i+1, len(pkg.DirIndexes))
   129  			}
   130  			dir := pkg.DirNames[pkg.DirIndexes[i]]
   131  			// Remove leading '/' since SCALIBR fs paths don't include that.
   132  			path := strings.TrimPrefix(dir+base, "/")
   133  
   134  			if pkgs, ok := locationToPKGs[path]; ok {
   135  				for _, pkg := range pkgs {
   136  					if !slices.ContainsFunc(pkg.ExploitabilitySignals, func(s *vex.PackageExploitabilitySignal) bool {
   137  						return s.Plugin == Name
   138  					}) {
   139  						pkg.ExploitabilitySignals = append(pkg.ExploitabilitySignals, &vex.PackageExploitabilitySignal{
   140  							Plugin:          Name,
   141  							Justification:   vex.ComponentNotPresent,
   142  							MatchesAllVulns: true,
   143  						})
   144  					}
   145  				}
   146  			}
   147  		}
   148  	}
   149  
   150  	return nil
   151  }