Skip to content

Commit

Permalink
gobin: add go binary package scanner
Browse files Browse the repository at this point in the history
Signed-off-by: Hank Donnay <hdonnay@redhat.com>
  • Loading branch information
hdonnay committed Dec 5, 2022
1 parent 171fbb9 commit 2541614
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 0 deletions.
20 changes: 20 additions & 0 deletions gobin/ecosystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gobin

import (
"context"

"github.com/quay/claircore/indexer"
)

// NewEcosystem provides the ecosystem for handling go binaries.
func NewEcosystem(ctx context.Context) *indexer.Ecosystem {
return &indexer.Ecosystem{
PackageScanners: func(context.Context) ([]indexer.PackageScanner, error) {
return []indexer.PackageScanner{Detector{}}, nil
},
DistributionScanners: func(context.Context) ([]indexer.DistributionScanner, error) { return nil, nil },
RepositoryScanners: func(context.Context) ([]indexer.RepositoryScanner, error) { return nil, nil },
// BUG(hank) The Ecosystem needs a coalescer that understands whiteouts.
Coalescer: func(context.Context) (indexer.Coalescer, error) { return nil, nil },
}
}
58 changes: 58 additions & 0 deletions gobin/exe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package gobin

import (
"context"
"debug/buildinfo"
"io"

"github.com/quay/zlog"

"github.com/quay/claircore"
)

func toPackages(ctx context.Context, out *[]*claircore.Package, p string, r io.ReaderAt) error {
bi, err := buildinfo.Read(r)
if err != nil {
zlog.Info(ctx).
Err(err).
Msg("unable to open executable")
return nil
}
ctx = zlog.ContextWithValues(ctx, "exe", p)
pkgdb := "go:" + p

*out = append(*out, &claircore.Package{
Kind: claircore.BINARY,
Name: "runtime",
Version: bi.GoVersion,
PackageDB: pkgdb,
})
ev := zlog.Debug(ctx)
vs := map[string]string{
"runtime": bi.GoVersion,
}
*out = append(*out, &claircore.Package{
Kind: claircore.BINARY,
PackageDB: pkgdb,
Name: bi.Main.Path,
Version: bi.Main.Version,
})
if ev.Enabled() {
vs[bi.Main.Path] = bi.Main.Version
}
for _, d := range bi.Deps {
*out = append(*out, &claircore.Package{
Kind: claircore.BINARY,
PackageDB: pkgdb,
Name: d.Path,
Version: d.Version,
})
if ev.Enabled() {
vs[d.Path] = d.Version
}
}
ev.
Interface("versions", vs).
Msg("analyzed exe")
return nil
}
159 changes: 159 additions & 0 deletions gobin/gobin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Package gobin implements a package scanner that pulls go runtime and
// dependency information out of a compiled executable.
package gobin

import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
"os"
"runtime/trace"
"sync"

"github.com/quay/zlog"

"github.com/quay/claircore"
"github.com/quay/claircore/indexer"
"github.com/quay/claircore/pkg/tarfs"
)

// Detector detects go binaries and reports the packages used to build them.
type Detector struct{}

const (
detectorName = `gobin`
detectorVersion = `1`
detectorKind = `package`
)

var _ indexer.PackageScanner = Detector{}

// Name implements [indexer.PackageScanner].
func (Detector) Name() string { return detectorName }

// Version implements [indexer.PackageScanner].
func (Detector) Version() string { return detectorVersion }

// Kind implements [indexer.PackageScanner].
func (Detector) Kind() string { return detectorKind }

// Scan implements [indexer.PackageScanner].
func (Detector) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Package, error) {
const peekSz = 4
if err := ctx.Err(); err != nil {
return nil, err
}
defer trace.StartRegion(ctx, "Scanner.Scan").End()
trace.Log(ctx, "layer", l.Hash.String())
ctx = zlog.ContextWithValues(ctx,
"component", "gobin/Detector.Scan",
"version", detectorVersion,
"layer", l.Hash.String())
zlog.Debug(ctx).Msg("start")
defer zlog.Debug(ctx).Msg("done")

rd, err := l.Reader()
if err != nil {
return nil, err
}
defer rd.Close()
sys, err := tarfs.New(rd)
if err != nil {
return nil, err
}

var out []*claircore.Package

peek := make([]byte, peekSz)
// Spooling support.
//
// Only create a single spool file per call, re-use for every binary.
var spool spoolfile
walk := func(p string, d fs.DirEntry, err error) error {
ctx := zlog.ContextWithValues(ctx, "path", d.Name())
switch {
case err != nil:
return err
case d.IsDir():
return nil
}
fi, err := d.Info()
if err != nil {
return err
}
m := fi.Mode()
switch {
case !m.IsRegular():
return nil
case m.Perm()&0o555 == 0:
// Not executable
return nil
}
f, err := sys.Open(p)
if err != nil {
return fmt.Errorf("gobin: unable to open %q: %w", p, err)
}
defer f.Close()

if _, err := io.ReadFull(f, peek); err != nil {
return fmt.Errorf("gobin: unable to read %q: %w", p, err)
}
if !bytes.HasPrefix(peek, []byte("\x7fELF")) && !bytes.HasPrefix(peek, []byte("MZ")) {
// not an ELF or PE binary
return nil
}
rd, ok := f.(io.ReaderAt)
if !ok {
// Need to spool the exe.
if err := spool.Setup(); err != nil {
return fmt.Errorf("gobin: unable to setup spool: %w", err)
}
if _, err := spool.File.Write(peek); err != nil {
return fmt.Errorf("gobin: unable to spool %q: %w", p, err)
}
sz, err := io.Copy(spool.File, f)
if err != nil {
return fmt.Errorf("gobin: unable to spool %q: %w", p, err)
}
rd = io.NewSectionReader(spool.File, 0, sz+peekSz)
}
return toPackages(ctx, &out, p, rd)
}
if err := fs.WalkDir(sys, ".", walk); err != nil {
return nil, err
}

return out, nil
}

