github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/binary/elf_package_cataloger.go (about)

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