diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go index ccdc01d9c7a..2aae6e8347c 100644 --- a/syft/pkg/cataloger/golang/cataloger.go +++ b/syft/pkg/cataloger/golang/cataloger.go @@ -5,10 +5,10 @@ package golang import ( "fmt" + "regexp" "strings" "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/event/monitor" @@ -17,6 +17,9 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) +// this regex is used +var versionCandidateGroups = regexp.MustCompile(`(?P\d+(\.\d+)?(\.\d+)?)(?P\w*)`) + // NewGoModFileCataloger returns a new Go module cataloger object. func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger { c := goModCataloger{ @@ -56,14 +59,16 @@ func (p *progressingCataloger) Catalog(resolver file.Resolver) ([]pkg.Package, [ 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) { - if mValue, ok := goPkg.Metadata.(pkg.GolangBinMetadata); ok { - stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations) - if stdLibPkg != nil { - goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg) - } + stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations) + if stdLibPkg != nil { + goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg) totalLocations.Add(location) } } @@ -73,22 +78,15 @@ func (p *progressingCataloger) Catalog(resolver file.Resolver) ([]pkg.Package, [ return pkgs, relationships, err } func newGoStdLib(version string, location file.LocationSet) *pkg.Package { - // for matching we need to strip the go prefix - // this can be preserved for metadata purposes - matchVersion := strings.TrimPrefix(version, "go") - cpes := make([]cpe.CPE, 0) - compilerCPE, err := cpe.New(fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", matchVersion)) + stdlibCpe, err := generateStdlibCpe(version) if err != nil { - log.Warn("could not build cpe for given compiler version: %s", version) return nil } - - cpes = append(cpes, compilerCPE) goCompilerPkg := &pkg.Package{ - Name: "Golang Standard Library", + Name: "stdlib", Version: version, - PURL: packageURL("stdlib", matchVersion), - CPEs: cpes, + PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")), + CPEs: []cpe.CPE{stdlibCpe}, Locations: location, Language: pkg.Go, Type: pkg.GoModulePkg, @@ -101,3 +99,31 @@ func newGoStdLib(version string, location file.LocationSet) *pkg.Package { 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 + 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 and + // 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) +} diff --git a/syft/pkg/cataloger/golang/cataloger_test.go b/syft/pkg/cataloger/golang/cataloger_test.go index 7323e9fa804..b1e26ba3517 100644 --- a/syft/pkg/cataloger/golang/cataloger_test.go +++ b/syft/pkg/cataloger/golang/cataloger_test.go @@ -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" ) @@ -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) + }) + } +}