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>
This commit is contained in:
Alex Goodman 2025-03-28 13:36:27 -04:00 committed by GitHub
parent 40dd5d0bbd
commit c53f2fbad3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1766 additions and 106 deletions

View File

@ -121,55 +121,55 @@ func TestCataloger(t *testing.T) {
// app binaries (always dlls)
net8AppBinaryOnlyPkgs := []string{
"Humanizer (net6.0) @ 2.14.1.48190 (/app/Humanizer.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/af/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/ar/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/az/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/bg/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/bn-BD/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/cs/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/da/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/de/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/el/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/es/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/fa/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/fi-FI/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/fr-BE/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/fr/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/he/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/hr/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/hu/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/hy/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/id/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/is/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/it/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/ja/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/ku/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/lv/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/nb-NO/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/nb/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/nl/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/pl/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/pt/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/ro/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/ru/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/sk/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/sl/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/sr-Latn/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/sr/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/sv/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/tr/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/uk/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/uz-Cyrl-UZ/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/uz-Latn-UZ/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/vi/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/zh-CN/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/zh-Hans/Humanizer.resources.dll)",
"Humanizer (net6.0) @ 2.14.1.48190 (/app/zh-Hant/Humanizer.resources.dll)",
"Humanizer (netstandard2.0) @ 2.14.1.48190 (/app/ko-KR/Humanizer.resources.dll)",
"Humanizer (netstandard2.0) @ 2.14.1.48190 (/app/ms-MY/Humanizer.resources.dll)",
"Humanizer (netstandard2.0) @ 2.14.1.48190 (/app/mt/Humanizer.resources.dll)",
"Humanizer (netstandard2.0) @ 2.14.1.48190 (/app/th-TH/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/Humanizer.dll)",
"Humanizer @ 2.14.1.48190 (/app/af/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ar/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/az/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/bg/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/bn-BD/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/cs/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/da/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/de/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/el/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/es/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/fa/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/fi-FI/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/fr-BE/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/fr/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/he/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/hr/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/hu/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/hy/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/id/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/is/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/it/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ja/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ku/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/lv/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/nb-NO/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/nb/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/nl/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/pl/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/pt/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ro/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ru/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/sk/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/sl/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/sr-Latn/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/sr/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/sv/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/tr/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/uk/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/uz-Cyrl-UZ/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/uz-Latn-UZ/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/vi/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/zh-CN/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/zh-Hans/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/zh-Hant/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ko-KR/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/ms-MY/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/mt/Humanizer.resources.dll)",
"Humanizer @ 2.14.1.48190 (/app/th-TH/Humanizer.resources.dll)",
"Json.NET @ 13.0.3.27908 (/app/Newtonsoft.Json.dll)",
"dotnetapp @ 1.0.0.0 (/app/dotnetapp.dll)",
}
@ -287,7 +287,6 @@ func TestCataloger(t *testing.T) {
net8AppExpectedDepSelfContainedPkgs = append(net8AppExpectedDepSelfContainedPkgs, net8AppExpectedDepPkgsWithoutUnpairedDlls...)
net8AppExpectedDepSelfContainedPkgs = append(net8AppExpectedDepSelfContainedPkgs,
// add the CLR runtime packages...
".NET Runtime @ 8,0,1425,11118 (/app/coreclr.dll)",
"runtimepack.Microsoft.NETCore.App.Runtime.win-x64 @ 8.0.14 (/app/dotnetapp.deps.json)",
)
@ -581,6 +580,10 @@ func TestCataloger(t *testing.T) {
assertAllBinaryEntries := func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
t.Helper()
for _, p := range pkgs {
if p.Name == "Microsoft.NETCore.App" {
// for the runtime app we created ourselves there is no metadata for
continue
}
// assert that all packages have an executable associated with it
m, ok := p.Metadata.(pkg.DotnetPortableExecutableEntry)
if !ok {
@ -674,6 +677,18 @@ func TestCataloger(t *testing.T) {
pkgtest.AssertPackagesEqualIgnoreLayers(t, expected, actual)
}
assertAccurateNetRuntimePackage := func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
// the package with the CPE is the runtime package
for _, p := range pkgs {
if len(p.CPEs) == 0 {
continue
}
assert.Contains(t, p.Name, "Microsoft.NETCore.App")
return
}
t.Error("expected at least one runtime package with a CPE")
}
cases := []struct {
name string
fixture string
@ -699,12 +714,45 @@ func TestCataloger(t *testing.T) {
//expectedPkgs: net8AppExpectedDepPkgs,
//expectedRels: net8AppExpectedDepRelationships,
// we care about DLL claims in the deps.json, so the main application inherits all relationships to/from humarizer
// we care about DLL claims in the deps.json, so the main application inherits all relationships to/from humanizer
expectedPkgs: net8AppExpectedDepPkgsWithoutUnpairedDlls,
expectedRels: replaceAll(net8AppDepOnlyRelationshipsWithoutHumanizer, "Humanizer @ 2.14.1", "dotnetapp @ 1.0.0"),
assertion: assertAlmostAllDepEntriesWithExecutables, // important! this is what makes this case different from the previous one... dep entries have attached executables
},
{
name: "combined cataloger (with runtime)",
fixture: "image-net8-app-with-runtime",
cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
expectedPkgs: func() []string {
pkgs := net8AppExpectedDepPkgsWithoutUnpairedDlls
pkgs = append(pkgs, "Microsoft.NETCore.App.Runtime.linux-x64 @ 8.0.14 (/usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.14/Microsoft.NETCore.App.deps.json)")
return pkgs
}(),
expectedRels: func() []string {
x := replaceAll(net8AppDepOnlyRelationshipsWithoutHumanizer, "Humanizer @ 2.14.1", "dotnetapp @ 1.0.0")
// the main application should also have a relationship to the runtime package
x = append(x, "Microsoft.NETCore.App.Runtime.linux-x64 @ 8.0.14 (/usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.14/Microsoft.NETCore.App.deps.json) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp.deps.json)")
return x
}(),
assertion: assertAccurateNetRuntimePackage,
},
{
name: "combined cataloger (with runtime, no deps.json anywhere)",
fixture: "image-net8-app-with-runtime-nodepsjson",
cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
expectedPkgs: func() []string {
// all the same packages we found in "image-net8-app-with-runtime", however we create a runtime package out of all of the DLLs we found instead
x := net8AppBinaryOnlyPkgs
x = append(x, "Microsoft.NETCore.App @ 8.0.14 (/usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.14/Microsoft.CSharp.dll)")
return x
}(),
// important: no relationships should be found
assertion: func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
assertAllBinaryEntries(t, pkgs, relationships)
assertAccurateNetRuntimePackage(t, pkgs, relationships)
},
},
{
name: "combined cataloger (require dll pairings)",
fixture: "image-net8-app",
@ -887,7 +935,13 @@ func TestCataloger(t *testing.T) {
cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
// we care about DLL claims in the deps.json, so the main application inherits all relationships to/from humarizer
expectedPkgs: net8AppExpectedDepSelfContainedPkgs,
expectedRels: replaceAll(net8AppExpectedDepSelfContainedRelationships, "Humanizer @ 2.14.1", "dotnetapp @ 1.0.0"),
expectedRels: func() []string {
x := replaceAll(net8AppExpectedDepSelfContainedRelationships, "Humanizer @ 2.14.1", "dotnetapp @ 1.0.0")
// the main application also has a dependency on the runtime package
x = append(x, "runtimepack.Microsoft.NETCore.App.Runtime.win-x64 @ 8.0.14 (/app/dotnetapp.deps.json) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp.deps.json)")
return x
}(),
assertion: assertAccurateNetRuntimePackage,
},
{
name: "pe cataloger (self-contained)",
@ -945,6 +999,24 @@ func TestCataloger(t *testing.T) {
return x
}(),
},
{
name: "net2 app, combined cataloger (private assets)",
fixture: "image-net2-app",
cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
expectedPkgs: []string{
"Serilog @ 2.10.0 (/app/helloworld.deps.json)",
"Serilog.Sinks.Console @ 4.0.1 (/app/helloworld.deps.json)",
"helloworld @ 1.0.0 (/app/helloworld.deps.json)",
"runtime.linux-x64.Microsoft.NETCore.App @ 2.2.8 (/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.8/Microsoft.NETCore.App.deps.json)",
},
expectedRels: []string{
"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.Sinks.Console @ 4.0.1 (/app/helloworld.deps.json) [dependency-of] helloworld @ 1.0.0 (/app/helloworld.deps.json)",
"runtime.linux-x64.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)",
},
assertion: assertAccurateNetRuntimePackage,
},
}
for _, tt := range cases {

View File

@ -35,7 +35,7 @@ func (c depsBinaryCataloger) Name() string {
return "dotnet-deps-binary-cataloger"
}
func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen
depJSONDocs, unknowns, err := findDepsJSON(resolver)
if err != nil {
return nil, nil, err
@ -61,21 +61,86 @@ func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver)
depDocGroups = append(depDocGroups, remainingDepsJSONs)
}
var roots []*pkg.Package
for _, docs := range depDocGroups {
for _, doc := range docs {
ps, rs := packagesFromLogicalDepsJSON(doc, c.config)
rts, ps, rs := packagesFromLogicalDepsJSON(doc, c.config)
if rts != nil {
roots = append(roots, rts)
}
pkgs = append(pkgs, ps...)
relationships = append(relationships, rs...)
}
}
// track existing runtime packages so we don't create duplicates
existingRuntimeVersions := strset.New()
var runtimePkgs []*pkg.Package
for i := range pkgs {
p := &pkgs[i]
if isRuntime(p.Name) {
existingRuntimeVersions.Add(p.Version)
runtimePkgs = append(runtimePkgs, p)
}
}
runtimes := make(map[string][]file.Location)
for _, pe := range remainingPeFiles {
runtimeVer, isRuntimePkg := isRuntimePackageLocation(pe.Location)
if isRuntimePkg {
runtimes[runtimeVer] = append(runtimes[runtimeVer], pe.Location)
// we should never catalog runtime DLLs as packages themselves, instead there should be a single logical package
continue
}
pkgs = append(pkgs, newDotnetBinaryPackage(pe.VersionResources, pe.Location))
}
// if we found any runtime DLLs we ignored, then make packages for each version found
for version, locs := range runtimes {
if len(locs) == 0 || existingRuntimeVersions.Has(version) {
continue
}
rtp := pkg.Package{
Name: "Microsoft.NETCore.App",
Version: version,
Type: pkg.DotnetPkg,
CPEs: runtimeCPEs(version),
Locations: file.NewLocationSet(locs...),
}
pkgs = append(pkgs, rtp)
runtimePkgs = append(runtimePkgs, &rtp)
}
// create a relationship from every runtime package to every root package
for _, root := range roots {
for _, runtimePkg := range runtimePkgs {
relationships = append(relationships, artifact.Relationship{
From: *runtimePkg,
To: *root,
Type: artifact.DependencyOfRelationship,
})
}
}
return pkgs, relationships, unknowns
}
var runtimeDLLPathPattern = regexp.MustCompile(`/Microsoft\.NETCore\.App/(?P<version>\d+\.\d+\.\d+)/[^/]+\.dll`)
func isRuntimePackageLocation(loc file.Location) (string, bool) {
// we should look at the realpath to see if it is a "**/Microsoft.NETCore.App/\d+.\d+.\d+/*.dll"
// and if so treat it as a runtime package
if match := runtimeDLLPathPattern.FindStringSubmatch(loc.RealPath); match != nil {
versionIndex := runtimeDLLPathPattern.SubexpIndex("version")
if versionIndex != -1 {
version := match[versionIndex]
return version, true
}
}
return "", false
}
// partitionPEs pairs PE files with the deps.json based on directory containment.
func partitionPEs(depJsons []logicalDepsJSON, peFiles []logicalPE) ([]logicalDepsJSON, []logicalPE, []logicalDepsJSON) {
// sort deps.json paths from longest to shortest. This is so we are processing the most specific match first.
@ -136,24 +201,20 @@ func attachAssociatedExecutables(dep *logicalDepsJSON, pe logicalPE) bool {
p.Executables = append(p.Executables, pe)
dep.PackagesByNameVersion[key] = p // update the map with the modified package
found = true
continue
}
if p.NativePaths.Has(relativeDllPath) {
pe.TargetPath = relativeDllPath
p.Executables = append(p.Executables, pe)
dep.PackagesByNameVersion[key] = p // update the map with the modified package
found = true
continue
}
}
return found
}
var libPrefixPattern = regexp.MustCompile(`^lib/net[^/]+/`)
// trimLibPrefix removes prefixes like "lib/net6.0/" from a path.
func trimLibPrefix(s string) string {
if match := libPrefixPattern.FindString(s); match != "" {
parts := strings.Split(s, "/")
if len(parts) > 2 {
return strings.Join(parts[2:], "/")
}
}
return s
}
// isParentOf checks if parentFile's directory is a prefix of childFile's directory.
func isParentOf(parentFile, childFile string) bool {
parentDir := path.Dir(parentFile)
@ -166,7 +227,7 @@ func packagesFromDepsJSON(docs []logicalDepsJSON, config CatalogerConfig) ([]pkg
var pkgs []pkg.Package
var relationships []artifact.Relationship
for _, ldj := range docs {
ps, rs := packagesFromLogicalDepsJSON(ldj, config)
_, ps, rs := packagesFromLogicalDepsJSON(ldj, config)
pkgs = append(pkgs, ps...)
relationships = append(relationships, rs...)
}
@ -174,9 +235,9 @@ func packagesFromDepsJSON(docs []logicalDepsJSON, config CatalogerConfig) ([]pkg
}
// packagesFromLogicalDepsJSON converts a logicalDepsJSON (using the new map type) into catalog packages.
func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) ([]pkg.Package, []artifact.Relationship) {
func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) (*pkg.Package, []pkg.Package, []artifact.Relationship) {
var rootPkg *pkg.Package
if rootLpkg, hasRoot := doc.RootPackage(); !hasRoot {
if rootLpkg, hasRoot := doc.RootPackage(); hasRoot {
rootPkg = newDotnetDepsPackage(rootLpkg, doc.Location)
}
@ -222,7 +283,7 @@ func packagesFromLogicalDepsJSON(doc logicalDepsJSON, config CatalogerConfig) ([
}
}
return pkgs, relationshipsFromLogicalDepsJSON(doc, pkgMap, skippedDepPkgs)
return rootPkg, pkgs, relationshipsFromLogicalDepsJSON(doc, pkgMap, skippedDepPkgs)
}
// relationshipsFromLogicalDepsJSON creates relationships from a logicalDepsJSON document for only the given syft packages.
@ -253,7 +314,7 @@ func relationshipsFromLogicalDepsJSON(doc logicalDepsJSON, pkgMap map[string]pkg
}
// we have a skipped package, so we need to create a relationship but looking a the nearest
// package with an associated PE file for even dependency listed on the skipped package.
// Take note that the skipped depedency's dependency could also be skipped, so we need to
// Take note that the skipped dependency's dependency could also be skipped, so we need to
// do this recursively.
depPkgs = findNearestDependencyPackages(skippedDepPkg, pkgMap, skipped, strset.New())
} else {

View File

@ -3,6 +3,7 @@ package dotnet
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/scylladb/go-set/strset"
@ -25,6 +26,7 @@ type depsTarget struct {
Dependencies map[string]string `json:"dependencies"`
Runtime map[string]map[string]string `json:"runtime"`
Resources map[string]map[string]string `json:"resources"`
Native map[string]map[string]string `json:"native"`
}
type depsLibrary struct {
@ -49,6 +51,11 @@ type logicalDepsJSONPackage struct {
// to the target path as described in the deps.json target entry under "resource".
ResourcePathsByRelativeDLLPath map[string]string
// NativePathsByRelativeDLLPath is a map of the relative path to the DLL relative to the deps.json file
// to the target path as described in the deps.json target entry under "native". These should not have
// any runtime references to trim from the front of the path.
NativePaths *strset.Set
// Executables is a list of all the executables that are part of this package. This is populated by the PE cataloger
// and not something that is found in the deps.json file. This allows us to associate the PE files with this package
// based on the relative path to the DLL.
@ -117,35 +124,43 @@ func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON {
for _, targets := range deps.Targets {
for libName, target := range targets {
_, exists := packageMap[libName]
if !exists {
var lib *depsLibrary
l, ok := deps.Libraries[libName]
if ok {
lib = &l
}
runtimePaths := make(map[string]string)
for path := range target.Runtime {
runtimePaths[trimLibPrefix(path)] = path
}
resourcePaths := make(map[string]string)
for path := range target.Resources {
trimmedPath := trimLibPrefix(path)
if _, exists := resourcePaths[trimmedPath]; exists {
continue
}
resourcePaths[trimmedPath] = path
}
p := &logicalDepsJSONPackage{
NameVersion: libName,
Library: lib,
Targets: &target,
RuntimePathsByRelativeDLLPath: runtimePaths,
ResourcePathsByRelativeDLLPath: resourcePaths,
}
packageMap[libName] = p
nameVersions.Add(libName)
if exists {
continue
}
var lib *depsLibrary
l, ok := deps.Libraries[libName]
if ok {
lib = &l
}
runtimePaths := make(map[string]string)
for path := range target.Runtime {
runtimePaths[trimLibPrefix(path)] = path
}
resourcePaths := make(map[string]string)
for path := range target.Resources {
trimmedPath := trimLibPrefix(path)
if _, exists := resourcePaths[trimmedPath]; exists {
continue
}
resourcePaths[trimmedPath] = path
}
nativePaths := strset.New()
for path := range target.Native {
nativePaths.Add(path)
}
p := &logicalDepsJSONPackage{
NameVersion: libName,
Library: lib,
Targets: &target,
RuntimePathsByRelativeDLLPath: runtimePaths,
ResourcePathsByRelativeDLLPath: resourcePaths,
NativePaths: nativePaths,
}
packageMap[libName] = p
nameVersions.Add(libName)
}
}
packages := make(map[string]logicalDepsJSONPackage)
@ -166,3 +181,18 @@ func getLogicalDepsJSON(deps depsJSON) logicalDepsJSON {
BundlingDetected: bundlingDetected,
}
}
var libPathPattern = regexp.MustCompile(`^(?:runtimes/[^/]+/)?lib/net[^/]+/(?P<targetPath>.+)`)
// trimLibPrefix removes prefixes like "lib/net6.0/" or "runtimes/linux-arm/lib/netcoreapp2.2/" from a path.
// It captures and returns everything after the framework version section using a named capture group.
func trimLibPrefix(s string) string {
if match := libPathPattern.FindStringSubmatch(s); len(match) > 1 {
// Get the index of the named capture group
targetPathIndex := libPathPattern.SubexpIndex("targetPath")
if targetPathIndex != -1 {
return match[targetPathIndex]
}
}
return s
}

View File

@ -0,0 +1,73 @@
package dotnet
import (
"testing"
)
func TestTrimLibPrefix(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Empty path",
input: "",
expected: "",
},
{
name: "simple .NET 6.0 path",
input: "lib/net6.0/Humanizer.dll",
expected: "Humanizer.dll",
},
{
name: "locale-specific resource path",
input: "lib/net6.0/af/Humanizer.resources.dll",
expected: "af/Humanizer.resources.dll",
},
{
name: "netstandard path",
input: "lib/netstandard2.0/Serilog.Sinks.Console.dll",
expected: "Serilog.Sinks.Console.dll",
},
{
name: "runtime-specific path",
input: "runtimes/linux-arm/lib/netcoreapp2.2/System.Collections.Concurrent.dll",
expected: "System.Collections.Concurrent.dll",
},
{
name: "runtime-specific path with locale",
input: "runtimes/win/lib/net6.0/fr-ME/re/Microsoft.Data.SqlClient.resources.dll",
expected: "fr-ME/re/Microsoft.Data.SqlClient.resources.dll",
},
{
name: "subdirectories",
input: "lib/net7.0/Microsoft/Extensions/Logging.dll",
expected: "Microsoft/Extensions/Logging.dll",
},
{
name: "doesn't match the pattern",
input: "content/styles/main.css",
expected: "content/styles/main.css",
},
{
name: "different framework format",
input: "lib/net472/Newtonsoft.Json.dll",
expected: "Newtonsoft.Json.dll",
},
{
name: "frameworkless lib",
input: "lib/Newtonsoft.Json.dll",
expected: "lib/Newtonsoft.Json.dll", // should not match our pattern
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := trimLibPrefix(tc.input)
if result != tc.expected {
t.Errorf("trimLibPrefix(%q) = %q; want %q", tc.input, result, tc.expected)
}
})
}
}

View File

@ -2,12 +2,15 @@ package dotnet
import (
"fmt"
"path"
"regexp"
"strconv"
"strings"
"github.com/anchore/go-version"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@ -31,6 +34,11 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
m := newDotnetDepsEntry(lp)
var cpes []cpe.CPE
if isRuntime(name) {
cpes = runtimeCPEs(ver)
}
p := &pkg.Package{
Name: name,
Version: ver,
@ -38,6 +46,7 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
PURL: packageURL(m),
Language: pkg.Dotnet,
Type: pkg.DotnetPkg,
CPEs: cpes,
Metadata: m,
}
@ -46,6 +55,69 @@ func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location)
return p
}
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
}
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:*:*:*:*:*:*:*
// 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 ...?
fields := strings.Split(ver, ".")
majorVersion, err := strconv.Atoi(fields[0])
if err != nil {
log.WithFields("error", err).Tracef("failed to parse .NET major version from %q", ver)
return nil
}
var minorVersion int
if len(fields) > 1 {
minorVersion, err = strconv.Atoi(fields[1])
if err != nil {
log.WithFields("error", err).Tracef("failed to parse .NET minor version from %q", ver)
return nil
}
}
productName := "dotnet"
if majorVersion < 5 {
productName = "dotnet_core"
}
return []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "microsoft",
Product: productName,
Version: fmt.Sprintf("%d.%d", majorVersion, minorVersion),
},
// 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,
},
}
}
// newDotnetDepsEntry creates a Dotnet dependency entry using the new logicalDepsJSONPackage.
func newDotnetDepsEntry(lp logicalDepsJSONPackage) pkg.DotnetDepsEntry {
name, ver := extractNameAndVersion(lp.NameVersion)
@ -145,7 +217,14 @@ func packageURL(m pkg.DotnetDepsEntry) string {
}
func newDotnetBinaryPackage(versionResources map[string]string, f file.Location) pkg.Package {
name := findNameFromVersionResources(versionResources)
// TODO: we may decide to use the runtime information in the metadata, but that is not captured today
name, _ := findNameAndRuntimeFromVersionResources(versionResources)
if name == "" {
// older .NET runtime dlls may not have any version resources
name = strings.TrimSuffix(strings.TrimSuffix(path.Base(f.RealPath), ".exe"), ".dll")
}
ver := findVersionFromVersionResources(versionResources)
metadata := newDotnetPortableExecutableEntryFromMap(versionResources)
@ -179,26 +258,37 @@ func binaryPackageURL(name, version string) string {
).ToString()
}
func findNameFromVersionResources(versionResources map[string]string) string {
var binRuntimeSuffixPattern = regexp.MustCompile(`\s*\((?P<runtime>net[^)]*[0-9]+(\.[0-9]+)?)\)$`)
func findNameAndRuntimeFromVersionResources(versionResources map[string]string) (string, string) {
// PE files not authored by Microsoft tend to use ProductName as an identifier.
nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
if isMicrosoftVersionResource(versionResources) {
// For Microsoft files, prioritize FileDescription.
// for Microsoft files, prioritize FileDescription.
nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
}
var name string
for _, field := range nameFields {
value := spaceNormalize(versionResources[field])
if value == "" {
continue
}
return value
name = value
break
}
return ""
}
var runtime string
// look for indications of the runtime, such as "(net8.0)" or "(netstandard2.2)" suffixes
runtimes := binRuntimeSuffixPattern.FindStringSubmatch(name)
if len(runtimes) > 1 {
runtime = strings.TrimSpace(runtimes[1])
name = strings.TrimSpace(strings.TrimSuffix(name, runtimes[0]))
}
return name, runtime
}
func isMicrosoftVersionResource(versionResources map[string]string) bool {
return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")

View File

@ -1,10 +1,12 @@
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"
@ -377,3 +379,183 @@ func Test_spaceNormalize(t *testing.T) {
})
}
}
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)
}
})
}
}

