github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/pkg/cataloger/golang/parse_go_binary.go (about)

     1  package golang
     2  
     3  import (
     4  	"bytes"
     5  	"debug/elf"
     6  	"debug/macho"
     7  	"debug/pe"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"regexp"
    12  	"runtime/debug"
    13  	"strings"
    14  	"time"
    15  
    16  	"golang.org/x/mod/module"
    17  
    18  	"github.com/anchore/syft/syft/artifact"
    19  	"github.com/anchore/syft/syft/file"
    20  	"github.com/anchore/syft/syft/pkg"
    21  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    22  	"github.com/lineaje-labs/syft/internal"
    23  	"github.com/lineaje-labs/syft/syft/pkg/cataloger/golang/internal/xcoff"
    24  	"github.com/lineaje-labs/syft/syft/pkg/cataloger/internal/unionreader"
    25  )
    26  
    27  const GOARCH = "GOARCH"
    28  
    29  var (
    30  	// errUnrecognizedFormat is returned when a given executable file doesn't
    31  	// appear to be in a known format, or it breaks the rules of that format,
    32  	// or when there are I/O errors reading the file.
    33  	errUnrecognizedFormat = errors.New("unrecognized file format")
    34  	// devel is used to recognize the current default version when a golang main distribution is built
    35  	// https://github.com/golang/go/issues/29228 this issue has more details on the progress of being able to
    36  	// inject the correct version into the main module of the build process
    37  
    38  	knownBuildFlagPatterns = []*regexp.Regexp{
    39  		regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]ersion=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
    40  		regexp.MustCompile(`(?m)\.([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
    41  	}
    42  )
    43  
    44  const devel = "(devel)"
    45  
    46  type goBinaryCataloger struct {
    47  	licenses goLicenses
    48  }
    49  
    50  // parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler.
    51  func (c *goBinaryCataloger) parseGoBinary(
    52  	resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser,
    53  ) ([]pkg.Package, []artifact.Relationship, error) {
    54  	var pkgs []pkg.Package
    55  
    56  	unionReader, err := unionreader.GetUnionReader(reader.ReadCloser)
    57  	if err != nil {
    58  		return nil, nil, err
    59  	}
    60  
    61  	mods := scanFile(unionReader, reader.RealPath)
    62  	internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
    63  
    64  	for _, mod := range mods {
    65  		pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...)
    66  	}
    67  
    68  	return pkgs, nil, nil
    69  }
    70  
    71  func (c *goBinaryCataloger) makeGoMainPackage(
    72  	resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location,
    73  ) pkg.Package {
    74  	gbs := getBuildSettings(mod.Settings)
    75  	main := c.newGoBinaryPackage(
    76  		resolver,
    77  		&mod.Main,
    78  		mod.Main.Path,
    79  		mod.GoVersion,
    80  		arch,
    81  		gbs,
    82  		mod.cryptoSettings,
    83  		location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
    84  	)
    85  
    86  	if main.Version != devel {
    87  		return main
    88  	}
    89  
    90  	version, hasVersion := gbs["vcs.revision"]
    91  	timestamp, hasTimestamp := gbs["vcs.time"]
    92  
    93  	var ldflags string
    94  	if metadata, ok := main.Metadata.(pkg.GolangBinaryBuildinfoEntry); ok {
    95  		// we've found a specific version from the ldflags! use it as the version.
    96  		// why not combine that with the pseudo version (e.g. v1.2.3-0.20210101000000-abcdef123456)?
    97  		// short answer: we're assuming that if a specific semver was provided in the ldflags that
    98  		// there is a matching vcs tag to match that could be referenced. This assumption could
    99  		// be incorrect in terms of the go.mod contents, but is not incorrect in terms of the logical
   100  		// version of the package.
   101  		ldflags = metadata.BuildSettings["-ldflags"]
   102  	}
   103  
   104  	majorVersion, fullVersion := extractVersionFromLDFlags(ldflags)
   105  	if fullVersion != "" {
   106  		version = fullVersion
   107  	} else if hasVersion && hasTimestamp {
   108  		// NOTE: err is ignored, because if parsing fails
   109  		// we still use the empty Time{} struct to generate an empty date, like 00010101000000
   110  		// for consistency with the pseudo-version format: https://go.dev/ref/mod#pseudo-versions
   111  		ts, _ := time.Parse(time.RFC3339, timestamp)
   112  		if len(version) >= 12 {
   113  			version = version[:12]
   114  		}
   115  
   116  		version = module.PseudoVersion(majorVersion, fullVersion, ts, version)
   117  	}
   118  	if version != "" {
   119  		main.Version = version
   120  		main.PURL = packageURL(main.Name, main.Version)
   121  
   122  		main.SetID()
   123  	}
   124  
   125  	return main
   126  }
   127  
   128  func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) {
   129  	if ldflags == "" {
   130  		return "", ""
   131  	}
   132  
   133  	for _, pattern := range knownBuildFlagPatterns {
   134  		groups := internal.MatchNamedCaptureGroups(pattern, ldflags)
   135  		v, ok := groups["version"]
   136  
   137  		if !ok {
   138  			continue
   139  		}
   140  
   141  		fullVersion = v
   142  		if !strings.HasPrefix(v, "v") {
   143  			fullVersion = fmt.Sprintf("v%s", v)
   144  		}
   145  		components := strings.Split(v, ".")
   146  
   147  		if len(components) == 0 {
   148  			continue
   149  		}
   150  
   151  		majorVersion = strings.TrimPrefix(components[0], "v")
   152  		return majorVersion, fullVersion
   153  	}
   154  
   155  	return "", ""
   156  }
   157  
   158  func getGOARCH(settings []debug.BuildSetting) string {
   159  	for _, s := range settings {
   160  		if s.Key == GOARCH {
   161  			return s.Value
   162  		}
   163  	}
   164  
   165  	return ""
   166  }
   167  
   168  func getGOARCHFromBin(r io.ReaderAt) (string, error) {
   169  	// Read the first bytes of the file to identify the format, then delegate to
   170  	// a format-specific function to load segment and section headers.
   171  	ident := make([]byte, 16)
   172  	if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil {
   173  		return "", fmt.Errorf("unrecognized file format: %w", err)
   174  	}
   175  
   176  	var arch string
   177  	switch {
   178  	case bytes.HasPrefix(ident, []byte("\x7FELF")):
   179  		f, err := elf.NewFile(r)
   180  		if err != nil {
   181  			return "", fmt.Errorf("unrecognized file format: %w", err)
   182  		}
   183  		arch = f.Machine.String()
   184  	case bytes.HasPrefix(ident, []byte("MZ")):
   185  		f, err := pe.NewFile(r)
   186  		if err != nil {
   187  			return "", fmt.Errorf("unrecognized file format: %w", err)
   188  		}
   189  		arch = fmt.Sprintf("%d", f.Machine)
   190  	case bytes.HasPrefix(ident, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(ident[1:], []byte("\xFA\xED\xFE")):
   191  		f, err := macho.NewFile(r)
   192  		if err != nil {
   193  			return "", fmt.Errorf("unrecognized file format: %w", err)
   194  		}
   195  		arch = f.Cpu.String()
   196  	case bytes.HasPrefix(ident, []byte{0x01, 0xDF}) || bytes.HasPrefix(ident, []byte{0x01, 0xF7}):
   197  		f, err := xcoff.NewFile(r)
   198  		if err != nil {
   199  			return "", fmt.Errorf("unrecognized file format: %w", err)
   200  		}
   201  		arch = fmt.Sprintf("%d", f.FileHeader.TargetMachine)
   202  	default:
   203  		return "", errUnrecognizedFormat
   204  	}
   205  
   206  	arch = strings.Replace(arch, "EM_", "", 1)
   207  	arch = strings.Replace(arch, "Cpu", "", 1)
   208  	arch = strings.ToLower(arch)
   209  
   210  	return arch, nil
   211  }
   212  
   213  func getBuildSettings(settings []debug.BuildSetting) map[string]string {
   214  	m := make(map[string]string)
   215  	for _, s := range settings {
   216  		m[s.Key] = s.Value
   217  	}
   218  	return m
   219  }
   220  
   221  func createMainModuleFromPath(path string) (mod debug.Module) {
   222  	mod.Path = path
   223  	mod.Version = devel
   224  	return
   225  }
   226  
   227  func (c *goBinaryCataloger) buildGoPkgInfo(
   228  	resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string,
   229  ) []pkg.Package {
   230  	var pkgs []pkg.Package
   231  	if mod == nil {
   232  		return pkgs
   233  	}
   234  
   235  	var empty debug.Module
   236  	if mod.Main == empty && mod.Path != "" {
   237  		mod.Main = createMainModuleFromPath(mod.Path)
   238  	}
   239  
   240  	for _, dep := range mod.Deps {
   241  		if dep == nil {
   242  			continue
   243  		}
   244  		p := c.newGoBinaryPackage(
   245  			resolver,
   246  			dep,
   247  			mod.Main.Path,
   248  			mod.GoVersion,
   249  			arch,
   250  			nil,
   251  			mod.cryptoSettings,
   252  			location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
   253  		)
   254  		if pkg.IsValid(&p) {
   255  			pkgs = append(pkgs, p)
   256  		}
   257  	}
   258  
   259  	if mod.Main == empty {
   260  		return pkgs
   261  	}
   262  
   263  	main := c.makeGoMainPackage(resolver, mod, arch, location)
   264  	pkgs = append(pkgs, main)
   265  
   266  	return pkgs
   267  }