From e7f1a803e7587741a6133800329927280ee5098e Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Mon, 29 Jun 2026 13:52:55 -0400 Subject: [PATCH] =?UTF-8?q?fixed=20dotnet=20cataloger=20can't=20find=20pac?= =?UTF-8?q?kages=20from=20deps.json=20in=20linux=20el=E2=80=A6=20(#4517)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed dotnet cataloger can't find packages from deps.json in linux elf, fixed #4514 Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * split bundle and PE concerns Signed-off-by: Alex Goodman * limit resource usage of readall call Signed-off-by: Alex Goodman * removed duplicat Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * make sure the first 4 bytes in elf arent lostt Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * revert readelfbundle func, check size of readdeps json Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * revert readelfbundle func, check size of readdeps json, fixed #4514 Co-authored-by: Alex Goodman Signed-off-by: Rez Moss Signed-off-by: Alex Goodman * move dotnet net8 linux fixture to testdata convention Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Signed-off-by: Alex Goodman * address malformed elf size claims + add tests Signed-off-by: Alex Goodman * dont key off of cataloger name in testing Signed-off-by: Alex Goodman --------- Signed-off-by: Rez Moss Signed-off-by: Alex Goodman Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Alex Goodman Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- cmd/syft/internal/options/catalog.go | 4 +- .../cataloger/binary/pe_package_cataloger.go | 2 +- syft/pkg/cataloger/dotnet/cataloger_test.go | 182 ++++++- .../cataloger/dotnet/deps_binary_cataloger.go | 72 +++ syft/pkg/cataloger/dotnet/package.go | 22 +- syft/pkg/cataloger/dotnet/pe.go | 2 +- .../.gitignore | 2 + .../Dockerfile | 16 + .../src/Program.cs | 48 ++ .../src/dotnetapp.csproj | 15 + .../src/packages.lock.json | 459 ++++++++++++++++++ .../internal/{pe => dotnet/bundle}/bundle.go | 97 +--- .../internal/dotnet/bundle/bundle_elf.go | 83 ++++ .../internal/dotnet/bundle/bundle_elf_test.go | 63 +++ .../cataloger/internal/dotnet/pe/bundle.go | 103 ++++ .../internal/{ => dotnet}/pe/bundle_test.go | 0 .../cataloger/internal/{ => dotnet}/pe/pe.go | 0 .../internal/{ => dotnet}/pe/pe_test.go | 0 .../{ => dotnet}/pe/testdata/Makefile | 0 .../image-dotnet31-single-file/Dockerfile | 2 +- .../image-dotnet31-single-file/src/Program.cs | 0 .../src/hello.csproj | 0 .../image-dotnet5-single-file/Dockerfile | 2 +- .../image-dotnet5-single-file/src/Program.cs | 0 .../src/hello.csproj | 0 .../image-dotnet6-single-file/Dockerfile | 2 +- .../image-dotnet6-single-file/src/Program.cs | 0 .../src/hello.csproj | 0 .../dotnet/pe/testdata/image-net8-app | 1 + .../pe/testdata/image-net8-app-single-file | 1 + .../testdata/net8-app-single-file.deps.json | 0 .../internal/pe/testdata/image-net8-app | 1 - .../pe/testdata/image-net8-app-single-file | 1 - 33 files changed, 1070 insertions(+), 110 deletions(-) create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json rename syft/pkg/cataloger/internal/{pe => dotnet/bundle}/bundle.go (52%) create mode 100644 syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go create mode 100644 syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go create mode 100644 syft/pkg/cataloger/internal/dotnet/pe/bundle.go rename syft/pkg/cataloger/internal/{ => dotnet}/pe/bundle_test.go (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/pe.go (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/pe_test.go (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/Makefile (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/Dockerfile (95%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/src/Program.cs (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/src/hello.csproj (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/Dockerfile (95%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/src/Program.cs (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/src/hello.csproj (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/Dockerfile (95%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/src/Program.cs (100%) rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/src/hello.csproj (100%) create mode 120000 syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app create mode 120000 syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/net8-app-single-file.deps.json (100%) delete mode 120000 syft/pkg/cataloger/internal/pe/testdata/image-net8-app delete mode 120000 syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index bcb8ab66f..1b74df358 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -176,7 +176,9 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL). WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL). WithPropagateDLLClaimsToParents(cfg.Dotnet.PropagateDLLClaimsToParents). - WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected), + WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected). + WithExcludeProjectReferences(cfg.Dotnet.ExcludeProjectReferences), + Golang: golang.DefaultCatalogerConfig(). WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)). WithLocalModCacheDir(cfg.Golang.LocalModCacheDir). diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger.go b/syft/pkg/cataloger/binary/pe_package_cataloger.go index ba90d8835..16292be76 100644 --- a/syft/pkg/cataloger/binary/pe_package_cataloger.go +++ b/syft/pkg/cataloger/binary/pe_package_cataloger.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" - "github.com/anchore/syft/syft/pkg/cataloger/internal/pe" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/pe" ) // NewPEPackageCataloger returns a cataloger that interprets packages from DLL, EXE, and BPL files. diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go index 65076f9f1..04b2caf20 100644 --- a/syft/pkg/cataloger/dotnet/cataloger_test.go +++ b/syft/pkg/cataloger/dotnet/cataloger_test.go @@ -14,10 +14,11 @@ import ( func TestCataloger_Globs(t *testing.T) { tests := []struct { - name string - fixture string - cataloger pkg.Cataloger - expected []string + name string + fixture string + cataloger pkg.Cataloger + expected []string + ignoreUnfulfilled []string }{ { name: "obtain deps.json files", @@ -47,15 +48,21 @@ func TestCataloger_Globs(t *testing.T) { "src/something.dll", "src/something.exe", }, + // the binary cataloger probes executables by MIME type to find embedded bundles, + // but the glob fixtures aren't real binaries so those queries go unfulfilled + ignoreUnfulfilled: []string{"application/x-executable", "application/x-sharedlib"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pkgtest.NewCatalogTester(). + tester := pkgtest.NewCatalogTester(). FromDirectory(t, test.fixture). - ExpectsResolverContentQueries(test.expected). - TestCataloger(t, test.cataloger) + ExpectsResolverContentQueries(test.expected) + if len(test.ignoreUnfulfilled) > 0 { + tester = tester.IgnoreUnfulfilledPathResponses(test.ignoreUnfulfilled...) + } + tester.TestCataloger(t, test.cataloger) }) } } @@ -1105,6 +1112,167 @@ func TestCataloger(t *testing.T) { }, assertion: assertSingleFileDeployment, }, + { + name: "combined cataloger (single file linux)", + fixture: "image-net8-app-single-file-linux", + cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()), + expectedPkgs: []string{ + "Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.af @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.az @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.da @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.de @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.el @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.es @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.he @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.id @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.is @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.it @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp)", + "Newtonsoft.Json @ 13.0.3 (/app/dotnetapp)", + "dotnetapp @ 1.0.0 (/app/dotnetapp)", + "runtimepack.Microsoft.NETCore.App.Runtime.linux-musl-x64 @ 8.0.14 (/app/dotnetapp)", + }, + expectedRels: []string{ + "Humanizer @ 2.14.1 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.af @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.az @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.da @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.de @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.el @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.es @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.he @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.id @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.is @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.it @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.af @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.az @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.da @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.de @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.el @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.es @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.he @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.id @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.is @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.it @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)", + "Newtonsoft.Json @ 13.0.3 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)", + "runtimepack.Microsoft.NETCore.App.Runtime.linux-musl-x64 @ 8.0.14 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)", + }, + }, { name: "deps cataloger (self-contained)", fixture: "image-net8-app-self-contained", diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go index fbf53e648..ea96b1710 100644 --- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go +++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go @@ -1,6 +1,7 @@ package dotnet import ( + "bytes" "context" "fmt" "io" @@ -16,7 +17,9 @@ import ( "github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle" ) const ( @@ -26,6 +29,8 @@ const ( bplGlob = "**/*.bpl" ) +var elfMagic = []byte{0x7f, 'E', 'L', 'F'} + // depsBinaryCataloger will search for both deps.json evidence and PE file evidence to create packages. All packages // from both sources are raised up, but with one merge operation applied; If a deps.json package reference can be // correlated with a PE file, the PE file is attached to the package as supporting evidence. @@ -38,11 +43,16 @@ func (c depsBinaryCataloger) Name() string { } func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen + elfDepsJSONs, elfUnknowns := findELFBundledDepsJSON(resolver) + depJSONDocs, unknowns, err := findDepsJSON(resolver) if err != nil { return nil, nil, err } + depJSONDocs = append(depJSONDocs, elfDepsJSONs...) + unknowns = unknown.Join(unknowns, elfUnknowns) + peFiles, ldpeUnknownErr, err := findPEFiles(resolver) if err != nil { return nil, nil, err @@ -520,6 +530,68 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) { return ldpe, nil } +func findELFBundledDepsJSON(resolver file.Resolver) ([]logicalDepsJSON, error) { + locs, err := resolver.FilesByMIMEType("application/x-executable", "application/x-sharedlib") + if err != nil { + return nil, nil + } + + var depsJSONs []logicalDepsJSON + var unknownErr error + for _, loc := range locs { + doc, err := readELFBundledDepsJSON(resolver, loc) + if err != nil { + unknownErr = unknown.Append(unknownErr, loc, err) + continue + } + if doc != nil { + depsJSONs = append(depsJSONs, *doc) + } + } + + return depsJSONs, unknownErr +} + +func readELFBundledDepsJSON(resolver file.Resolver, loc file.Location) (*logicalDepsJSON, error) { + reader, err := resolver.FileContentsByLocation(loc) + if err != nil { + return nil, err + } + defer internal.CloseAndLogError(reader, loc.RealPath) + + header := make([]byte, 4) + if _, err := io.ReadFull(reader, header); err != nil { + return nil, nil + } + if !bytes.Equal(header, elfMagic) { + return nil, nil + } + + uReader, err := unionreader.GetUnionReader(reader) + if err != nil { + return nil, err + } + + depsJSON, err := bundle.ExtractDepsJSONFromELFBundle(uReader) + if err != nil { + return nil, err + } + + if depsJSON == "" { + return nil, nil + } + + doc, err := newDepsJSON(file.NewLocationReadCloser(loc, io.NopCloser(strings.NewReader(depsJSON)))) + if err != nil || doc == nil { + return nil, nil + } + + doc.Location = loc + lDoc := getLogicalDepsJSON(*doc, nil) + + return &lDoc, nil +} + func extractEmbeddedDeps(pe logicalPE) *logicalDepsJSON { doc, err := newDepsJSON(file.NewLocationReadCloser(pe.Location, io.NopCloser(strings.NewReader(pe.EmbeddedDepsJSON)))) if err != nil || doc == nil { diff --git a/syft/pkg/cataloger/dotnet/package.go b/syft/pkg/cataloger/dotnet/package.go index 9a11745fe..ac2361d60 100644 --- a/syft/pkg/cataloger/dotnet/package.go +++ b/syft/pkg/cataloger/dotnet/package.go @@ -20,6 +20,7 @@ var ( spaceRegex = regexp.MustCompile(`[\s\xa0]+`) numberRegex = regexp.MustCompile(`\d`) versionPunctuationRegex = regexp.MustCompile(`[.,]+`) + nonPrintableRegex = regexp.MustCompile(`[\x00-\x1f]`) ) type runtimeFamily string @@ -204,18 +205,29 @@ func cleanVersionResourceField(values ...string) string { return "" } +var ( + depsJSONPathRegex = regexp.MustCompile(`([^\\\/]+)\.deps\.json$`) + exePathRegex = regexp.MustCompile(`([^\\\/]+)\.exe$`) + singleFileRegex = regexp.MustCompile(`([^\\\/]+)$`) +) + func getDepsJSONFilePrefix(p string) string { - r := regexp.MustCompile(`([^\\\/]+)\.deps\.json$`) - match := r.FindStringSubmatch(p) + match := depsJSONPathRegex.FindStringSubmatch(p) if len(match) > 1 { return match[1] } - r = regexp.MustCompile(`([^\\\/]+)\.exe$`) - match = r.FindStringSubmatch(p) + match = exePathRegex.FindStringSubmatch(p) if len(match) > 1 { return match[1] } + + // Handle ELF + match = singleFileRegex.FindStringSubmatch(p) + if len(match) > 1 && !strings.Contains(match[1], ".") { + return match[1] + } + return "" } @@ -336,7 +348,7 @@ func spaceNormalize(value string) string { // Consolidate all whitespace. value = spaceRegex.ReplaceAllString(value, " ") // Remove non-printable characters. - value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "") + value = nonPrintableRegex.ReplaceAllString(value, "") // Consolidate again and trim. value = spaceRegex.ReplaceAllString(value, " ") value = strings.TrimSpace(value) diff --git a/syft/pkg/cataloger/dotnet/pe.go b/syft/pkg/cataloger/dotnet/pe.go index 77e433c06..a1257c56c 100644 --- a/syft/pkg/cataloger/dotnet/pe.go +++ b/syft/pkg/cataloger/dotnet/pe.go @@ -2,7 +2,7 @@ package dotnet import ( "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/pkg/cataloger/internal/pe" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/pe" ) // logicalPE represents a PE file within the context of a .NET project (considering the deps.json file). diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore new file mode 100644 index 000000000..b0b8376dc --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore @@ -0,0 +1,2 @@ +/app +/extract.sh \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile new file mode 100644 index 000000000..88095de5d --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile @@ -0,0 +1,16 @@ +# This is the same as the net8-app image, however, the entire .NET runtime is compiled into a single binary, residing with the application. +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build +ARG RUNTIME=linux-musl-x64 +WORKDIR /src + +COPY src/*.csproj . +COPY src/packages.lock.json . +RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode --force-evaluate + +COPY src/ . +RUN dotnet publish -r $RUNTIME -p:PublishSingleFile=true -p:RestorePackagesWithLockFile=true -p:ErrorOnDuplicatePublishOutputFiles=false --self-contained true -p:EnableCompressionInSingleFile=true -p:DebugSymbols=false -p:DebugType=None -o /app + + +FROM busybox +WORKDIR /app +COPY --from=build /app . \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs new file mode 100644 index 000000000..d1856dbc2 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs @@ -0,0 +1,48 @@ +using System; +using Humanizer; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +namespace IndirectDependencyExample +{ + class Program + { + static void Main(string[] args) + { + string runtimeInfo = "hello world!\n"; + Console.WriteLine(runtimeInfo); + + + Console.WriteLine($"\"this_is_a_test\" to title case: {"this_is_a_test".Humanize(LetterCasing.Title)}"); + + const string jsonString = @" + { + ""message"": ""Hello from JSON!"", + ""details"": { + ""timestamp"": ""2025-03-26T12:00:00Z"", + ""version"": ""1.0.0"", + ""metadata"": { + ""author"": ""Claude"", + ""environment"": ""Development"" + } + }, + ""items"": [ + { + ""id"": 1, + ""name"": ""Item One"" + }, + { + ""id"": 2, + ""name"": ""Item Two"" + } + ] + }"; + + JObject jsonObject = JObject.Parse(jsonString); + + string message = (string)jsonObject["message"]; + Console.WriteLine($"Message: {message}"); + } + } +} \ No newline at end of file diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj new file mode 100644 index 000000000..de8b12f87 --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + true + + + + + + + + diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json new file mode 100644 index 000000000..a0f55dc2c --- /dev/null +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/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/internal/pe/bundle.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go similarity index 52% rename from syft/pkg/cataloger/internal/pe/bundle.go rename to syft/pkg/cataloger/internal/dotnet/bundle/bundle.go index a360441d5..facb70e73 100644 --- a/syft/pkg/cataloger/internal/pe/bundle.go +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go @@ -1,8 +1,6 @@ -package pe +package bundle import ( - "bytes" - "debug/pe" "encoding/binary" "errors" "fmt" @@ -45,92 +43,8 @@ const ( dotNetFileTypeSymbols ) -// extractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle. -// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into -// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed -// in a placeholder location within the PE structure, pointing to the bundle header which contains -// file entry metadata. For V2+ bundles (.NET 5+), the header includes direct offsets to deps.json; -// for V1 bundles (.NET Core 3.x), we parse the manifest to locate it. -// -// ┌──────────────────────────────────┐ -// │ PE AppHost Binary │ Standard PE structure -// │ ... │ -// │ [8B offset][32B signature] │ Bundle marker (in placeholder within PE) -// │ ... │ -// ├──────────────────────────────────┤ -// │ Bundled Files │ Raw file contents (assemblies, deps.json, etc.) -// ├──────────────────────────────────┤ -// │ Bundle Header │ Version info, file count, deps.json offset (V2+) -// │ File Manifest │ Per-file: offset, size, type, path -// └──────────────────────────────────┘ -// -// Parsing strategy: -// 1. Search only the PE portion (using section headers) for the bundle signature -// 2. Read 8 bytes before signature to get header offset -// 3. Parse header to get deps.json location (V2+) or scan manifest entries (V1) -// -// See related documentation for more information: -// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md -// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/bundler.md -// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs -// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs -// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/header.h -// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h -// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h -func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) { - headerOffset, err := findBundleHeaderOffset(r, sections) - if err != nil { - return "", err - } - if headerOffset == 0 { - return "", nil // not a .NET single-file bundle - } - - return readDepsJSONFromBundleHeader(r, headerOffset) -} - -// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset. -// Returns 0 if no bundle marker is found (not a single-file bundle). -func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) { - peEndOffset := calculatePEEndOffset(sections) - - if _, err := r.Seek(0, io.SeekStart); err != nil { - return 0, err - } - - peData := make([]byte, peEndOffset) - n, err := io.ReadFull(r, peData) - if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { - return 0, err - } - peData = peData[:n] - - idx := bytes.Index(peData, dotNetBundleSignature) - if idx == -1 || idx < 8 { - return 0, nil - } - - // the header offset is stored in the 8 bytes immediately before the signature - headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx])) - return headerOffset, nil -} - -// calculatePEEndOffset determines where the PE structure ends based on section headers, -// adding padding for alignment. This bounds our search for the bundle marker. -func calculatePEEndOffset(sections []pe.SectionHeader32) int64 { - var peEndOffset int64 - for _, sec := range sections { - endOfSection := int64(sec.PointerToRawData) + int64(sec.SizeOfRawData) - if endOfSection > peEndOffset { - peEndOffset = endOfSection - } - } - // add buffer for alignment padding after sections - return peEndOffset + 4096 -} - -// readDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content. -func readDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) { +// ReadDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content. +func ReadDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) { if _, err := r.Seek(headerOffset, io.SeekStart); err != nil { return "", err } @@ -194,6 +108,9 @@ func read7BitEncodedInt(r io.Reader) (int, error) { // readDepsJSONAtOffset reads deps.json content at a specific offset using seeks (avoiding loading entire file) func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) { + if size <= 0 || size > 3*1024*1024 { // 3MB max deps.json size in dotnet bundle + return "", nil + } if _, err := r.Seek(offset, io.SeekStart); err != nil { return "", fmt.Errorf("failed to seek to deps.json at offset %d: %w", offset, err) } @@ -206,7 +123,7 @@ func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) { // findDepsJSONInManifest parses manifest entries to find deps.json (for V1 bundles or fallback) func findDepsJSONInManifest(r io.ReadSeeker, numFiles int32, majorVersion uint32) (string, error) { - for range numFiles { + for i := int32(0); i < numFiles; i++ { var offset, size int64 if err := binary.Read(r, binary.LittleEndian, &offset); err != nil { diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go new file mode 100644 index 000000000..8725a7d01 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go @@ -0,0 +1,83 @@ +package bundle + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "io" + + "github.com/anchore/syft/syft/internal/unionreader" +) + +// ExtractDepsJSONFromELFBundle extracts the deps.json content from a .net singlefile +// bundle contained within an ELF bin +func ExtractDepsJSONFromELFBundle(r unionreader.UnionReader) (string, error) { + headerOffset, err := findBundleHeaderOffsetInELF(r) + if err != nil || headerOffset == 0 { + return "", err + } + return ReadDepsJSONFromBundleHeader(r, headerOffset) +} + +func findBundleHeaderOffsetInELF(r unionreader.UnionReader) (int64, error) { + elfFile, err := elf.NewFile(r) + if err != nil { + return 0, nil + } + + elfEndOffset := calculateELFEndOffset(elfFile) + if elfEndOffset == 0 { + return 0, nil + } + + // clamp to the actual file size so a malformed ELF (with bogus segment/section + // offsets+sizes) can't drive an arbitrarily large allocation below. + fileSize, err := r.Seek(0, io.SeekEnd) + if err != nil { + return 0, err + } + if elfEndOffset > fileSize { + elfEndOffset = fileSize + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return 0, err + } + + searchData := make([]byte, elfEndOffset) + n, err := io.ReadFull(r, searchData) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return 0, err + } + searchData = searchData[:n] + + idx := bytes.Index(searchData, dotNetBundleSignature) + if idx == -1 || idx < 8 { + return 0, nil + } + + return int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx])), nil +} + +func calculateELFEndOffset(f *elf.File) int64 { + var endOffset int64 + + for _, prog := range f.Progs { + end := int64(prog.Off) + int64(prog.Filesz) + if end > endOffset { + endOffset = end + } + } + + for _, sec := range f.Sections { + if sec.Type == elf.SHT_NOBITS { + continue + } + end := int64(sec.Offset) + int64(sec.Size) + if end > endOffset { + endOffset = end + } + } + return endOffset + 4096 +} diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go new file mode 100644 index 000000000..7a49dbea0 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go @@ -0,0 +1,63 @@ +package bundle + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// readSeekCloser adapts a *bytes.Reader to the unionreader.UnionReader interface (adds Close). +type readSeekCloser struct { + *bytes.Reader +} + +func (readSeekCloser) Close() error { return nil } + +// buildELFWithHugeFilesz returns a minimal, parseable ELF64 whose single program header +// declares an absurd p_filesz. calculateELFEndOffset will compute a huge end offset; the +// clamp in findBundleHeaderOffsetInELF must keep the allocation bounded to the real file. +func buildELFWithHugeFilesz() []byte { + const ( + ehSize = 64 + phSize = 56 + phOff = ehSize + phCount = 1 + ) + buf := make([]byte, ehSize+phSize) + + // e_ident + copy(buf[0:4], []byte{0x7f, 'E', 'L', 'F'}) + buf[4] = 2 // ELFCLASS64 + buf[5] = 1 // ELFDATA2LSB + buf[6] = 1 // EV_CURRENT + + le := binary.LittleEndian + le.PutUint16(buf[16:], 2) // e_type = ET_EXEC + le.PutUint16(buf[18:], 0x3e) // e_machine = x86-64 + le.PutUint32(buf[20:], 1) // e_version + le.PutUint64(buf[32:], phOff) // e_phoff + le.PutUint16(buf[52:], ehSize) // e_ehsize + le.PutUint16(buf[54:], phSize) // e_phentsize + le.PutUint16(buf[56:], phCount) // e_phnum + // e_shoff/e_shnum left zero so no sections are parsed + + ph := buf[phOff:] + le.PutUint32(ph[0:], 1) // p_type = PT_LOAD + le.PutUint64(ph[16:], 0) // p_offset + le.PutUint64(ph[32:], 1<<60) // p_filesz (bogus, attacker-controlled) + le.PutUint64(ph[40:], 1<<60) // p_memsz + return buf +} + +func TestExtractDepsJSONFromELFBundle_MalformedFileszDoesNotOverAllocate(t *testing.T) { + data := buildELFWithHugeFilesz() + r := readSeekCloser{bytes.NewReader(data)} + + // must not OOM/panic on the bogus p_filesz, and find no bundle signature + content, err := ExtractDepsJSONFromELFBundle(r) + require.NoError(t, err) + assert.Empty(t, content) +} diff --git a/syft/pkg/cataloger/internal/dotnet/pe/bundle.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle.go new file mode 100644 index 000000000..791b476b2 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/pe/bundle.go @@ -0,0 +1,103 @@ +package pe + +import ( + "bytes" + "debug/pe" + "encoding/binary" + "errors" + "io" + + "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle" +) + +// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles. +var dotNetBundleSignature = []byte{ + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae, +} + +// ExtractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle. +// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into +// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed +// in a placeholder location within the PE structure, pointing to the bundle header which contains +// file entry metadata. For V2+ bundles (.NET 5+), the header includes direct offsets to deps.json; +// for V1 bundles (.NET Core 3.x), we parse the manifest to locate it. +// +// ┌──────────────────────────────────┐ +// │ PE AppHost Binary │ Standard PE structure +// │ ... │ +// │ [8B offset][32B signature] │ Bundle marker (in placeholder within PE) +// │ ... │ +// ├──────────────────────────────────┤ +// │ Bundled Files │ Raw file contents (assemblies, deps.json, etc.) +// ├──────────────────────────────────┤ +// │ Bundle Header │ Version info, file count, deps.json offset (V2+) +// │ File Manifest │ Per-file: offset, size, type, path +// └──────────────────────────────────┘ +// +// Parsing strategy: +// 1. Search only the PE portion (using section headers) for the bundle signature +// 2. Read 8 bytes before signature to get header offset +// 3. Parse header to get deps.json location (V2+) or scan manifest entries (V1) +// +// See related documentation for more information: +// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md +// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/bundler.md +// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs +// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs +// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/header.h +// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h +// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h +func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) { + headerOffset, err := findBundleHeaderOffset(r, sections) + if err != nil { + return "", err + } + if headerOffset == 0 { + return "", nil // not a .NET single-file bundle + } + + return bundle.ReadDepsJSONFromBundleHeader(r, headerOffset) +} + +// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset. +// Returns 0 if no bundle marker is found (not a single-file bundle). +func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) { + peEndOffset := calculatePEEndOffset(sections) + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return 0, err + } + + peData := make([]byte, peEndOffset) + n, err := io.ReadFull(r, peData) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return 0, err + } + peData = peData[:n] + + idx := bytes.Index(peData, dotNetBundleSignature) + if idx == -1 || idx < 8 { + return 0, nil + } + + // the header offset is stored in the 8 bytes immediately before the signature + headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx])) + return headerOffset, nil +} + +// calculatePEEndOffset determines where the PE structure ends based on section headers, +// adding padding for alignment. This bounds our search for the bundle marker. +func calculatePEEndOffset(sections []pe.SectionHeader32) int64 { + var peEndOffset int64 + for _, sec := range sections { + endOfSection := int64(sec.PointerToRawData) + int64(sec.SizeOfRawData) + if endOfSection > peEndOffset { + peEndOffset = endOfSection + } + } + // add buffer for alignment padding after sections + return peEndOffset + 4096 +} diff --git a/syft/pkg/cataloger/internal/pe/bundle_test.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go similarity index 100% rename from syft/pkg/cataloger/internal/pe/bundle_test.go rename to syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go diff --git a/syft/pkg/cataloger/internal/pe/pe.go b/syft/pkg/cataloger/internal/dotnet/pe/pe.go similarity index 100% rename from syft/pkg/cataloger/internal/pe/pe.go rename to syft/pkg/cataloger/internal/dotnet/pe/pe.go diff --git a/syft/pkg/cataloger/internal/pe/pe_test.go b/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go similarity index 100% rename from syft/pkg/cataloger/internal/pe/pe_test.go rename to syft/pkg/cataloger/internal/dotnet/pe/pe_test.go diff --git a/syft/pkg/cataloger/internal/pe/testdata/Makefile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/Makefile similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/Makefile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/Makefile diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile similarity index 95% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile index dc7f30763..71fdc97f7 100644 --- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/core/sdk:3.1 AS build WORKDIR /src -COPY src/ . +COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ -p:SelfContained=true \ diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/Program.cs similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/Program.cs rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/Program.cs diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/hello.csproj similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/hello.csproj rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/hello.csproj diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile similarity index 95% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile index 71ad584de..d9ab0b574 100644 --- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:5.0 AS build WORKDIR /src -COPY src/ . +COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ -p:SelfContained=true \ diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/Program.cs similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/Program.cs rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/Program.cs diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/hello.csproj similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/hello.csproj rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/hello.csproj diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile similarity index 95% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile index 2089ef9e6..bf89e026f 100644 --- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src -COPY src/ . +COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ -p:SelfContained=true \ diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/Program.cs similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/Program.cs rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/Program.cs diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/hello.csproj similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/hello.csproj rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/hello.csproj diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app new file mode 120000 index 000000000..34cfd36fd --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app @@ -0,0 +1 @@ +../../../../dotnet/testdata/image-net8-app \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file new file mode 120000 index 000000000..964979916 --- /dev/null +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file @@ -0,0 +1 @@ +../../../../dotnet/testdata/image-net8-app-single-file \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/pe/testdata/net8-app-single-file.deps.json b/syft/pkg/cataloger/internal/dotnet/pe/testdata/net8-app-single-file.deps.json similarity index 100% rename from syft/pkg/cataloger/internal/pe/testdata/net8-app-single-file.deps.json rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/net8-app-single-file.deps.json diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app b/syft/pkg/cataloger/internal/pe/testdata/image-net8-app deleted file mode 120000 index 497e7d17c..000000000 --- a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app +++ /dev/null @@ -1 +0,0 @@ -../../../dotnet/testdata/image-net8-app \ No newline at end of file diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file b/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file deleted file mode 120000 index 884c00cff..000000000 --- a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file +++ /dev/null @@ -1 +0,0 @@ -../../../dotnet/testdata/image-net8-app-single-file \ No newline at end of file