From c53f2fbad369b7c19d28b3b63511c95a04e33ff1 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 28 Mar 2025 13:36:27 -0400 Subject: [PATCH] Better represent .NET runtime packages (#3768) * clean up .NET runtime packages Signed-off-by: Alex Goodman * add runtime relationships Signed-off-by: Alex Goodman * remove runtime references from binary package name Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- syft/pkg/cataloger/dotnet/cataloger_test.go | 176 +++++-- .../cataloger/dotnet/deps_binary_cataloger.go | 101 +++- syft/pkg/cataloger/dotnet/deps_json.go | 86 ++-- syft/pkg/cataloger/dotnet/deps_json_test.go | 73 +++ syft/pkg/cataloger/dotnet/package.go | 102 +++- syft/pkg/cataloger/dotnet/package_test.go | 182 +++++++ .../test-fixtures/image-net2-app/.gitignore | 2 + .../test-fixtures/image-net2-app/Dockerfile | 17 + .../image-net2-app/src/Program.cs | 42 ++ .../image-net2-app/src/helloworld.csproj | 18 + .../image-net8-app-no-depjson/.gitignore | 1 + .../.gitignore | 2 + .../Dockerfile | 32 ++ .../src/Program.cs | 31 ++ .../src/dotnetapp.csproj | 15 + .../src/packages.lock.json | 459 ++++++++++++++++++ .../image-net8-app-with-runtime/.gitignore | 2 + .../image-net8-app-with-runtime/Dockerfile | 26 + .../src/Program.cs | 31 ++ .../src/dotnetapp.csproj | 15 + .../src/packages.lock.json | 459 ++++++++++++++++++ 21 files changed, 1766 insertions(+), 106 deletions(-) create mode 100644 syft/pkg/cataloger/dotnet/deps_json_test.go create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/Dockerfile create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/Program.cs create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/helloworld.csproj create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/Dockerfile create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/Program.cs create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/dotnetapp.csproj create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/packages.lock.json create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/Dockerfile create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/Program.cs create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/dotnetapp.csproj create mode 100644 syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/packages.lock.json diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go index bda2519f3..68d63cbfa 100644 --- a/syft/pkg/cataloger/dotnet/cataloger_test.go +++ b/syft/pkg/cataloger/dotnet/cataloger_test.go @@ -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 { diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index 5f34ce005..bba07858d 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -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\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 { diff --git a/syft/pkg/cataloger/dotnet/deps_json.go b/syft/pkg/cataloger/dotnet/deps_json.go index f5bddc064..f7add21f6 100644 --- a/syft/pkg/cataloger/dotnet/deps_json.go +++ b/syft/pkg/cataloger/dotnet/deps_json.go @@ -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.+)`) + +// 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 +} diff --git a/syft/pkg/cataloger/dotnet/deps_json_test.go b/syft/pkg/cataloger/dotnet/deps_json_test.go new file mode 100644 index 000000000..1c8158271 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/deps_json_test.go @@ -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) + } + }) + } +} diff --git a/syft/pkg/cataloger/dotnet/package.go b/syft/pkg/cataloger/dotnet/package.go index 7b39c552a..b96c6d0fc 100644 --- a/syft/pkg/cataloger/dotnet/package.go +++ b/syft/pkg/cataloger/dotnet/package.go @@ -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*\((?Pnet[^)]*[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") diff --git a/syft/pkg/cataloger/dotnet/package_test.go b/syft/pkg/cataloger/dotnet/package_test.go index 985b99be3..21ea0cddc 100644 --- a/syft/pkg/cataloger/dotnet/package_test.go +++ b/syft/pkg/cataloger/dotnet/package_test.go @@ -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) + } + }) + } +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/.gitignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/.gitignore new file mode 100644 index 000000000..b0b8376dc --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/.gitignore @@ -0,0 +1,2 @@ +/app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/Dockerfile b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/Dockerfile new file mode 100644 index 000000000..ade800686 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/Dockerfile @@ -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"] diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/Program.cs b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/Program.cs new file mode 100644 index 000000000..37c29fdf1 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/Program.cs @@ -0,0 +1,42 @@ +using System; +using Serilog; + +namespace HelloWorld +{ + /// + /// Main program class! + /// + public class Program + { + /// + /// Entry point for the application! + /// + /// Command line arguments! + 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(); + } + } +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/helloworld.csproj b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/helloworld.csproj new file mode 100644 index 000000000..9146aac7b --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net2-app/src/helloworld.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp2.2 + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-no-depjson/.gitignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-no-depjson/.gitignore index 6750dba93..b0b8376dc 100644 --- a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-no-depjson/.gitignore +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-no-depjson/.gitignore @@ -1 +1,2 @@ /app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/.gitignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/.gitignore new file mode 100644 index 000000000..b0b8376dc --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/.gitignore @@ -0,0 +1,2 @@ +/app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/Dockerfile b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/Dockerfile new file mode 100644 index 000000000..8b1034800 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/Dockerfile @@ -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"] + diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/Program.cs b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/Program.cs new file mode 100644 index 000000000..af4200bf1 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/Program.cs @@ -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"; +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/dotnetapp.csproj b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/dotnetapp.csproj new file mode 100644 index 000000000..de8b12f87 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/dotnetapp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + true + + + + + + + + diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/packages.lock.json b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/packages.lock.json new file mode 100644 index 000000000..a0f55dc2c --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime-nodepsjson/src/packages.lock.json @@ -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": {} + } +} \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/.gitignore b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/.gitignore new file mode 100644 index 000000000..b0b8376dc --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/.gitignore @@ -0,0 +1,2 @@ +/app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/Dockerfile b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/Dockerfile new file mode 100644 index 000000000..944d2d31b --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/Dockerfile @@ -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"] + diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/Program.cs b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/Program.cs new file mode 100644 index 000000000..af4200bf1 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/Program.cs @@ -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"; +} diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/dotnetapp.csproj b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/dotnetapp.csproj new file mode 100644 index 000000000..de8b12f87 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/dotnetapp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + true + + + + + + + + diff --git a/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/packages.lock.json b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/packages.lock.json new file mode 100644 index 000000000..a0f55dc2c --- /dev/null +++ b/syft/pkg/cataloger/dotnet/test-fixtures/image-net8-app-with-runtime/src/packages.lock.json @@ -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": {} + } +} \ No newline at end of file