Skip to content

Commit

Permalink
feat: add package for go compiler given binary detection (#2195)
Browse files Browse the repository at this point in the history
adds a unique synthetic package to the SBOM output that represents the go compiler when it is detected as a part of a package discovered by the go binary cataloger.

When using an SBOM generated by syft - downstream vulnerability scanners now have the opportunity to detect/report on the PURL/CPEs attached to the new stdlib package.
---------

Signed-off-by: Christopher Phillips <[email protected]>
  • Loading branch information
spiffcs authored Oct 6, 2023
1 parent 87e57aa commit f6c8057
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 4 deletions.
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

0 comments on commit f6c8057

Please sign in to comment.