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 }