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  }