From 3e21379492e10396cb1a5f87758ece3337978def Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 3 May 2024 16:12:00 -0400 Subject: [PATCH] [wip] Signed-off-by: Alex Goodman --- .../parse_dotnet_portable_executable.go | 53 ++++++-- .../parse_dotnet_portable_executable_test.go | 123 ++++++++++++++++++ syft/pkg/dotnet.go | 17 ++- 3 files changed, 178 insertions(+), 15 deletions(-) diff --git a/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable.go b/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable.go index 3ec83f229..e2d37778f 100644 --- a/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable.go +++ b/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable.go @@ -69,13 +69,16 @@ func buildDotNetPackage(versionResources map[string]string, f file.LocationReadC } metadata := pkg.DotnetPortableExecutableEntry{ - AssemblyVersion: versionResources["Assembly Version"], - LegalCopyright: versionResources["LegalCopyright"], - Comments: versionResources["Comments"], - InternalName: versionResources["InternalName"], - CompanyName: versionResources["CompanyName"], - ProductName: versionResources["ProductName"], - ProductVersion: versionResources["ProductVersion"], + AssemblyVersion: versionResources["Assembly Version"], + LegalCopyright: versionResources["LegalCopyright"], + Comments: versionResources["Comments"], + InternalName: versionResources["InternalName"], + CompanyName: versionResources["CompanyName"], + ProductName: versionResources["ProductName"], + ProductVersion: versionResources["ProductVersion"], + FileDescription: versionResources["FileDescription"], + FileVersion: versionResources["FileVersion"], + OriginalFilename: versionResources["OriginalFilename"], } dnpkg = pkg.Package{ @@ -215,7 +218,7 @@ func findName(versionResources map[string]string) string { } for _, field := range nameFields { - value := spaceNormalize(versionResources[field]) + value := resolveValue(versionResources[field], versionResources, nil) if value == "" { continue } @@ -225,6 +228,40 @@ func findName(versionResources map[string]string) string { return "" } +func resolveValue(value string, collection map[string]string, visited map[string]bool) string { + value = spaceNormalize(value) + + if value == "" { + return "" + } + + if visited == nil { + visited = make(map[string]bool) + } + + if visited[value] { + return value + } + visited[value] = true + + hasIndirect, nextKey := hasIndirectFieldPrefix(value) + if !hasIndirect { + return value + } + + return resolveValue(collection[nextKey], collection, visited) +} + +func hasIndirectFieldPrefix(value string) (bool, string) { + for _, prefix := range []string{"f#", "p("} { + cleanValue := strings.TrimPrefix(value, prefix) + if cleanValue != value { + return true, cleanValue + } + } + return false, value +} + // normalizes a string to a trimmed version with all contigous whitespace collapsed to a single space character func spaceNormalize(value string) string { value = strings.TrimSpace(value) diff --git a/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable_test.go b/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable_test.go index 4d915a50d..aa528b22b 100644 --- a/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable_test.go +++ b/syft/pkg/cataloger/dotnet/parse_dotnet_portable_executable_test.go @@ -346,3 +346,126 @@ func Test_spaceNormalize(t *testing.T) { }) } } + +func Test_resolveValue(t *testing.T) { + + tests := []struct { + name string + value string + collection map[string]string + want string + }{ + { + name: "simple value", + value: "value", + collection: map[string]string{ + "key": "value", + }, + want: "value", + }, + { + name: "simple value with spaces", + value: " value ", + collection: map[string]string{ + "key": "value", + }, + want: "value", + }, + { + name: "indirect value - f#", + value: "f#other", + collection: map[string]string{ + "key": "f#other", + "other": "value", + }, + want: "value", + }, + { + name: "indirect value - p(", + value: "f#other", + collection: map[string]string{ + "key": "p(other", + "other": "value", + }, + want: "value", + }, + { + name: "indirect value with cycles", + value: "f#other", + collection: map[string]string{ + "key": "f#other", + "other": "f#key", + }, + want: "f#other", // this is NOT ideal, but there is no "good" answer here + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, resolveValue(tt.value, tt.collection, nil)) + }) + } +} + +func Test_findVersion(t *testing.T) { + + tests := []struct { + name string + versionResources map[string]string + want string + }{ + { + name: "prefer file version over product version (when both semver)", + versionResources: map[string]string{ + "FileVersion": "1.2.3", + "ProductVersion": "4.5.6", + }, + want: "4.5.6", + }, + { + name: "prefer file version over product version (when both semver)", + versionResources: map[string]string{ + "FileVersion": "1.2.3", + "ProductVersion": "4.5.6.7", + }, + want: "1.2.3", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, findVersion(tt.versionResources)) + }) + } +} + +func Test_keepGreaterSemanticVersion(t *testing.T) { + tests := []struct { + name string + productVersion string + fileVersion string + want string + }{ + { + name: "product semver is greater", + productVersion: "3.0.0", + fileVersion: "2.0.0", + want: "3.0.0", + }, + { + name: "file semver is greater", + productVersion: "2.0.0", + fileVersion: "3.0.0", + want: "3.0.0", + }, + { + name: "semver preferred over non-semver", + productVersion: "3.0.0.2", + fileVersion: "3.0.0", + want: "3.0.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, keepGreaterSemanticVersion(tt.productVersion, tt.fileVersion)) + }) + } +} diff --git a/syft/pkg/dotnet.go b/syft/pkg/dotnet.go index 4477ac01b..70ffcb926 100644 --- a/syft/pkg/dotnet.go +++ b/syft/pkg/dotnet.go @@ -11,11 +11,14 @@ type DotnetDepsEntry struct { // DotnetPortableExecutableEntry is a struct that represents a single entry found within "VersionResources" section of a .NET Portable Executable binary file. type DotnetPortableExecutableEntry struct { - AssemblyVersion string `json:"assemblyVersion"` - LegalCopyright string `json:"legalCopyright"` - Comments string `json:"comments,omitempty"` - InternalName string `json:"internalName,omitempty"` - CompanyName string `json:"companyName"` - ProductName string `json:"productName"` - ProductVersion string `json:"productVersion"` + AssemblyVersion string `json:"assemblyVersion"` + LegalCopyright string `json:"legalCopyright"` + Comments string `json:"comments,omitempty"` + InternalName string `json:"internalName,omitempty"` + CompanyName string `json:"companyName"` + ProductName string `json:"productName"` + ProductVersion string `json:"productVersion"` + FileVersion string `json:"fileVersion,omitempty"` + FileDescription string `json:"fileDescription,omitempty"` + OriginalFilename string `json:"originalFilename,omitempty"` }