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

     1  package gobin
     2  
     3  import (
     4  	"context"
     5  	"debug/buildinfo"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  	_ "unsafe" // for error linkname tricks
    13  
    14  	"github.com/quay/zlog"
    15  
    16  	"github.com/quay/claircore"
    17  )
    18  
    19  //go:linkname errNotGoExe debug/buildinfo.errNotGoExe
    20  var errNotGoExe error
    21  
    22  // It's frustrating that there's no good way to check the error returned from
    23  // [buildinfo.Read]. It's either doing a string compare, which will break
    24  // silently if the error's contents are changed, or the linker tricks done here,
    25  // which will break loudly if the error is renamed or built differently.
    26  
    27  func toPackages(ctx context.Context, out *[]*claircore.Package, p string, r io.ReaderAt) error {
    28  	bi, err := buildinfo.Read(r)
    29  	switch {
    30  	case errors.Is(err, nil):
    31  	case errors.Is(err, errNotGoExe):
    32  		return nil
    33  	default:
    34  		zlog.Debug(ctx).
    35  			Err(err).
    36  			Msg("unable to open executable")
    37  		return nil
    38  	}
    39  	ctx = zlog.ContextWithValues(ctx, "exe", p)
    40  	pkgdb := "go:" + p
    41  	badVers := make(map[string]string)
    42  	defer func() {
    43  		if len(badVers) == 0 {
    44  			return
    45  		}
    46  		zlog.Warn(ctx).
    47  			Interface("module_versions", badVers).
    48  			Msg("invalid semantic versions found in binary")
    49  	}()
    50  
    51  	// TODO(hank) This package could use canonical versions, but the
    52  	// [claircore.Version] type is lossy for pre-release versions (I'm sorry).
    53  
    54  	// TODO(hank) The "go version" is documented as the toolchain that produced
    55  	// the binary, which may be distinct from the version of the stdlib used?
    56  	// Need to investigate.
    57  	runtimeVer, err := ParseVersion(strings.TrimPrefix(bi.GoVersion, "go"))
    58  	switch {
    59  	case errors.Is(err, nil):
    60  	case errors.Is(err, ErrInvalidSemVer):
    61  		badVers["stdlib"] = bi.GoVersion
    62  	default:
    63  		return fmt.Errorf("error parsing runtime version: %q: %w", bi.GoVersion, err)
    64  	}
    65  
    66  	*out = append(*out, &claircore.Package{
    67  		Kind:              claircore.BINARY,
    68  		Name:              "stdlib",
    69  		Version:           bi.GoVersion,
    70  		PackageDB:         pkgdb,
    71  		Filepath:          p,
    72  		NormalizedVersion: runtimeVer,
    73  	})
    74  
    75  	ev := zlog.Debug(ctx)
    76  	vs := map[string]string{
    77  		"stdlib": bi.GoVersion,
    78  	}
    79  	var mmv string
    80  	mainVer, err := ParseVersion(bi.Main.Version)
    81  	switch {
    82  	case errors.Is(err, nil):
    83  	case bi.Main.Version == `(devel)`, bi.Main.Version == ``:
    84  		// This is currently the state of any main module built from source; see
    85  		// the package documentation. Don't record it as a "bad" version and
    86  		// pull out any vcs metadata that's been stamped in.
    87  		mmv = bi.Main.Version
    88  		var v []string
    89  		for _, s := range bi.Settings {
    90  			switch s.Key {
    91  			case "vcs":
    92  				v = append(v, s.Value)
    93  			case "vcs.revision":
    94  				switch len(s.Value) {
    95  				case 40, 64:
    96  					v = append(v, "commit "+s.Value)
    97  				default:
    98  					v = append(v, "rev "+s.Value)
    99  				}
   100  			case "vcs.time":
   101  				v = append(v, "built at "+s.Value)
   102  			case "vcs.modified":
   103  				if s.Value == "true" {
   104  					v = append(v, "dirty")
   105  				}
   106  			default:
   107  			}
   108  		}
   109  		switch {
   110  		case len(v) != 0:
   111  			mmv = fmt.Sprintf("(devel) (%s)", strings.Join(v, ", "))
   112  		case mmv == ``:
   113  			mmv = `(devel)` // Not totally sure what else to put here.
   114  		}
   115  	case errors.Is(err, ErrInvalidSemVer):
   116  		badVers[bi.Main.Path] = bi.Main.Version
   117  		mmv = bi.Main.Version
   118  	default:
   119  		return fmt.Errorf("error parsing main version: %q: %w", bi.Main.Version, err)
   120  	}
   121  
   122  	// This substitution makes the results look like `go version -m` output.
   123  	name := bi.Main.Path
   124  	if name == "" {
   125  		name = "command-line-arguments"
   126  	}
   127  	*out = append(*out, &claircore.Package{
   128  		Kind:              claircore.BINARY,
   129  		PackageDB:         pkgdb,
   130  		Name:              name,
   131  		Version:           mmv,
   132  		Filepath:          p,
   133  		NormalizedVersion: mainVer,
   134  	})
   135  
   136  	if ev.Enabled() {
   137  		vs[bi.Main.Path] = bi.Main.Version
   138  	}
   139  	for _, d := range bi.Deps {
   140  		// Replacements are only evaluated for the main module and seem to only
   141  		// be evaluated once, so this shouldn't be recursive.
   142  		if r := d.Replace; r != nil {
   143  			d = r
   144  		}
   145  		nv, err := ParseVersion(d.Version)
   146  		switch {
   147  		case errors.Is(err, nil):
   148  		case errors.Is(err, ErrInvalidSemVer):
   149  			badVers[d.Path] = d.Version
   150  		default:
   151  			return fmt.Errorf("error parsing dep version: %q: %w", d.Version, err)
   152  		}
   153  
   154  		*out = append(*out, &claircore.Package{
   155  			Kind:              claircore.BINARY,
   156  			PackageDB:         pkgdb,
   157  			Name:              d.Path,
   158  			Version:           d.Version,
   159  			Filepath:          p,
   160  			NormalizedVersion: nv,
   161  		})
   162  
   163  		if ev.Enabled() {
   164  			vs[d.Path] = d.Version
   165  		}
   166  	}
   167  	ev.
   168  		Interface("versions", vs).
   169  		Msg("analyzed exe")
   170  	return nil
   171  }
   172  
   173  var versionRegex = regexp.MustCompile(`^v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?$`)
   174  var ErrInvalidSemVer = errors.New("invalid semantic version")
   175  
   176  // ParseVersion will return a claircore.Version of type semver given
   177  // a valid semantic version. If the string is not a valid semver it
   178  // will return an ErrInvalidSemVer.
   179  func ParseVersion(ver string) (c claircore.Version, err error) {
   180  	m := versionRegex.FindStringSubmatch(ver)
   181  	if m == nil {
   182  		err = ErrInvalidSemVer
   183  		return
   184  	}
   185  	if c.V[1], err = fitInt32(m[1]); err != nil {
   186  		return
   187  	}
   188  	if c.V[2], err = fitInt32(strings.TrimPrefix(m[2], ".")); err != nil {
   189  		return
   190  	}
   191  	if c.V[3], err = fitInt32(strings.TrimPrefix(m[3], ".")); err != nil {
   192  		return
   193  	}
   194  	c.Kind = "semver"
   195  	return
   196  }
   197  
   198  func fitInt32(seg string) (int32, error) {
   199  	if len(seg) > 9 {
   200  		// Technically 2147483647 is possible so this should be well within bounds.
   201  		// Slicing here to avoid any big.Int allocations at the expense of a little
   202  		// more accuracy.
   203  		seg = seg[:9]
   204  	}
   205  	if seg == "" {
   206  		return 0, nil
   207  	}
   208  	i, err := strconv.ParseInt(seg, 10, 32)
   209  	if err != nil {
   210  		return 0, err
   211  	}
   212  	return int32(i), nil
   213  }