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 { if len(p.CPEs) == 0 {
continue 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 return
} }
t.Error("expected at least one runtime package with a CPE") 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 "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{ 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)", "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] 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)", "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", Name: "Microsoft.NETCore.App",
Version: version, Version: version,
Type: pkg.DotnetPkg, Type: pkg.DotnetPkg,
CPEs: runtimeCPEs(version), CPEs: runtimeCPEs("Microsoft.NETCore.App", version),
Locations: file.NewLocationSet(locs...), Locations: file.NewLocationSet(locs...),
} }
pkgs = append(pkgs, rtp) pkgs = append(pkgs, rtp)

View File

@ -22,6 +22,14 @@ var (
versionPunctuationRegex = regexp.MustCompile(`[.,]+`) 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. // newDotnetDepsPackage creates a new Dotnet dependency package from a logicalDepsJSONPackage.
// Note that the new logicalDepsJSONPackage now directly holds library and executable information. // Note that the new logicalDepsJSONPackage now directly holds library and executable information.
func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location) *pkg.Package { func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location) *pkg.Package {
@ -36,7 +44,7 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
var cpes []cpe.CPE var cpes []cpe.CPE
if isRuntime(name) { if isRuntime(name) {
cpes = runtimeCPEs(ver) cpes = runtimeCPEs(name, ver)
} }
p := &pkg.Package{ p := &pkg.Package{
@ -56,52 +64,68 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
} }
func isRuntime(name string) bool { func isRuntime(name string) bool {
// found in a self-contained net8 app in the deps.json for the application return runtimeFamilyFromName(name) != unknownRuntimeFamily
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
} }
func runtimeCPEs(ver string) []cpe.CPE { func runtimeFamilyFromName(name string) runtimeFamily {
// .NET Core Versions normalizedName := strings.ToLower(name)
// 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:*:*:*:*:*:*:*
// Unified .NET Versions // found in self-contained or framework-dependent apps in deps.json entries
// 2020: .NET 5.0, cpe:2.3:a:microsoft:dotnet:5.0:*:*:*:*:*:*:* if strings.HasPrefix(normalizedName, "runtimepack.microsoft.aspnetcore.app.runtime") ||
// 2021: .NET 6.0, cpe:2.3:a:microsoft:dotnet:6.0:*:*:*:*:*:*:* strings.HasPrefix(normalizedName, "microsoft.aspnetcore.app.runtime") ||
// 2022: .NET 7.0, cpe:2.3:a:microsoft:dotnet:7.0:*:*:*:*:*:*:* normalizedName == "microsoft.aspnetcore.app" ||
// 2023: .NET 8.0, cpe:2.3:a:microsoft:dotnet:8.0:*:*:*:*:*:*:* (strings.HasPrefix(normalizedName, "runtime") && strings.HasSuffix(normalizedName, "microsoft.aspnetcore.app")) {
// 2024: .NET 9.0, cpe:2.3:a:microsoft:dotnet:9.0:*:*:*:*:*:*:* return aspNetCoreRuntimeFamily
// 2025 ...? }
// 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, ".") fields := strings.Split(ver, ".")
if len(fields) == 0 {
return nil
}
normalizedVersionFields := make([]string, 0, len(fields))
majorVersion, err := strconv.Atoi(fields[0]) majorVersion, err := strconv.Atoi(fields[0])
if err != nil { 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 return nil
} }
normalizedVersionFields = append(normalizedVersionFields, strconv.Itoa(majorVersion))
var minorVersion int for _, field := range fields[1:] {
if len(fields) > 1 { value, err := strconv.Atoi(field)
minorVersion, err = strconv.Atoi(fields[1])
if err != nil { 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 return nil
} }
normalizedVersionFields = append(normalizedVersionFields, strconv.Itoa(value))
} }
productName := "dotnet" if len(normalizedVersionFields) == 1 {
if majorVersion < 5 { normalizedVersionFields = append(normalizedVersionFields, "0")
productName = "dotnet_core" }
productName := ".net"
if family == aspNetCoreRuntimeFamily {
productName = "asp.net_core"
} else if majorVersion < 5 {
productName = ".net_core"
} }
return []cpe.CPE{ return []cpe.CPE{
@ -110,7 +134,7 @@ func runtimeCPEs(ver string) []cpe.CPE {
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: productName, 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 // 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, 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) { func Test_NewDotnetBinaryPackage(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -401,18 +451,20 @@ func Test_spaceNormalize(t *testing.T) {
func TestRuntimeCPEs(t *testing.T) { func TestRuntimeCPEs(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
packageName string
version string version string
expected []cpe.CPE expected []cpe.CPE
}{ }{
{ {
name: ".NET Core 1.0", name: ".NET Core 1.0",
packageName: "Microsoft.NETCore.App",
version: "1.0", version: "1.0",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet_core", Product: ".net_core",
Version: "1.0", Version: "1.0",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -421,13 +473,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET Core 2.1", name: ".NET Core 2.1",
packageName: "Microsoft.NETCore.App",
version: "2.1", version: "2.1",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet_core", Product: ".net_core",
Version: "2.1", Version: "2.1",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -436,13 +489,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET Core 3.1", name: ".NET Core 3.1",
packageName: "Microsoft.NETCore.App",
version: "3.1", version: "3.1",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet_core", Product: ".net_core",
Version: "3.1", Version: "3.1",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -451,13 +505,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET Core 4.9 (hypothetical)", name: ".NET Core 4.9 (hypothetical)",
packageName: "Microsoft.NETCore.App",
version: "4.9", version: "4.9",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet_core", Product: ".net_core",
Version: "4.9", Version: "4.9",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -466,13 +521,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET 5.0", name: ".NET 5.0",
packageName: "Microsoft.NETCore.App",
version: "5.0", version: "5.0",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet", Product: ".net",
Version: "5.0", Version: "5.0",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -481,13 +537,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET 6.0", name: ".NET 6.0",
packageName: "Microsoft.NETCore.App",
version: "6.0", version: "6.0",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet", Product: ".net",
Version: "6.0", Version: "6.0",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -496,13 +553,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET 8.0", name: ".NET 8.0",
packageName: "Microsoft.NETCore.App",
version: "8.0", version: "8.0",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet", Product: ".net",
Version: "8.0", Version: "8.0",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -511,13 +569,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: ".NET 10.0 (future version)", name: ".NET 10.0 (future version)",
packageName: "Microsoft.NETCore.App",
version: "10.0", version: "10.0",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet", Product: ".net",
Version: "10.0", Version: "10.0",
}, },
Source: cpe.DeclaredSource, 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", version: "6.0.21",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet", Product: ".net",
Version: "6.0", 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, Source: cpe.DeclaredSource,
}, },
@ -541,13 +617,14 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: "Assumed minor version", name: "Assumed minor version",
packageName: "Microsoft.NETCore.App",
version: "6", version: "6",
expected: []cpe.CPE{ expected: []cpe.CPE{
{ {
Attributes: cpe.Attributes{ Attributes: cpe.Attributes{
Part: "a", Part: "a",
Vendor: "microsoft", Vendor: "microsoft",
Product: "dotnet", Product: ".net",
Version: "6.0", Version: "6.0",
}, },
Source: cpe.DeclaredSource, Source: cpe.DeclaredSource,
@ -556,11 +633,13 @@ func TestRuntimeCPEs(t *testing.T) {
}, },
{ {
name: "Invalid version format", name: "Invalid version format",
packageName: "Microsoft.NETCore.App",
version: "invalid", version: "invalid",
expected: nil, expected: nil,
}, },
{ {
name: "Empty version", name: "Empty version",
packageName: "Microsoft.NETCore.App",
version: "", version: "",
expected: nil, expected: nil,
}, },
@ -568,11 +647,11 @@ func TestRuntimeCPEs(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
result := runtimeCPEs(tc.version) result := runtimeCPEs(tc.packageName, tc.version)
if !reflect.DeepEqual(result, tc.expected) { if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("runtimeCPEs(%q) = %+v; want %+v", t.Errorf("runtimeCPEs(%q, %q) = %+v; want %+v",
tc.version, result, tc.expected) tc.packageName, tc.version, result, tc.expected)
} }
}) })
} }