mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +01:00
feat: add package for go compiler given binary detection (#2195)
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 <christopher.phillips@anchore.com>
This commit is contained in:
parent
87e57aa925
commit
f6c8057977
@ -4,14 +4,21 @@ Package golang provides a concrete Cataloger implementation for go.mod files.
|
|||||||
package golang
|
package golang
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/cpe"
|
||||||
"github.com/anchore/syft/syft/event/monitor"
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
"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.
|
// NewGoModFileCataloger returns a new Go module cataloger object.
|
||||||
func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
||||||
c := goModCataloger{
|
c := goModCataloger{
|
||||||
@ -47,5 +54,75 @@ func (p *progressingCataloger) Name() string {
|
|||||||
|
|
||||||
func (p *progressingCataloger) Catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
func (p *progressingCataloger) Catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
defer p.progress.SetCompleted()
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,9 @@ package golang
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cpe"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
"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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -62,6 +62,7 @@ func (c *goBinaryCataloger) parseGoBinary(resolver file.Resolver, _ *generic.Env
|
|||||||
for _, mod := range mods {
|
for _, mod := range mods {
|
||||||
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...)
|
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkgs, nil, nil
|
return pkgs, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
test/integration/go_compiler_detection_test.go
Normal file
62
test/integration/go_compiler_detection_test.go
Normal file
@ -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/stdlib@1.18.10"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
func TestRegressionGoArchDiscovery(t *testing.T) {
|
func TestRegressionGoArchDiscovery(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
expectedELFPkg = 4
|
expectedELFPkg = 5
|
||||||
expectedWINPkg = 4
|
expectedWINPkg = 5
|
||||||
expectedMACOSPkg = 4
|
expectedMACOSPkg = 5
|
||||||
)
|
)
|
||||||
// This is a regression test to make sure the way we detect go binary packages
|
// This is a regression test to make sure the way we detect go binary packages
|
||||||
// stays consistent and reproducible as the tool chain evolves
|
// stays consistent and reproducible as the tool chain evolves
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
FROM golang:1.18.10-alpine
|
||||||
Loading…
x
Reference in New Issue
Block a user