fix(dotnet): align runtime CPEs with NVD (#4743)

Signed-off-by: PGray <PGrayCS@users.noreply.github.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: PGray <PGrayCS@users.noreply.github.com>
This commit is contained in:
PGray 2026-05-06 14:07:49 +01:00 committed by GitHub
parent d81df67493
commit 48e91312e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 202 additions and 95 deletions

View File

@ -730,7 +730,10 @@ func TestCataloger(t *testing.T) {
if len(p.CPEs) == 0 {
continue
}
assert.Contains(t, p.Name, "Microsoft.NETCore.App")
if runtimeFamilyFromName(p.Name) != netRuntimeFamily {
continue
}
assert.Equal(t, runtimeCPEs(p.Name, p.Version), p.CPEs)
return
}
t.Error("expected at least one runtime package with a CPE")
@ -1186,6 +1189,7 @@ func TestCataloger(t *testing.T) {
"runtime.linux-x64.Microsoft.NETCore.DotNetHostPolicy @ 2.2.8 (/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.8/Microsoft.NETCore.App.deps.json)", // a compile target reference
},
expectedRels: []string{
"Microsoft.NETCore.App @ 2.2.8 (/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.8/Microsoft.NETCore.App.deps.json) [dependency-of] helloworld @ 1.0.0 (/app/helloworld.deps.json)",
"Microsoft.NETCore.DotNetHostPolicy @ 2.2.8 (/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.8/Microsoft.NETCore.App.deps.json) [dependency-of] Microsoft.NETCore.App @ 2.2.8 (/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.8/Microsoft.NETCore.App.deps.json)",
"Serilog @ 2.10.0 (/app/helloworld.deps.json) [dependency-of] Serilog.Sinks.Console @ 4.0.1 (/app/helloworld.deps.json)",
"Serilog @ 2.10.0 (/app/helloworld.deps.json) [dependency-of] helloworld @ 1.0.0 (/app/helloworld.deps.json)",

View File

@ -108,7 +108,7 @@ func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver)
Name: "Microsoft.NETCore.App",
Version: version,
Type: pkg.DotnetPkg,
CPEs: runtimeCPEs(version),
CPEs: runtimeCPEs("Microsoft.NETCore.App", version),
Locations: file.NewLocationSet(locs...),
}
pkgs = append(pkgs, rtp)

View File

