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