fix: more robust go main version extraction (#2767)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
Signed-off-by: Will Murphy <will.murphy@anchore.com>
Co-authored-by: Will Murphy <will.murphy@anchore.com>
This commit is contained in:
Keith Zantow 2024-04-11 11:58:51 -04:00 committed by GitHub
parent a5d77b9263
commit dde5d349b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 12 deletions

View File

@ -151,7 +151,10 @@ func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *exten
return main return main
} }
var semverPattern = regexp.MustCompile(`\x00(?P<version>v?(\d+\.\d+\.\d+[-\w]*[+\w]*))\x00`) // this is checking for (.L)? because at least one binary seems to have \xA0L preceding the version string, but for some reason
// this is unable to be matched by the regex here as \x00\xA0L;
// the only thing that seems to work is to just look for version strings following both \x00 and \x00.L for now
var semverPattern = regexp.MustCompile(`\x00(.L)?(?P<version>v?(\d+\.\d+\.\d+[-\w]*[+\w]*))\x00`)
func (c *goBinaryCataloger) findMainModuleVersion(metadata *pkg.GolangBinaryBuildinfoEntry, gbs pkg.KeyValues, reader io.ReadSeekCloser) string { func (c *goBinaryCataloger) findMainModuleVersion(metadata *pkg.GolangBinaryBuildinfoEntry, gbs pkg.KeyValues, reader io.ReadSeekCloser) string {
vcsVersion, hasVersion := gbs.Get("vcs.revision") vcsVersion, hasVersion := gbs.Get("vcs.revision")
@ -179,16 +182,8 @@ func (c *goBinaryCataloger) findMainModuleVersion(metadata *pkg.GolangBinaryBuil
if err != nil { if err != nil {
log.WithFields("error", err).Trace("unable to seek to start of go binary reader") log.WithFields("error", err).Trace("unable to seek to start of go binary reader")
} else { } else {
contents, err := io.ReadAll(reader) if v := extractVersionFromContents(reader); v != "" {
if err != nil { return v
log.WithFields("error", err).Trace("unable to read from go binary reader")
} else {
matchMetadata := internal.MatchNamedCaptureGroups(semverPattern, string(contents))
version, ok := matchMetadata["version"]
if ok {
return version
}
} }
} }
} }
@ -210,6 +205,21 @@ func (c *goBinaryCataloger) findMainModuleVersion(metadata *pkg.GolangBinaryBuil
return "" return ""
} }
func extractVersionFromContents(reader io.Reader) string {
contents, err := io.ReadAll(reader)
if err != nil {
log.WithFields("error", err).Trace("unable to read from go binary reader")
return ""
}
matchMetadata := internal.MatchNamedCaptureGroups(semverPattern, string(contents))
version, ok := matchMetadata["version"]
if ok {
return version
}
return ""
}
func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) { func extractVersionFromLDFlags(ldflags string) (majorVersion string, fullVersion string) {
if ldflags == "" { if ldflags == "" {
return "", "" return "", ""

View File

@ -2,6 +2,8 @@ package golang
import ( import (
"bufio" "bufio"
"bytes"
"errors"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@ -1090,3 +1092,53 @@ func Test_extractVersionFromLDFlags(t *testing.T) {
}) })
} }
} }
func Test_extractVersionFromContents(t *testing.T) {
tests := []struct {
name string
contents io.Reader
want string
}{
{
name: "empty string on error",
contents: &alwaysErrorReader{},
want: "",
},
{
name: "empty string on empty reader",
contents: bytes.NewReader([]byte{}),
want: "",
},
{
name: "null-byte delimited semver",
contents: strings.NewReader("\x001.2.3\x00"),
want: "1.2.3",
},
{
name: "null-byte delimited semver with v prefix",
contents: strings.NewReader("\x00v1.2.3\x00"),
want: "v1.2.3",
},
{
// 01a0bfc8: 0e74 5a3b 0000 a04c 7631 2e39 2e35 0000 .tZ;...Lv1.9.5.. from nginx-ingress-controller
// at /nginx-ingress-controller in registry.k8s.io/ingress-nginx/controller:v1.9.5
// digest: sha256:b3aba22b1da80e7acfc52b115cae1d4c687172cbf2b742d5b502419c25ff340e
// TODO: eventually use something for managing snippets, similar to what's used with binary classifier tests
name: "null byte, then random byte, then L then semver",
contents: strings.NewReader("\x0e\x74\x5a\x3b\x00\x00\xa0\x4cv1.9.5\x00\x00"),
want: "v1.9.5",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractVersionFromContents(tt.contents)
assert.Equal(t, tt.want, got)
})
}
}
type alwaysErrorReader struct{}
func (alwaysErrorReader) Read(_ []byte) (int, error) {
return 0, errors.New("read from always error reader")
}

View File

@ -5,12 +5,13 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/elliotchance/phpserialize"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"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"
"github.com/elliotchance/phpserialize"
) )
// parsePeclSerialized is a parser function for PECL metadata contents, returning "Default" php packages discovered. // parsePeclSerialized is a parser function for PECL metadata contents, returning "Default" php packages discovered.