type spoolfile struct {
sync.Once
File *os.File
err error
}

func (s *spoolfile) Setup() error {
s.Do(s.setup)
if s.err != nil {
return s.err
}
if _, err := s.File.Seek(0, io.SeekStart); err != nil {
return err
}
return nil
}

func (s *spoolfile) setup() {
f, err := os.CreateTemp("", "gobin.spool.*")
if err != nil {
s.err = err
return
}
if err := os.Remove(f.Name()); err != nil {
s.err = err
return
}
s.File = f
}
106 changes: 106 additions & 0 deletions gobin/gobin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package gobin

import (
"archive/tar"
"context"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/quay/zlog"

"github.com/quay/claircore"
)

func TestScanner(t *testing.T) {
ctx := zlog.Test(context.Background(), t)
tmpdir := t.TempDir()

// Build a go binary.
outname := filepath.Join(tmpdir, "bisect")
cmd := exec.CommandContext(ctx, "go", "build", "-o", outname, "github.com/quay/claircore/test/bisect")
out, err := cmd.CombinedOutput()
if len(out) != 0 {
t.Logf("%q", string(out))
}
if err != nil {
t.Fatal(err)
}
inf, err := os.Open(outname)
if err != nil {
t.Fatal(err)
}
defer inf.Close()
fi, err := inf.Stat()
if err != nil {
t.Fatal(err)
}
t.Logf("wrote binary to: %s", inf.Name())

// Write a tarball with the binary.
tarname := filepath.Join(tmpdir, "tar")
tf, err := os.Create(tarname)
if err != nil {
t.Fatal(err)
}
defer tf.Close()
tw := tar.NewWriter(tf)
hdr, err := tar.FileInfoHeader(fi, "")
if err != nil {
t.Fatal(err)
}
hdr.Name = "./bin/bisect"
if err := tw.WriteHeader(hdr); err != nil {
t.Error(err)
}
if _, err := io.Copy(tw, inf); err != nil {
t.Error(err)
}
if err := tw.Close(); err != nil {
t.Error(err)
}
t.Logf("wrote tar to: %s", tf.Name())

// Make a fake layer with the tarball.
l := claircore.Layer{
Hash: claircore.MustParseDigest(`sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`),
URI: `file:///dev/null`,
}
l.SetLocal(tf.Name())

// Run the scanner on the fake layer.
var s Detector
vs, err := s.Scan(ctx, &l)
if err != nil {
t.Error(err)
}
if len(vs) == 0 {
t.Error("no results returned")
}
// Why not just have a list? It'd change on every dependency update, which
// would be annoying.
for _, v := range vs {
switch {
case v.Name == "runtime":
continue
case v.Version == "(devel)":
continue
case v.Kind != claircore.BINARY:
case v.PackageDB != "go:bin/bisect":
t.Errorf("unexpected package DB: %s: %q", v.Name, v.PackageDB)
case !verRegexp.MatchString(v.Version):
t.Errorf("unexpected version: %s: %q", v.Name, v.Version)
case !strings.Contains(v.Name, "/"):
t.Errorf("unexpected module name: %q", v.Name)
default:
continue
}
t.Errorf("unexpected entry: %v", v)
}
}

var verRegexp = regexp.MustCompile(`^v([0-9]+\.){2}[0-9]+(-[.0-9]+-[0-9a-f]+)?(\+incompatible)?$`)
2 changes: 2 additions & 0 deletions libindex/libindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/quay/claircore"
"github.com/quay/claircore/alpine"
"github.com/quay/claircore/dpkg"
"github.com/quay/claircore/gobin"
"github.com/quay/claircore/indexer"
"github.com/quay/claircore/java"
"github.com/quay/claircore/pkg/omnimatcher"
Expand Down Expand Up @@ -95,6 +96,7 @@ func New(ctx context.Context, opts *Options, cl *http.Client) (*Libindex, error)
python.NewEcosystem(ctx),
java.NewEcosystem(ctx),
rhcc.NewEcosystem(ctx),
gobin.NewEcosystem(ctx),
}
}

Expand Down

0 comments on commit 2541614

Please sign in to comment.