-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gobin: add go binary package scanner
Signed-off-by: Hank Donnay <hdonnay@redhat.com>
- Loading branch information
Showing
5 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)?$`) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters