diff --git a/syft/pkg/cataloger/binary/pe_package.go b/syft/pkg/cataloger/binary/pe_package.go index a7cab30e272..4c19b649523 100644 --- a/syft/pkg/cataloger/binary/pe_package.go +++ b/syft/pkg/cataloger/binary/pe_package.go @@ -15,6 +15,10 @@ var ( // spaceRegex includes nbsp (#160) considered to be a space character spaceRegex = regexp.MustCompile(`[\s\xa0]+`) numberRegex = regexp.MustCompile(`\d`) + // commaSeparatedVersionRegex matches the comma-separated form that Windows + // VERSIONINFO resources often carry (mirroring the FILEVERSION x,y,z,w + // declaration in a .rc file), e.g. "3, 0, 21, 0" + commaSeparatedVersionRegex = regexp.MustCompile(`^\d+(?:\s*,\s*\d+)+$`) ) func newPEPackage(versionResources map[string]string, f file.Location) pkg.Package { @@ -129,6 +133,7 @@ func findVersionFromVR(versionResources map[string]string) string { func extractVersionFromResourcesValue(version string) string { version = strings.TrimSpace(version) + version = normalizeCommaSeparatedVersion(version) out := "" for i, f := range strings.Fields(version) { if containsNumber(out) && !containsNumber(f) { @@ -143,6 +148,23 @@ func extractVersionFromResourcesValue(version string) string { return out } +// normalizeCommaSeparatedVersion converts the comma-separated version form used +// by Windows VERSIONINFO resources into the canonical dotted form, e.g. +// "3, 0, 21, 0" becomes "3.0.21.0". Values that are not entirely made up of +// comma-separated numbers are returned unchanged. +func normalizeCommaSeparatedVersion(version string) string { + if !commaSeparatedVersionRegex.MatchString(version) { + return version + } + + fields := strings.Split(version, ",") + for i := range fields { + fields[i] = strings.TrimSpace(fields[i]) + } + + return strings.Join(fields, ".") +} + func containsNumber(s string) bool { return numberRegex.MatchString(s) } diff --git a/syft/pkg/cataloger/binary/pe_package_test.go b/syft/pkg/cataloger/binary/pe_package_test.go index cf5746317bb..9e23a8d9491 100644 --- a/syft/pkg/cataloger/binary/pe_package_test.go +++ b/syft/pkg/cataloger/binary/pe_package_test.go @@ -3,6 +3,8 @@ package binary import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/file" ) @@ -22,3 +24,44 @@ func TestGhostscriptPEGeneratesGenericPURL(t *testing.T) { t.Fatalf("expected purl %q, got %q", expected, p.PURL) } } + +func Test_findVersionFromVR(t *testing.T) { + tests := []struct { + name string + versionResources map[string]string + want string + }{ + { + name: "dotted version is kept as-is", + versionResources: map[string]string{"ProductVersion": "3.0.21.0"}, + want: "3.0.21.0", + }, + { + name: "comma separated version is normalized", + // VERSIONINFO resources often mirror the .rc FILEVERSION 3,0,21,0 form + versionResources: map[string]string{"ProductVersion": "3, 0, 21, 0"}, + want: "3.0.21.0", + }, + { + name: "comma separated version without spaces is normalized", + versionResources: map[string]string{"FileVersion": "3,0,21,0"}, + want: "3.0.21.0", + }, + { + name: "version with trailing comment is unaffected", + versionResources: map[string]string{"ProductVersion": "1.2.3 (build 456)"}, + want: "1.2.3", + }, + { + name: "non numeric comma separated value is kept as-is", + versionResources: map[string]string{"ProductVersion": "1.0, beta"}, + want: "1.0,", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, findVersionFromVR(tt.versionResources)) + }) + } +}