github.com/quay/claircore@v1.5.28/gobin/gobin.go (about)

     1  // Package gobin implements a package scanner that pulls go runtime and
     2  // dependency information out of a compiled executable.
     3  //
     4  // # Main module versioning
     5  //
     6  // The go toolchain currently only fills in version information for modules
     7  // obtained as a module. Most go executables are built from source checkouts,
     8  // meaning they are not in module form. See [issue 50603] for details on why and
     9  // what's being explored to provide this information. Accordingly, claircore
    10  // cannot report advisories for main modules.
    11  //
    12  // [issue 50603]: https://golang.org/issues/50603
    13  package gobin
    14  
    15  import (
    16  	"bytes"
    17  	"context"
    18  	"encoding/binary"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"os"
    24  	"runtime/trace"
    25  	"sync"
    26  
    27  	"github.com/quay/zlog"
    28  
    29  	"github.com/quay/claircore"
    30  	"github.com/quay/claircore/indexer"
    31  )
    32  
    33  // Detector detects go binaries and reports the packages used to build them.
    34  type Detector struct{}
    35  
    36  const (
    37  	detectorName    = `gobin`
    38  	detectorVersion = `5`
    39  	detectorKind    = `package`
    40  )
    41  
    42  var (
    43  	_ indexer.PackageScanner     = Detector{}
    44  	_ indexer.DefaultRepoScanner = Detector{}
    45  
    46  	Repository = claircore.Repository{
    47  		Name: "go",
    48  		URI:  "https://pkg.go.dev/",
    49  	}
    50  )
    51  
    52  // Name implements [indexer.PackageScanner].
    53  func (Detector) Name() string { return detectorName }
    54  
    55  // Version implements [indexer.PackageScanner].
    56  func (Detector) Version() string { return detectorVersion }
    57  
    58  // Kind implements [indexer.PackageScanner].
    59  func (Detector) Kind() string { return detectorKind }
    60  
    61  // Scan implements [indexer.PackageScanner].
    62  func (Detector) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Package, error) {
    63  	const peekSz = 18
    64  	if err := ctx.Err(); err != nil {
    65  		return nil, err
    66  	}
    67  	defer trace.StartRegion(ctx, "Scanner.Scan").End()
    68  	trace.Log(ctx, "layer", l.Hash.String())
    69  	ctx = zlog.ContextWithValues(ctx,
    70  		"component", "gobin/Detector.Scan",
    71  		"version", detectorVersion,
    72  		"layer", l.Hash.String())
    73  	zlog.Debug(ctx).Msg("start")
    74  	defer zlog.Debug(ctx).Msg("done")
    75  
    76  	sys, err := l.FS()
    77  	if err != nil {
    78  		return nil, fmt.Errorf("gobin: unable to open layer: %w", err)
    79  	}
    80  
    81  	var out []*claircore.Package
    82  
    83  	peek := make([]byte, peekSz)
    84  	// Spooling support.
    85  	//
    86  	// Only create a single spool file per call, re-use for every binary.
    87  	var spool spoolfile
    88  	walk := func(p string, d fs.DirEntry, err error) error {
    89  		ctx := zlog.ContextWithValues(ctx, "path", d.Name())
    90  		switch {
    91  		case err != nil:
    92  			return err
    93  		case d.IsDir():
    94  			return nil
    95  		case ctx.Err() != nil:
    96  			return ctx.Err()
    97  		}
    98  		fi, err := d.Info()
    99  		if err != nil {
   100  			return err
   101  		}
   102  		m := fi.Mode()
   103  		switch {
   104  		case !m.IsRegular():
   105  			return nil
   106  		case m.Perm()&0o555 == 0:
   107  			// Not executable
   108  			return nil
   109  		}
   110  		f, err := sys.Open(p)
   111  		if err != nil {
   112  			// TODO(crozzy): Remove log line once controller is in a
   113  			// position to log all the context when receiving an error.
   114  			zlog.Warn(ctx).Msg("unable to open file")
   115  			return fmt.Errorf("gobin: unable to open %q: %w", p, err)
   116  		}
   117  		defer f.Close()
   118  
   119  		_, err = io.ReadFull(f, peek)
   120  		switch {
   121  		case errors.Is(err, nil):
   122  		case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
   123  			// Valid error with empty, or tiny files.
   124  			return nil
   125  		default:
   126  			// TODO(crozzy): Remove log line once controller is in a
   127  			// position to log all the context when receiving an error.
   128  			zlog.Warn(ctx).Msg("unable to read file")
   129  			return fmt.Errorf("gobin: unable to read %q: %w", p, err)
   130  		}
   131  
   132  		isELF := bytes.HasPrefix(peek, []byte("\x7fELF"))
   133  		isPE := bytes.HasPrefix(peek, []byte("MZ"))
   134  		if !isELF && !isPE { // Do OSX containers exist?
   135  			// not an ELF or PE binary
   136  			return nil
   137  		}
   138  		if isELF {
   139  			// Using hex constants because the nice table on Wikipedia uses
   140  			// them.
   141  			var typ uint16
   142  			switch e := peek[0x05]; e {
   143  			case 1: // little-endian
   144  				typ = binary.LittleEndian.Uint16(peek[0x10:])
   145  			case 2: // big-endian
   146  				typ = binary.BigEndian.Uint16(peek[0x10:])
   147  			default:
   148  				zlog.Warn(ctx).
   149  					Uint8("endianness", e).
   150  					Msg("martian ELF")
   151  			}
   152  			if typ != 0x02 && typ != 0x03 {
   153  				// AKA [debug/elf.ET_EXEC] and [debug/elf.ET_DYN] -- not imported in this file by convention.
   154  				// Not an executable or shared object, skip.
   155  				return nil
   156  			}
   157  		}
   158  
   159  		rd, ok := f.(io.ReaderAt)
   160  		if !ok {
   161  			// Need to spool the exe.
   162  			if err := spool.Setup(); err != nil {
   163  				return fmt.Errorf("gobin: unable to setup spool: %w", err)
   164  			}
   165  			if _, err := spool.File.Write(peek); err != nil {
   166  				return fmt.Errorf("gobin: unable to spool %q: %w", p, err)
   167  			}
   168  			sz, err := io.Copy(spool.File, f)
   169  			if err != nil {
   170  				return fmt.Errorf("gobin: unable to spool %q: %w", p, err)
   171  			}
   172  			rd = io.NewSectionReader(spool.File, 0, sz+peekSz)
   173  		}
   174  		return toPackages(ctx, &out, p, rd)
   175  	}
   176  	if err := fs.WalkDir(sys, ".", walk); err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	return out, nil
   181  }
   182  
   183  // DefaultRepository implements [indexer.DefaultRepoScanner].
   184  func (Detector) DefaultRepository(ctx context.Context) *claircore.Repository {
   185  	return &Repository
   186  }
   187  
   188  type spoolfile struct {
   189  	sync.Once
   190  	File *os.File
   191  	err  error
   192  }
   193  
   194  func (s *spoolfile) Setup() error {
   195  	s.Do(s.setup)
   196  	if s.err != nil {
   197  		return s.err
   198  	}
   199  	if _, err := s.File.Seek(0, io.SeekStart); err != nil {
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  func (s *spoolfile) setup() {
   206  	f, err := os.CreateTemp("", "gobin.spool.*")
   207  	if err != nil {
   208  		s.err = err
   209  		return
   210  	}
   211  	if err := os.Remove(f.Name()); err != nil {
   212  		s.err = err
   213  		return
   214  	}
   215  	s.File = f
   216  }