Files
syft/syft/pkg/cataloger/binary/elf_package_cataloger.go
T
William Murphy 93a7d2ee27 fix: better clean up of file handles (#2823)
* always close ELF cataloger file handles

The elf-binary-package-cataloger does its own file IO to account for the
possibility of a logical ELF package being broken across multiple
physical files. However, this casued it to skip the normal invocation
pattern in the generic cataloger code that prevented file leaks. Ensure
this cataloger always closes its file handles.

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* defer closing of generic cataloger file handles

Otherwise, a panicking cataloger could leak file handles.

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* add unit test for file closed on panic parser

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* make invoke parser a static function

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* push error logging down into invoke parser

Signed-off-by: Will Murphy <will.murphy@anchore.com>

---------

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2024-05-01 12:58:17 -04:00

165 lines
5.2 KiB
Go

package binary
import (
"context"
"debug/elf"
"encoding/json"
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
)
var _ pkg.Cataloger = (*elfPackageCataloger)(nil)
type elfPackageCataloger struct {
}
// TODO: for now this accounts for a single data shape from the .note.package section of an ELF binary.
// In the future, this should be generalized to support multiple data shapes, including non-json data.
// For example, fedora includes an ELF section header as a prefix to the JSON payload: https://github.com/anchore/syft/issues/2713
type elfBinaryPackageNotes struct {
Name string `json:"name"`
Version string `json:"version"`
PURL string `json:"purl"`
CPE string `json:"cpe"`
License string `json:"license"`
pkg.ELFBinaryPackageNoteJSONPayload `json:",inline"`
Location file.Location `json:"-"`
}
type elfPackageKey struct {
Name string
Version string
PURL string
CPE string
}
func NewELFPackageCataloger() pkg.Cataloger {
return &elfPackageCataloger{}
}
func (c *elfPackageCataloger) Name() string {
return "elf-binary-package-cataloger"
}
func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
locations, err := resolver.FilesByMIMEType(mimetype.ExecutableMIMETypeSet.List()...)
if err != nil {
return nil, nil, fmt.Errorf("unable to get binary files by mime type: %w", err)
}
// first find all ELF binaries that have notes
var notesByLocation = make(map[elfPackageKey][]elfBinaryPackageNotes)
for _, location := range locations {
notes, key, err := parseElfPackageNotes(resolver, location, c)
if err != nil {
return nil, nil, err
}
if notes == nil {
continue
}
notesByLocation[key] = append(notesByLocation[key], *notes)
}
// now we have all ELF binaries that have notes, let's create packages for them.
// we do this in a second pass since it is possible that we have multiple ELF binaries with the same name and version
// which means the set of binaries collectively represent a single logical package.
var pkgs []pkg.Package
for _, notes := range notesByLocation {
noteLocations := file.NewLocationSet()
for _, note := range notes {
noteLocations.Add(note.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
}
// create a package for each unique name/version pair (based on the first note found)
pkgs = append(pkgs, newELFPackage(notes[0], noteLocations, nil))
}
// why not return relationships? We have an executable cataloger that will note the dynamic libraries imported by
// each binary. After all files and packages are processed there is a final task that creates package-to-package
// and package-to-file relationships based on the dynamic libraries imported by each binary.
return pkgs, nil, nil
}
func parseElfPackageNotes(resolver file.Resolver, location file.Location, c *elfPackageCataloger) (*elfBinaryPackageNotes, elfPackageKey, error) {
reader, err := resolver.FileContentsByLocation(location)
if err != nil {
return nil, elfPackageKey{}, fmt.Errorf("unable to get binary contents %q: %w", location.Path(), err)
}
defer internal.CloseAndLogError(reader, location.AccessPath)
notes, err := c.parseElfNotes(file.LocationReadCloser{
Location: location,
ReadCloser: reader,
})
if err != nil {
log.WithFields("file", location.Path(), "error", err).Trace("unable to parse ELF notes")
return nil, elfPackageKey{}, nil
}
if notes == nil {
return nil, elfPackageKey{}, nil
}
notes.Location = location
key := elfPackageKey{
Name: notes.Name,
Version: notes.Version,
PURL: notes.PURL,
CPE: notes.CPE,
}
return notes, key, nil
}
func (c *elfPackageCataloger) parseElfNotes(reader file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
metadata, err := getELFNotes(reader)
if err != nil {
return nil, fmt.Errorf("unable to process ELF binary: %w", err)
}
if metadata == nil || metadata.Name == "" || metadata.Version == "" {
return nil, nil
}
return metadata, nil
}
func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
unionReader, err := unionreader.GetUnionReader(r)
if err != nil {
return nil, fmt.Errorf("unable to get union reader for binary: %w", err)
}
f, err := elf.NewFile(unionReader)
if f == nil || err != nil {
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to parse binary as ELF")
return nil, nil
}
noteSection := f.Section(".note.package")
if noteSection == nil {
return nil, nil
}
notes, err := noteSection.Data()
if err != nil {
return nil, err
}
var metadata elfBinaryPackageNotes
if err := json.Unmarshal(notes, &metadata); err != nil {
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to unmarshal ELF package notes as JSON")
return nil, nil
}
return &metadata, err
}