github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/binary/elf_package_cataloger.go (about) 1 package binary 2 3 import ( 4 "context" 5 "debug/elf" 6 "encoding/json" 7 "fmt" 8 9 "github.com/anchore/syft/internal" 10 "github.com/anchore/syft/internal/log" 11 "github.com/anchore/syft/internal/mimetype" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/internal/unionreader" 15 "github.com/anchore/syft/syft/pkg" 16 ) 17 18 var _ pkg.Cataloger = (*elfPackageCataloger)(nil) 19 20 type elfPackageCataloger struct { 21 } 22 23 // TODO: for now this accounts for a single data shape from the .note.package section of an ELF binary. 24 // In the future, this should be generalized to support multiple data shapes, including non-json data. 25 // For example, fedora includes an ELF section header as a prefix to the JSON payload: https://github.com/anchore/syft/issues/2713 26 27 type elfBinaryPackageNotes struct { 28 Name string `json:"name"` 29 Version string `json:"version"` 30 PURL string `json:"purl"` 31 CPE string `json:"cpe"` 32 License string `json:"license"` 33 pkg.ELFBinaryPackageNoteJSONPayload `json:",inline"` 34 Location file.Location `json:"-"` 35 } 36 37 type elfPackageKey struct { 38 Name string 39 Version string 40 PURL string 41 CPE string 42 } 43 44 func NewELFPackageCataloger() pkg.Cataloger { 45 return &elfPackageCataloger{} 46 } 47 48 func (c *elfPackageCataloger) Name() string { 49 return "elf-binary-package-cataloger" 50 } 51 52 func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { 53 locations, err := resolver.FilesByMIMEType(mimetype.ExecutableMIMETypeSet.List()...) 54 if err != nil { 55 return nil, nil, fmt.Errorf("unable to get binary files by mime type: %w", err) 56 } 57 58 // first find all ELF binaries that have notes 59 var notesByLocation = make(map[elfPackageKey][]elfBinaryPackageNotes) 60 for _, location := range locations { 61 notes, key, err := parseElfPackageNotes(resolver, location, c) 62 if err != nil { 63 return nil, nil, err 64 } 65 if notes == nil { 66 continue 67 } 68 notesByLocation[key] = append(notesByLocation[key], *notes) 69 } 70 71 // now we have all ELF binaries that have notes, let's create packages for them. 72 // we do this in a second pass since it is possible that we have multiple ELF binaries with the same name and version 73 // which means the set of binaries collectively represent a single logical package. 74 var pkgs []pkg.Package 75 for _, notes := range notesByLocation { 76 noteLocations := file.NewLocationSet() 77 for _, note := range notes { 78 noteLocations.Add(note.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) 79 } 80 81 // create a package for each unique name/version pair (based on the first note found) 82 pkgs = append(pkgs, newELFPackage(notes[0], noteLocations, nil)) 83 } 84 85 // why not return relationships? We have an executable cataloger that will note the dynamic libraries imported by 86 // each binary. After all files and packages are processed there is a final task that creates package-to-package 87 // and package-to-file relationships based on the dynamic libraries imported by each binary. 88 return pkgs, nil, nil 89 } 90 91 func parseElfPackageNotes(resolver file.Resolver, location file.Location, c *elfPackageCataloger) (*elfBinaryPackageNotes, elfPackageKey, error) { 92 reader, err := resolver.FileContentsByLocation(location) 93 if err != nil { 94 return nil, elfPackageKey{}, fmt.Errorf("unable to get binary contents %q: %w", location.Path(), err) 95 } 96 defer internal.CloseAndLogError(reader, location.AccessPath) 97 98 notes, err := c.parseElfNotes(file.LocationReadCloser{ 99 Location: location, 100 ReadCloser: reader, 101 }) 102 103 if err != nil { 104 log.WithFields("file", location.Path(), "error", err).Trace("unable to parse ELF notes") 105 return nil, elfPackageKey{}, nil 106 } 107 108 if notes == nil { 109 return nil, elfPackageKey{}, nil 110 } 111 112 notes.Location = location 113 key := elfPackageKey{ 114 Name: notes.Name, 115 Version: notes.Version, 116 PURL: notes.PURL, 117 CPE: notes.CPE, 118 } 119 return notes, key, nil 120 } 121 122 func (c *elfPackageCataloger) parseElfNotes(reader file.LocationReadCloser) (*elfBinaryPackageNotes, error) { 123 metadata, err := getELFNotes(reader) 124 if err != nil { 125 return nil, fmt.Errorf("unable to process ELF binary: %w", err) 126 } 127 128 if metadata == nil || metadata.Name == "" || metadata.Version == "" { 129 return nil, nil 130 } 131 132 return metadata, nil 133 } 134 135 func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) { 136 unionReader, err := unionreader.GetUnionReader(r) 137 if err != nil { 138 return nil, fmt.Errorf("unable to get union reader for binary: %w", err) 139 } 140 141 f, err := elf.NewFile(unionReader) 142 if f == nil || err != nil { 143 log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to parse binary as ELF") 144 return nil, nil 145 } 146 147 noteSection := f.Section(".note.package") 148 if noteSection == nil { 149 return nil, nil 150 } 151 152 notes, err := noteSection.Data() 153 if err != nil { 154 return nil, err 155 } 156 157 var metadata elfBinaryPackageNotes 158 if err := json.Unmarshal(notes, &metadata); err != nil { 159 log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to unmarshal ELF package notes as JSON") 160 return nil, nil 161 } 162 163 return &metadata, err 164 }