Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add package for go compiler given binary detection #2195

Merged
merged 12 commits into from
Oct 6, 2023
Merged
79 changes: 78 additions & 1 deletion syft/pkg/cataloger/golang/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ Package golang provides a concrete Cataloger implementation for go.mod files.
package golang

import (
"fmt"
"regexp"
"strings"

"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)

var versionCandidateGroups = regexp.MustCompile(`(?P<version>\d+(\.\d+)?(\.\d+)?)(?P<candidate>\w*)`)

// NewGoModFileCataloger returns a new Go module cataloger object.
func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger {
c := goModCataloger{
Expand Down Expand Up @@ -47,5 +54,75 @@ func (p *progressingCataloger) Name() string {

func (p *progressingCataloger) Catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
defer p.progress.SetCompleted()
return p.cataloger.Catalog(resolver)
pkgs, relationships, err := p.cataloger.Catalog(resolver)
goCompilerPkgs := []pkg.Package{}
totalLocations := file.NewLocationSet()
for _, goPkg := range pkgs {
mValue, ok := goPkg.Metadata.(pkg.GolangBinMetadata)
if !ok {
continue
}
// go binary packages should only contain a single location
for _, location := range goPkg.Locations.ToSlice() {
if !totalLocations.Contains(location) {
stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations)
if stdLibPkg != nil {
goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg)
totalLocations.Add(location)
}
}
}
}
pkgs = append(pkgs, goCompilerPkgs...)
return pkgs, relationships, err
}
func newGoStdLib(version string, location file.LocationSet) *pkg.Package {
stdlibCpe, err := generateStdlibCpe(version)
if err != nil {
return nil
}
goCompilerPkg := &pkg.Package{
Name: "stdlib",
Version: version,
PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")),
CPEs: []cpe.CPE{stdlibCpe},
Locations: location,
Language: pkg.Go,
Type: pkg.GoModulePkg,
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
GoCompiledVersion: version,
},
}
goCompilerPkg.SetID()

return goCompilerPkg
}

func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) {
// GoCompiledVersion when pulled from a binary is prefixed by go
version = strings.TrimPrefix(version, "go")

// we also need to trim starting from the first +<metadata> to
// correctly extract potential rc candidate information for cpe generation
// ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned
after, _, found := strings.Cut("+", version)
if found {
version = after
}

// extracting <version> and <candidate>
// https://regex101.com/r/985GsI/1
captureGroups := internal.MatchNamedCaptureGroups(versionCandidateGroups, version)
vr, ok := captureGroups["version"]
if !ok || vr == "" {
return stdlibCpe, fmt.Errorf("could not match candidate version for: %s", version)
}

cpeString := fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", captureGroups["version"])
if candidate, ok := captureGroups["candidate"]; ok && candidate != "" {
cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate)
}

return cpe.New(cpeString)
}
30 changes: 30 additions & 0 deletions syft/pkg/cataloger/golang/cataloger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package golang
import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)

Expand Down Expand Up @@ -56,3 +59,30 @@ func Test_Binary_Cataloger_Globs(t *testing.T) {
})
}
}

func Test_Binary_Cataloger_Stdlib_Cpe(t *testing.T) {
tests := []struct {
name string
candidate string
want string
}{
{
name: "generateStdlibCpe generates a cpe with a - for a major version",
candidate: "go1.21.0",
want: "cpe:2.3:a:golang:go:1.21.0:-:*:*:*:*:*:*",
},
{
name: "generateStdlibCpe generates a cpe with an rc candidate for a major rc version",
candidate: "go1.21rc2",
want: "cpe:2.3:a:golang:go:1.21:rc2:*:*:*:*:*:*",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := generateStdlibCpe(tc.candidate)
assert.NoError(t, err, "expected no err; got %v", err)
assert.Equal(t, cpe.String(got), tc.want)
})
}
}
1 change: 1 addition & 0 deletions syft/pkg/cataloger/golang/parse_go_binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func (c *goBinaryCataloger) parseGoBinary(resolver file.Resolver, _ *generic.Env
for _, mod := range mods {
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...)
}

return pkgs, nil, nil
}

Expand Down
62 changes: 62 additions & 0 deletions test/integration/go_compiler_detection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package integration

import (
"testing"

"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/source"
)

func TestGolangCompilerDetection(t *testing.T) {
tests := []struct {
name string
image string
expectedCompilers []string
expectedCPE []cpe.CPE
expectedPURL []string
}{
{
name: "syft can detect a single golang compiler given the golang base image",
image: "image-golang-compiler",
expectedCompilers: []string{"go1.18.10"},
expectedCPE: []cpe.CPE{cpe.Must("cpe:2.3:a:golang:go:1.18.10:-:*:*:*:*:*:*")},
expectedPURL: []string{"pkg:golang/[email protected]"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sbom, _ := catalogFixtureImage(t, tt.image, source.SquashedScope, nil)
packages := sbom.Artifacts.Packages.PackagesByName("stdlib")

foundCompilerVersions := make(map[string]struct{})
foundCPE := make(map[cpe.CPE]struct{})
foundPURL := make(map[string]struct{})

for _, pkg := range packages {
foundCompilerVersions[pkg.Version] = struct{}{}
foundPURL[pkg.PURL] = struct{}{}
for _, cpe := range pkg.CPEs {
foundCPE[cpe] = struct{}{}
}
}

for _, expectedCompiler := range tt.expectedCompilers {
if _, ok := foundCompilerVersions[expectedCompiler]; !ok {
t.Fatalf("expected %s version; not found in found compilers: %v", expectedCompiler, foundCompilerVersions)
}
}

for _, expectedPURL := range tt.expectedPURL {
if _, ok := foundPURL[expectedPURL]; !ok {
t.Fatalf("expected %s purl; not found in found purl: %v", expectedPURL, expectedPURLs)
}
}

for _, expectedCPE := range tt.expectedCPE {
if _, ok := foundCPE[expectedCPE]; !ok {
t.Fatalf("expected %s version; not found in found cpe: %v", expectedCPE, expectedCPE)
}
}
})
}
}
6 changes: 3 additions & 3 deletions test/integration/regression_go_bin_scanner_arch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (

func TestRegressionGoArchDiscovery(t *testing.T) {
const (
expectedELFPkg = 4
expectedWINPkg = 4
expectedMACOSPkg = 4
expectedELFPkg = 5
expectedWINPkg = 5
expectedMACOSPkg = 5
)
// This is a regression test to make sure the way we detect go binary packages
// stays consistent and reproducible as the tool chain evolves
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM golang:1.18.10-alpine