View File

@ -0,0 +1,2 @@
/app
/extract.sh

View File

@ -0,0 +1,17 @@
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/core/sdk:2.2 AS build
ARG RUNTIME=win-x64
WORKDIR /src
COPY src/helloworld.csproj .
RUN dotnet restore -r $RUNTIME
COPY src/*.cs .
RUN dotnet publish -c Release --no-restore -o /app
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/core/runtime:2.2
WORKDIR /app
COPY --from=build /app .
# this is a realistic application image since the runtime is with the app
ENTRYPOINT ["dotnet", "HelloWorld.dll"]

View File

@ -0,0 +1,42 @@
using System;
using Serilog;
namespace HelloWorld
{
/// <summary>
/// Main program class!
/// </summary>
public class Program
{
/// <summary>
/// Entry point for the application!
/// </summary>
/// <param name="args">Command line arguments!</param>
public static void Main(string[] args)
{
// configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.CreateLogger();
try
{
Log.Information("Starting up the application");
Console.WriteLine("Hello World!");
Log.Information("Application completed successfully");
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -1 +1,2 @@
/app
/extract.sh

View File

@ -0,0 +1,2 @@
/app
/extract.sh

View File

@ -0,0 +1,32 @@
# This represents a basic .NET project build where the project dependencies are downloaded and the project is built.
# The output is a directory tree of DLLs, a project.lock.json (not used in these tests), a .deps.json file, and
# a .runtimeconfig.json file (not used in these tests).
# With this deployment strategy there IS a bundled runtime.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
COPY src/packages.lock.json .
RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode
# copy and publish app and libraries
COPY src/ .
RUN dotnet publish -r $RUNTIME --no-restore -o /app
# remove the deps.json ... important!
RUN rm -f /app/dotnetapp.deps.json
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/runtime:8.0@sha256:a6fc92280fbf2149cd6846d39c5bf7b9b535184e470aa68ef2847b9a02f6b99e
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .
# this is an odd choice, but possible
RUN rm -f /usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.14/Microsoft.NETCore.App.deps.json
# this is a more realistic application image since the runtime is with the app
ENTRYPOINT ["dotnet", "dotnetapp.dll"]

View File

@ -0,0 +1,31 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
using static System.Console;
WriteLine("Runtime and Environment Information");
// OS and .NET information
WriteLine($"{nameof(RuntimeInformation.OSArchitecture)}: {RuntimeInformation.OSArchitecture}");
WriteLine($"{nameof(RuntimeInformation.OSDescription)}: {RuntimeInformation.OSDescription}");
WriteLine($"{nameof(RuntimeInformation.FrameworkDescription)}: {RuntimeInformation.FrameworkDescription}");
WriteLine();
// Environment information
WriteLine($"{nameof(Environment.UserName)}: {Environment.UserName}");
WriteLine($"HostName: {Dns.GetHostName()}");
WriteLine($"{nameof(Environment.ProcessorCount)}: {Environment.ProcessorCount}");
WriteLine();
// Memory information
WriteLine($"Available Memory (GC): {GetInBestUnit(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes)}");
string GetInBestUnit(long size)
{
const double Mebi = 1024 * 1024;
const double Gibi = Mebi * 1024;
if (size < Mebi) return $"{size} bytes";
if (size < Gibi) return $"{size / Mebi:F} MiB";
return $"{size / Gibi:F} GiB";
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,459 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Humanizer": {
"type": "Direct",
"requested": "[2.14.1, )",
"resolved": "2.14.1",
"contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
"dependencies": {
"Humanizer.Core.af": "2.14.1",
"Humanizer.Core.ar": "2.14.1",
"Humanizer.Core.az": "2.14.1",
"Humanizer.Core.bg": "2.14.1",
"Humanizer.Core.bn-BD": "2.14.1",
"Humanizer.Core.cs": "2.14.1",
"Humanizer.Core.da": "2.14.1",
"Humanizer.Core.de": "2.14.1",
"Humanizer.Core.el": "2.14.1",
"Humanizer.Core.es": "2.14.1",
"Humanizer.Core.fa": "2.14.1",
"Humanizer.Core.fi-FI": "2.14.1",
"Humanizer.Core.fr": "2.14.1",
"Humanizer.Core.fr-BE": "2.14.1",
"Humanizer.Core.he": "2.14.1",
"Humanizer.Core.hr": "2.14.1",
"Humanizer.Core.hu": "2.14.1",
"Humanizer.Core.hy": "2.14.1",
"Humanizer.Core.id": "2.14.1",
"Humanizer.Core.is": "2.14.1",
"Humanizer.Core.it": "2.14.1",
"Humanizer.Core.ja": "2.14.1",
"Humanizer.Core.ko-KR": "2.14.1",
"Humanizer.Core.ku": "2.14.1",
"Humanizer.Core.lv": "2.14.1",
"Humanizer.Core.ms-MY": "2.14.1",
"Humanizer.Core.mt": "2.14.1",
"Humanizer.Core.nb": "2.14.1",
"Humanizer.Core.nb-NO": "2.14.1",
"Humanizer.Core.nl": "2.14.1",
"Humanizer.Core.pl": "2.14.1",
"Humanizer.Core.pt": "2.14.1",
"Humanizer.Core.ro": "2.14.1",
"Humanizer.Core.ru": "2.14.1",
"Humanizer.Core.sk": "2.14.1",
"Humanizer.Core.sl": "2.14.1",
"Humanizer.Core.sr": "2.14.1",
"Humanizer.Core.sr-Latn": "2.14.1",
"Humanizer.Core.sv": "2.14.1",
"Humanizer.Core.th-TH": "2.14.1",
"Humanizer.Core.tr": "2.14.1",
"Humanizer.Core.uk": "2.14.1",
"Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
"Humanizer.Core.uz-Latn-UZ": "2.14.1",
"Humanizer.Core.vi": "2.14.1",
"Humanizer.Core.zh-CN": "2.14.1",
"Humanizer.Core.zh-Hans": "2.14.1",
"Humanizer.Core.zh-Hant": "2.14.1"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Humanizer.Core.af": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ar": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.az": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bg": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bn-BD": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.cs": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.da": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.de": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.el": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.es": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fa": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fi-FI": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr-BE": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.he": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hu": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hy": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.id": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.is": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.it": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ja": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ko-KR": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ku": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.lv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ms-MY": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.mt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb-NO": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ro": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ru": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr-Latn": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.th-TH": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.tr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Cyrl-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Latn-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.vi": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-CN": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hans": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hant": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
}
},
"net8.0/win-x64": {}
}
}

View File

@ -0,0 +1,2 @@
/app
/extract.sh

View File

@ -0,0 +1,26 @@
# This represents a basic .NET project build where the project dependencies are downloaded and the project is built.
# The output is a directory tree of DLLs, a project.lock.json (not used in these tests), a .deps.json file, and
# a .runtimeconfig.json file (not used in these tests).
# With this deployment strategy there IS a bundled runtime.
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
ARG RUNTIME=win-x64
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj .
COPY src/packages.lock.json .
RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode
# copy and publish app and libraries
COPY src/ .
RUN dotnet publish -r $RUNTIME --no-restore -o /app
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/runtime:8.0@sha256:a6fc92280fbf2149cd6846d39c5bf7b9b535184e470aa68ef2847b9a02f6b99e
WORKDIR /app
COPY --from=build /app .
# just a nice to have for later...
#COPY --from=build /src/packages.lock.json .
# this is a more realistic application image since the runtime is with the app
ENTRYPOINT ["dotnet", "dotnetapp.dll"]

View File

@ -0,0 +1,31 @@
using System;
using System.Net;
using System.Runtime.InteropServices;
using static System.Console;
WriteLine("Runtime and Environment Information");
// OS and .NET information
WriteLine($"{nameof(RuntimeInformation.OSArchitecture)}: {RuntimeInformation.OSArchitecture}");
WriteLine($"{nameof(RuntimeInformation.OSDescription)}: {RuntimeInformation.OSDescription}");
WriteLine($"{nameof(RuntimeInformation.FrameworkDescription)}: {RuntimeInformation.FrameworkDescription}");
WriteLine();
// Environment information
WriteLine($"{nameof(Environment.UserName)}: {Environment.UserName}");
WriteLine($"HostName: {Dns.GetHostName()}");
WriteLine($"{nameof(Environment.ProcessorCount)}: {Environment.ProcessorCount}");
WriteLine();
// Memory information
WriteLine($"Available Memory (GC): {GetInBestUnit(GC.GetGCMemoryInfo().TotalAvailableMemoryBytes)}");
string GetInBestUnit(long size)
{
const double Mebi = 1024 * 1024;
const double Gibi = Mebi * 1024;
if (size < Mebi) return $"{size} bytes";
if (size < Gibi) return $"{size / Mebi:F} MiB";
return $"{size / Gibi:F} GiB";
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Humanizer" Version="2.14.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,459 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"Humanizer": {
"type": "Direct",
"requested": "[2.14.1, )",
"resolved": "2.14.1",
"contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
"dependencies": {
"Humanizer.Core.af": "2.14.1",
"Humanizer.Core.ar": "2.14.1",
"Humanizer.Core.az": "2.14.1",
"Humanizer.Core.bg": "2.14.1",
"Humanizer.Core.bn-BD": "2.14.1",
"Humanizer.Core.cs": "2.14.1",
"Humanizer.Core.da": "2.14.1",
"Humanizer.Core.de": "2.14.1",
"Humanizer.Core.el": "2.14.1",
"Humanizer.Core.es": "2.14.1",
"Humanizer.Core.fa": "2.14.1",
"Humanizer.Core.fi-FI": "2.14.1",
"Humanizer.Core.fr": "2.14.1",
"Humanizer.Core.fr-BE": "2.14.1",
"Humanizer.Core.he": "2.14.1",
"Humanizer.Core.hr": "2.14.1",
"Humanizer.Core.hu": "2.14.1",
"Humanizer.Core.hy": "2.14.1",
"Humanizer.Core.id": "2.14.1",
"Humanizer.Core.is": "2.14.1",
"Humanizer.Core.it": "2.14.1",
"Humanizer.Core.ja": "2.14.1",
"Humanizer.Core.ko-KR": "2.14.1",
"Humanizer.Core.ku": "2.14.1",
"Humanizer.Core.lv": "2.14.1",
"Humanizer.Core.ms-MY": "2.14.1",
"Humanizer.Core.mt": "2.14.1",
"Humanizer.Core.nb": "2.14.1",
"Humanizer.Core.nb-NO": "2.14.1",
"Humanizer.Core.nl": "2.14.1",
"Humanizer.Core.pl": "2.14.1",
"Humanizer.Core.pt": "2.14.1",
"Humanizer.Core.ro": "2.14.1",
"Humanizer.Core.ru": "2.14.1",
"Humanizer.Core.sk": "2.14.1",
"Humanizer.Core.sl": "2.14.1",
"Humanizer.Core.sr": "2.14.1",
"Humanizer.Core.sr-Latn": "2.14.1",
"Humanizer.Core.sv": "2.14.1",
"Humanizer.Core.th-TH": "2.14.1",
"Humanizer.Core.tr": "2.14.1",
"Humanizer.Core.uk": "2.14.1",
"Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
"Humanizer.Core.uz-Latn-UZ": "2.14.1",
"Humanizer.Core.vi": "2.14.1",
"Humanizer.Core.zh-CN": "2.14.1",
"Humanizer.Core.zh-Hans": "2.14.1",
"Humanizer.Core.zh-Hant": "2.14.1"
}
},
"Newtonsoft.Json": {
"type": "Direct",
"requested": "[13.0.3, )",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Humanizer.Core": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
},
"Humanizer.Core.af": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ar": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.az": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bg": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.bn-BD": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.cs": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.da": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.de": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.el": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.es": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fa": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fi-FI": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.fr-BE": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.he": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hu": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.hy": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.id": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.is": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.it": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ja": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ko-KR": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ku": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.lv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ms-MY": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.mt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nb-NO": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.nl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.pt": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ro": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.ru": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sl": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sr-Latn": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.sv": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.th-TH": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.tr": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uk": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Cyrl-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.uz-Latn-UZ": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.vi": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-CN": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hans": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
},
"Humanizer.Core.zh-Hant": {
"type": "Transitive",
"resolved": "2.14.1",
"contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
"dependencies": {
"Humanizer.Core": "[2.14.1]"
}
}
},
"net8.0/win-x64": {}
}
}