@ -22,6 +22,14 @@ var (
versionPunctuationRegex = regexp.MustCompile(`[.,]+`)
)
type runtimeFamily string
const (
unknownRuntimeFamily runtimeFamily = ""
netRuntimeFamily runtimeFamily = "net"
aspNetCoreRuntimeFamily runtimeFamily = "aspnet_core"
)
// newDotnetDepsPackage creates a new Dotnet dependency package from a logicalDepsJSONPackage.
// Note that the new logicalDepsJSONPackage now directly holds library and executable information.
func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location) *pkg.Package {
@ -36,7 +44,7 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
var cpes []cpe.CPE
if isRuntime(name) {
cpes = runtimeCPEs(ver)
cpes = runtimeCPEs(name, ver)
}
p := &pkg.Package{
@ -56,52 +64,68 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
}
func isRuntime(name string) bool {
// found in a self-contained net8 app in the deps.json for the application
selfContainedRuntimeDependency := strings.HasPrefix(name, "runtimepack.Microsoft.NETCore.App.Runtime")
// found in net8 apps in the deps.json for the runtime
explicitRuntimeDependency := strings.HasPrefix(name, "Microsoft.NETCore.App.Runtime")
// found in net2 apps in the deps.json for the runtime
producesARuntime := strings.HasPrefix(name, "runtime") && strings.HasSuffix(name, "Microsoft.NETCore.App")
return selfContainedRuntimeDependency || explicitRuntimeDependency || producesARuntime
return runtimeFamilyFromName(name) != unknownRuntimeFamily
}
func runtimeCPEs(ver string) []cpe.CPE {
// .NET Core Versions
// 2016: .NET Core 1.0, cpe:2.3:a:microsoft:dotnet_core:1.0:*:*:*:*:*:*:*
// 2016: .NET Core 1.1, cpe:2.3:a:microsoft:dotnet_core:1.1:*:*:*:*:*:*:*
// 2017: .NET Core 2.0, cpe:2.3:a:microsoft:dotnet_core:2.0:*:*:*:*:*:*:*
// 2018: .NET Core 2.1, cpe:2.3:a:microsoft:dotnet_core:2.1:*:*:*:*:*:*:*
// 2018: .NET Core 2.2, cpe:2.3:a:microsoft:dotnet_core:2.2:*:*:*:*:*:*:*
// 2019: .NET Core 3.0, cpe:2.3:a:microsoft:dotnet_core:3.0:*:*:*:*:*:*:*
// 2019: .NET Core 3.1, cpe:2.3:a:microsoft:dotnet_core:3.1:*:*:*:*:*:*:*
func runtimeFamilyFromName(name string) runtimeFamily {
normalizedName := strings.ToLower(name)
// Unified .NET Versions
// 2020: .NET 5.0, cpe:2.3:a:microsoft:dotnet:5.0:*:*:*:*:*:*:*
// 2021: .NET 6.0, cpe:2.3:a:microsoft:dotnet:6.0:*:*:*:*:*:*:*
// 2022: .NET 7.0, cpe:2.3:a:microsoft:dotnet:7.0:*:*:*:*:*:*:*
// 2023: .NET 8.0, cpe:2.3:a:microsoft:dotnet:8.0:*:*:*:*:*:*:*
// 2024: .NET 9.0, cpe:2.3:a:microsoft:dotnet:9.0:*:*:*:*:*:*:*
// 2025 ...?
// found in self-contained or framework-dependent apps in deps.json entries
if strings.HasPrefix(normalizedName, "runtimepack.microsoft.aspnetcore.app.runtime") ||
strings.HasPrefix(normalizedName, "microsoft.aspnetcore.app.runtime") ||
normalizedName == "microsoft.aspnetcore.app" ||
(strings.HasPrefix(normalizedName, "runtime") && strings.HasSuffix(normalizedName, "microsoft.aspnetcore.app")) {
return aspNetCoreRuntimeFamily
}
// found in self-contained, framework-dependent, and synthesized runtime packages
if strings.HasPrefix(normalizedName, "runtimepack.microsoft.netcore.app.runtime") ||
strings.HasPrefix(normalizedName, "microsoft.netcore.app.runtime") ||
normalizedName == "microsoft.netcore.app" ||
(strings.HasPrefix(normalizedName, "runtime") && strings.HasSuffix(normalizedName, "microsoft.netcore.app")) {
return netRuntimeFamily
}
return unknownRuntimeFamily
}
func runtimeCPEs(name, ver string) []cpe.CPE {
family := runtimeFamilyFromName(name)
if family == unknownRuntimeFamily {
return nil
}
fields := strings.Split(ver, ".")
if len(fields) == 0 {
return nil
}
normalizedVersionFields := make([]string, 0, len(fields))
majorVersion, err := strconv.Atoi(fields[0])
if err != nil {
log.WithFields("error", err).Tracef("failed to parse .NET major version from %q", ver)
log.WithFields("error", err).Tracef("failed to parse .NET runtime major version from %q", ver)
return nil
}
normalizedVersionFields = append(normalizedVersionFields, strconv.Itoa(majorVersion))
var minorVersion int
if len(fields) > 1 {
minorVersion, err = strconv.Atoi(fields[1])
for _, field := range fields[1:] {
value, err := strconv.Atoi(field)
if err != nil {
log.WithFields("error", err).Tracef("failed to parse .NET minor version from %q", ver)
log.WithFields("error", err).Tracef("failed to parse .NET runtime version component %q from %q", field, ver)
return nil
}
normalizedVersionFields = append(normalizedVersionFields, strconv.Itoa(value))
}
productName := "dotnet"
if majorVersion < 5 {
productName = "dotnet_core"
if len(normalizedVersionFields) == 1 {
normalizedVersionFields = append(normalizedVersionFields, "0")
}
productName := ".net"
if family == aspNetCoreRuntimeFamily {
productName = "asp.net_core"
} else if majorVersion < 5 {
productName = ".net_core"
}
return []cpe.CPE{
@ -110,7 +134,7 @@ func runtimeCPEs(ver string) []cpe.CPE {
Part: "a",
Vendor: "microsoft",
Product: productName,
Version: fmt.Sprintf("%d.%d", majorVersion, minorVersion),
Version: strings.Join(normalizedVersionFields, "."),
},
// we didn't find this in the underlying material, but this is the convention in NVD and we are certain this is a runtime package
Source: cpe.DeclaredSource,

View File

@ -46,6 +46,56 @@ func Test_getDepsJSONFilePrefix(t *testing.T) {
}
}
func Test_newDotnetDepsPackage_AssignsRuntimeCPEs(t *testing.T) {
tests := []struct {
name string
nameVersion string
expected []cpe.CPE
}{
{
name: ".NET runtime package",
nameVersion: "runtimepack.Microsoft.NETCore.App.Runtime.win-x64/10.0.4",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: ".net",
Version: "10.0.4",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: "ASP.NET Core runtime package",
nameVersion: "runtimepack.Microsoft.AspNetCore.App.Runtime.win-x64/10.0.4",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "asp.net_core",
Version: "10.0.4",
},
Source: cpe.DeclaredSource,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual := newDotnetDepsPackage(
logicalDepsJSONPackage{NameVersion: tc.nameVersion},
file.NewLocation("/app/test.deps.json"),
)
assert.Equal(t, tc.expected, actual.CPEs)
})
}
}
func Test_NewDotnetBinaryPackage(t *testing.T) {
tests := []struct {
name string
@ -401,18 +451,20 @@ func Test_spaceNormalize(t *testing.T) {
func TestRuntimeCPEs(t *testing.T) {
tests := []struct {
name string
packageName string
version string
expected []cpe.CPE
}{
{
name: ".NET Core 1.0",
packageName: "Microsoft.NETCore.App",
version: "1.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Product: ".net_core",
Version: "1.0",
},
Source: cpe.DeclaredSource,
@ -421,13 +473,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET Core 2.1",
packageName: "Microsoft.NETCore.App",
version: "2.1",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Product: ".net_core",
Version: "2.1",
},
Source: cpe.DeclaredSource,
@ -436,13 +489,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET Core 3.1",
packageName: "Microsoft.NETCore.App",
version: "3.1",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Product: ".net_core",
Version: "3.1",
},
Source: cpe.DeclaredSource,
@ -451,13 +505,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET Core 4.9 (hypothetical)",
packageName: "Microsoft.NETCore.App",
version: "4.9",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet_core",
Product: ".net_core",
Version: "4.9",
},
Source: cpe.DeclaredSource,
@ -466,13 +521,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET 5.0",
packageName: "Microsoft.NETCore.App",
version: "5.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Product: ".net",
Version: "5.0",
},
Source: cpe.DeclaredSource,
@ -481,13 +537,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET 6.0",
packageName: "Microsoft.NETCore.App",
version: "6.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Product: ".net",
Version: "6.0",
},
Source: cpe.DeclaredSource,
@ -496,13 +553,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET 8.0",
packageName: "Microsoft.NETCore.App",
version: "8.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Product: ".net",
Version: "8.0",
},
Source: cpe.DeclaredSource,
@ -511,13 +569,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: ".NET 10.0 (future version)",
packageName: "Microsoft.NETCore.App",
version: "10.0",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Product: ".net",
Version: "10.0",
},
Source: cpe.DeclaredSource,
@ -525,15 +584,32 @@ func TestRuntimeCPEs(t *testing.T) {
},
},
{
name: "Patch version should not be included",
name: "Patch version should be preserved",
packageName: "Microsoft.NETCore.App",
version: "6.0.21",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Version: "6.0",
Product: ".net",
Version: "6.0.21",
},
Source: cpe.DeclaredSource,
},
},
},
{
name: "ASP.NET Core runtime",
packageName: "runtimepack.Microsoft.AspNetCore.App.Runtime.win-x64",
version: "9.0.10",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "asp.net_core",
Version: "9.0.10",
},
Source: cpe.DeclaredSource,
},
@ -541,13 +617,14 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: "Assumed minor version",
packageName: "Microsoft.NETCore.App",
version: "6",
expected: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: "dotnet",
Product: ".net",
Version: "6.0",
},
Source: cpe.DeclaredSource,
@ -556,11 +633,13 @@ func TestRuntimeCPEs(t *testing.T) {
},
{
name: "Invalid version format",
packageName: "Microsoft.NETCore.App",
version: "invalid",
expected: nil,
},
{
name: "Empty version",
packageName: "Microsoft.NETCore.App",
version: "",
expected: nil,
},
@ -568,11 +647,11 @@ func TestRuntimeCPEs(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := runtimeCPEs(tc.version)
result := runtimeCPEs(tc.packageName, tc.version)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("runtimeCPEs(%q) = %+v; want %+v",
tc.version, result, tc.expected)
t.Errorf("runtimeCPEs(%q, %q) = %+v; want %+v",
tc.packageName, tc.version, result, tc.expected)
}
})
}