syft/syft/pkg/cataloger/dotnet/package_test.go
Alex Goodman c53f2fbad3
Better represent .NET runtime packages (#3768)
* clean up .NET runtime packages

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add runtime relationships

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove runtime references from binary package name

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-03-28 13:36:27 -04:00

562 lines
15 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package dotnet
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func Test_getDepsJSONFilePrefix(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{
name: "windows-style full path",
path: `C:\Code\Projects\My-Project\My.Rest.Project.deps.json`,
want: "My.Rest.Project",
},
{
name: "leading backslash",
path: `\My.Project.deps.json`,
want: "My.Project",
},
{
name: "unix-style path with lots of prefixes",
path: "/my/cool/project/cool-project.deps.json",
want: "cool-project",
},
{
name: "unix-style relative path",
path: "cool-project/my-dotnet-project.deps.json",
want: "my-dotnet-project",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, getDepsJSONFilePrefix(tt.path), "getDepsJSONFilePrefix(%v)", tt.path)
})
}
}
func Test_NewDotnetBinaryPackage(t *testing.T) {
tests := []struct {
name string
versionResources map[string]string
expectedPackage pkg.Package
}{
{
name: "dotnet package with extra version info",
versionResources: map[string]string{
"InternalName": "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
"FileVersion": "3.14.40721.0918 xxxfffdddjjjj",
"FileDescription": "Active Directory Authentication Library",
"ProductName": "Active Directory Authentication Library",
"Comments": "",
"CompanyName": "Microsoft Corporation",
"LegalTrademarks": "",
"LegalCopyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
"OriginalFilename": "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
"ProductVersion": "c61f043686a544863efc014114c42e844f905336",
"Assembly Version": "3.14.2.11",
},
expectedPackage: pkg.Package{
Name: "Active Directory Authentication Library",
Version: "3.14.40721.0918",
Metadata: pkg.DotnetPortableExecutableEntry{
AssemblyVersion: "3.14.2.11",
LegalCopyright: "Copyright (c) Microsoft Corporation. All rights reserved.",
InternalName: "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll",
CompanyName: "Microsoft Corporation",
ProductName: "Active Directory Authentication Library",
ProductVersion: "c61f043686a544863efc014114c42e844f905336",
},
},
},
{
// show we can do a best effort to make a package from bad data
name: "dotnet package with malformed field and extended version",
versionResources: map[string]string{
"CompanyName": "Microsoft Corporation",
"FileDescription": "äbFile\xa0\xa1Versi on",
"FileVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
"InternalName": "äbFileVersion",
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
"OriginalFilename": "TProductName",
"ProductName": "Microsoft® .NET Framework",
"ProductVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
},
expectedPackage: pkg.Package{
Name: "äbFileVersi on",
Version: "4.6.25512.01",
PURL: "pkg:nuget/%C3%A4bFileVersi%20on@4.6.25512.01",
Metadata: pkg.DotnetPortableExecutableEntry{
LegalCopyright: "© Microsoft Corporation. All rights reserved.",
InternalName: "äb\x01FileVersion",
CompanyName: "Microsoft Corporation",
ProductName: "Microsoft® .NET Framework",
ProductVersion: "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
},
},
},
{
name: "System.Data.Linq.dll",
versionResources: map[string]string{
"CompanyName": "Microsoft Corporation",
"FileDescription": "System.Data.Linq.dll",
"FileVersion": "4.7.3190.0 built by: NET472REL1LAST_C",
"InternalName": "System.Data.Linq.dll",
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
"OriginalFilename": "System.Data.Linq.dll",
"ProductName": "Microsoft® .NET Framework",
"ProductVersion": "4.7.3190.0",
},
expectedPackage: pkg.Package{
Name: "System.Data.Linq.dll",
Version: "4.7.3190.0",
},
},
{
name: "curl",
versionResources: map[string]string{
"CompanyName": "curl, https://curl.se/",
"FileDescription": "The curl executable",
"FileVersion": "8.4.0",
"InternalName": "curl",
"LegalCopyright": "© Daniel Stenberg, <daniel@haxx.se>.",
"OriginalFilename": "curl.exe",
"ProductName": "The curl executable",
"ProductVersion": "8.4.0",
},
expectedPackage: pkg.Package{
Name: "The curl executable",
Version: "8.4.0",
},
},
{
name: "Prometheus",
versionResources: map[string]string{
"AssemblyVersion": "8.0.0.0",
"CompanyName": "",
"FileDescription": "",
"FileVersion": "8.0.1",
"InternalName": "Prometheus.AspNetCore.dll",
"OriginalFilename": "Prometheus.AspNetCore.dll",
"ProductName": "",
"ProductVersion": "8.0.1",
},
expectedPackage: pkg.Package{
Name: "Prometheus.AspNetCore.dll",
Version: "8.0.1",
},
},
{
name: "Hidden Input",
versionResources: map[string]string{
"FileDescription": "Reads from stdin without leaking info to the terminal and outputs back to stdout",
"FileVersion": "1, 0, 0, 0",
"InternalName": "hiddeninput",
"LegalCopyright": "Jordi Boggiano - 2012",
"OriginalFilename": "hiddeninput.exe",
"ProductName": "Hidden Input",
"ProductVersion": "1, 0, 0, 0",
},
expectedPackage: pkg.Package{
Name: "Hidden Input",
Version: "1, 0, 0, 0",
},
},
{
name: "SQLite3",
versionResources: map[string]string{
"CompanyName": "SQLite Development Team",
"FileDescription": "SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.",
"FileVersion": "3.23.2",
"InternalName": "sqlite3",
"LegalCopyright": "http://www.sqlite.org/copyright.html",
"ProductName": "SQLite",
"ProductVersion": "3.23.2",
},
expectedPackage: pkg.Package{
Name: "SQLite",
Version: "3.23.2",
},
},
{
name: "Brave Browser",
versionResources: map[string]string{
"CompanyName": "Brave Software, Inc.",
"FileDescription": "Brave Browser",
"FileVersion": "80.1.7.92",
"InternalName": "chrome_exe",
"LegalCopyright": "Copyright 2016 The Brave Authors. All rights reserved.",
"OriginalFilename": "chrome.exe",
"ProductName": "Brave Browser",
"ProductVersion": "80.1.7.92",
},
expectedPackage: pkg.Package{
Name: "Brave Browser",
Version: "80.1.7.92",
},
},
{
name: "Better product version",
versionResources: map[string]string{
"FileDescription": "Better version",
"FileVersion": "80.1.7",
"ProductVersion": "80.1.7.92",
},
expectedPackage: pkg.Package{
Name: "Better version",
Version: "80.1.7.92",
},
},
{
name: "Better file version",
versionResources: map[string]string{
"FileDescription": "Better version",
"FileVersion": "80.1.7.92",
"ProductVersion": "80.1.7",
},
expectedPackage: pkg.Package{
Name: "Better version",
Version: "80.1.7.92",
},
},
{
name: "Higher semantic version Product Version",
versionResources: map[string]string{
"FileDescription": "Higher semantic version Product Version",
"FileVersion": "3.0.0.0",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Higher semantic version Product Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Higher semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Higher semantic version File Version",
"FileVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
"ProductVersion": "3.0.0",
},
expectedPackage: pkg.Package{
Name: "Higher semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version File Version",
"FileVersion": "A",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version File Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version File Version",
"FileVersion": "A",
"ProductVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version File Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Invalid semantic version Product Version",
versionResources: map[string]string{
"FileDescription": "Invalid semantic version Product Version",
"FileVersion": "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
"ProductVersion": "A",
},
expectedPackage: pkg.Package{
Name: "Invalid semantic version Product Version",
Version: "3.0.1+b86b61bf676163639795b163d8d753b20aad6207",
},
},
{
name: "Semantically equal falls through, chooses File Version with more components",
versionResources: map[string]string{
"FileDescription": "Semantically equal falls through, chooses File Version with more components",
"FileVersion": "3.0.0.0",
"ProductVersion": "3.0.0",
},
expectedPackage: pkg.Package{
Name: "Semantically equal falls through, chooses File Version with more components",
Version: "3.0.0.0",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
location := file.NewLocation("")
got := newDotnetBinaryPackage(tc.versionResources, location)
// ignore certain metadata
if tc.expectedPackage.Metadata == nil {
got.Metadata = nil
}
// set known defaults
if tc.expectedPackage.Type == "" {
tc.expectedPackage.Type = pkg.DotnetPkg
}
if tc.expectedPackage.Language == "" {
tc.expectedPackage.Language = pkg.Dotnet
}
if tc.expectedPackage.PURL == "" {
tc.expectedPackage.PURL = binaryPackageURL(tc.expectedPackage.Name, tc.expectedPackage.Version)
}
tc.expectedPackage.Locations = file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
pkgtest.AssertPackagesEqual(t, tc.expectedPackage, got)
})
}
}
func Test_extractVersion(t *testing.T) {
tests := []struct {
input string
expected string
}{
{
input: "1, 0, 0, 0",
expected: "1, 0, 0, 0",
},
{
input: "Release 73",
expected: "Release 73",
},
{
input: "4.7.4076.0 built by: NET472REL1LAST_B",
expected: "4.7.4076.0",
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
got := extractVersionFromResourcesValue(test.input)
assert.Equal(t, test.expected, got)
})
}
}
func Test_spaceNormalize(t *testing.T) {
tests := []struct {
input string
expected string
}{
{
expected: "some spaces apart",
input: " some spaces\n\t\t \n\rapart\n",
},
{
expected: "söme ¡nvalid characters",
input: "\rsöme \u0001¡nvalid\t characters\n",
},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
got := spaceNormalize(test.input)
assert.Equal(t, test.expected, got)
})
}
}
func TestRuntimeCPEs(t *testing.T) {
tests := []struct {
name string
version string
expected []cpe.CPE
}{
{
name: ".NET Core 1.0",
version: "1.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Version: "1.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET Core 2.1",
version: "2.1",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Version: "2.1",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET Core 3.1",
version: "3.1",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Version: "3.1",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET Core 4.9 (hypothetical)",
version: "4.9",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Version: "4.9",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET 5.0",
version: "5.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "5.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET 6.0",
version: "6.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "6.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET 8.0",
version: "8.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "8.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: ".NET 10.0 (future version)",
version: "10.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "10.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: "Patch version should not be included",
version: "6.0.21",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "6.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: "Assumed minor version",
version: "6",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "6.0",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: "Invalid version format",
version: "invalid",
expected: nil,
},
{
name: "Empty version",
version: "",
expected: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := runtimeCPEs(tc.version)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("runtimeCPEs(%q) = %+v; want %+v",
tc.version, result, tc.expected)
}
})
}
}