github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/redhat/parse_rpm_db.go (about)

     1  package redhat
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	rpmdb "github.com/anchore/go-rpmdb/pkg"
    12  	"github.com/anchore/syft/internal/log"
    13  	"github.com/anchore/syft/internal/unknown"
    14  	"github.com/anchore/syft/syft/artifact"
    15  	"github.com/anchore/syft/syft/file"
    16  	"github.com/anchore/syft/syft/linux"
    17  	"github.com/anchore/syft/syft/pkg"
    18  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    19  )
    20  
    21  // parseRpmDB parses an "Packages" RPM DB and returns the Packages listed within it.
    22  //
    23  //nolint:funlen
    24  func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    25  	f, err := os.CreateTemp("", "rpmdb")
    26  	if err != nil {
    27  		return nil, nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)
    28  	}
    29  
    30  	defer func() {
    31  		err = f.Close()
    32  		if err != nil {
    33  			log.Errorf("failed to close temp rpmdb file: %+v", err)
    34  		}
    35  		err = os.Remove(f.Name())
    36  		if err != nil {
    37  			log.Errorf("failed to remove temp rpmdb file: %+v", err)
    38  		}
    39  	}()
    40  
    41  	_, err = io.Copy(f, reader)
    42  	if err != nil {
    43  		return nil, nil, fmt.Errorf("failed to copy rpmdb contents to temp file: %w", err)
    44  	}
    45  
    46  	db, err := rpmdb.Open(f.Name())
    47  	if err != nil {
    48  		return nil, nil, err
    49  	}
    50  	defer db.Close()
    51  
    52  	pkgList, err := db.ListPackages()
    53  	if err != nil {
    54  		return nil, nil, err
    55  	}
    56  
    57  	var allPkgs []pkg.Package
    58  
    59  	var distro *linux.Release
    60  	if env != nil {
    61  		distro = env.LinuxRelease
    62  	}
    63  
    64  	var errs error
    65  	for _, entry := range pkgList {
    66  		if entry == nil {
    67  			continue
    68  		}
    69  
    70  		files, err := extractRpmFileRecords(resolver, *entry)
    71  		errs = unknown.Join(errs, err)
    72  
    73  		// there is a period of time when RPM DB entries contain both PGP and RSA signatures that are the same.
    74  		// This appears to be a holdover, where nowadays only the RSA Header is used.
    75  		sigs, err := parseSignatures(strings.TrimSpace(entry.PGP), strings.TrimSpace(entry.RSAHeader))
    76  		if err != nil {
    77  			log.WithFields("error", err, "location", reader.RealPath, "pkg", fmt.Sprintf("%s@%s", entry.Name, entry.Version)).Trace("unable to parse signatures for package %s", entry.Name)
    78  			sigs = nil
    79  		}
    80  
    81  		metadata := pkg.RpmDBEntry{
    82  			Name:            entry.Name,
    83  			Version:         entry.Version,
    84  			Epoch:           entry.Epoch,
    85  			Arch:            entry.Arch,
    86  			Release:         entry.Release,
    87  			SourceRpm:       entry.SourceRpm,
    88  			Signatures:      sigs,
    89  			Vendor:          entry.Vendor,
    90  			Size:            entry.Size,
    91  			ModularityLabel: &entry.Modularitylabel,
    92  			Files:           files,
    93  			Provides:        entry.Provides,
    94  			Requires:        entry.Requires,
    95  		}
    96  
    97  		p := newDBPackage(
    98  			ctx,
    99  			reader.Location,
   100  			metadata,
   101  			distro,
   102  			[]string{entry.License},
   103  		)
   104  
   105  		if !pkg.IsValid(&p) {
   106  			log.WithFields("location", reader.RealPath, "pkg", fmt.Sprintf("%s@%s", entry.Name, entry.Version)).
   107  				Warn("ignoring invalid package found in RPM DB")
   108  			errs = unknown.Appendf(errs, reader, "invalild package found; name: %s, version: %s", entry.Name, entry.Version)
   109  			continue
   110  		}
   111  
   112  		p.SetID()
   113  		allPkgs = append(allPkgs, p)
   114  	}
   115  
   116  	if errs == nil && len(allPkgs) == 0 {
   117  		errs = fmt.Errorf("unable to determine packages")
   118  	}
   119  
   120  	return allPkgs, nil, errs
   121  }
   122  
   123  func parseSignatures(sigs ...string) ([]pkg.RpmSignature, error) {
   124  	var parsedSigs []pkg.RpmSignature
   125  	var errs error
   126  	for _, sig := range sigs {
   127  		if sig == "" {
   128  			continue
   129  		}
   130  		parts := strings.Split(sig, ",")
   131  		if len(parts) != 3 {
   132  			errs = errors.Join(fmt.Errorf("invalid signature format: %s", sig))
   133  			continue
   134  		}
   135  
   136  		methodParts := strings.SplitN(strings.TrimSpace(parts[0]), "/", 2)
   137  		if len(methodParts) != 2 {
   138  			errs = errors.Join(fmt.Errorf("invalid signature method format: %s", parts[0]))
   139  			continue
   140  		}
   141  
   142  		pka := strings.TrimSpace(methodParts[0])
   143  		hash := strings.TrimSpace(methodParts[1])
   144  
   145  		if pka == "" || hash == "" {
   146  			errs = errors.Join(fmt.Errorf("invalid signature method values: public-key=%q hash=%q", pka, hash))
   147  			continue
   148  		}
   149  
   150  		created := strings.TrimSpace(parts[1])
   151  		if created == "" {
   152  			errs = errors.Join(fmt.Errorf("invalid signature created value: %q", parts[1]))
   153  			continue
   154  		}
   155  
   156  		issuerFields := strings.Split(strings.TrimSpace(parts[2]), " ")
   157  		var issuer string
   158  		switch len(issuerFields) {
   159  		case 0:
   160  			errs = errors.Join(fmt.Errorf("no signature issuer value: %q", parts[2]))
   161  		case 1:
   162  			issuer = issuerFields[0]
   163  		default:
   164  			issuer = issuerFields[len(issuerFields)-1]
   165  			if issuer == "" {
   166  				errs = errors.Join(fmt.Errorf("invalid signature issuer value: %q", parts[2]))
   167  				continue
   168  			}
   169  		}
   170  
   171  		if len(issuer) < 5 {
   172  			errs = errors.Join(fmt.Errorf("invalid signature issuer length: %q", parts[2]))
   173  			continue
   174  		}
   175  
   176  		parsedSig := pkg.RpmSignature{
   177  			PublicKeyAlgorithm: pka,
   178  			HashAlgorithm:      hash,
   179  			Created:            created,
   180  			IssuerKeyID:        issuer,
   181  		}
   182  		parsedSigs = append(parsedSigs, parsedSig)
   183  	}
   184  	return parsedSigs, errs
   185  }
   186  
   187  // The RPM naming scheme is [name]-[version]-[release]-[arch], where version is implicitly expands to [epoch]:[version].
   188  // RPM version comparison depends on comparing at least the version and release fields together as a subset of the
   189  // naming scheme. This toELVersion function takes a RPM DB package information and converts it into a minimally comparable
   190  // version string, containing epoch (optional), version, and release information. Epoch is an optional field and can be
   191  // assumed to be 0 when not provided for comparison purposes, however, if the underlying RPM DB entry does not have
   192  // an epoch specified it would be slightly disingenuous to display a value of 0.
   193  func toELVersion(epoch *int, version, release string) string {
   194  	if epoch != nil {
   195  		return fmt.Sprintf("%d:%s-%s", *epoch, version, release)
   196  	}
   197  	return fmt.Sprintf("%s-%s", version, release)
   198  }
   199  
   200  func extractRpmFileRecords(resolver file.PathResolver, entry rpmdb.PackageInfo) ([]pkg.RpmFileRecord, error) {
   201  	var records = make([]pkg.RpmFileRecord, 0)
   202  
   203  	files, err := entry.InstalledFiles()
   204  	if err != nil {
   205  		log.Debugf("unable to parse listing of installed files for RPM DB entry: %s", err.Error())
   206  		return records, fmt.Errorf("unable to parse listing of installed files for RPM DB entry: %w", err)
   207  	}
   208  
   209  	for _, record := range files {
   210  		// only persist RPMDB file records which exist in the image/directory, otherwise ignore them
   211  		if resolver.HasPath(record.Path) {
   212  			records = append(records, pkg.RpmFileRecord{
   213  				Path: record.Path,
   214  				Mode: pkg.RpmFileMode(record.Mode),
   215  				Size: int(record.Size),
   216  				Digest: file.Digest{
   217  					Value:     record.Digest,
   218  					Algorithm: entry.DigestAlgorithm.String(),
   219  				},
   220  				UserName:  record.Username,
   221  				GroupName: record.Groupname,
   222  				Flags:     record.Flags.String(),
   223  			})
   224  		}
   225  	}
   226  	return records, nil
   227  }