diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app/Dockerfile b/syft/pkg/cataloger/dotnet/testdata/image-net8-app/Dockerfile index 8afe7e726..28cdca64d 100644 --- a/syft/pkg/cataloger/dotnet/testdata/image-net8-app/Dockerfile +++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app/Dockerfile @@ -72,9 +72,6 @@ RUN dotnet publish -r $RUNTIME --no-restore -o /app # > Humanizer.Core.zh-Hans 2.14.1 # > Humanizer.Core.zh-Hant 2.14.1 -# lets pull in a file that is not related at all and in fact is not a .NET binary either (this should be ignored) -RUN wget -O /app/jruby_windows_9_3_15_0.exe https://github.com/jruby/jruby/releases/download/9.3.15.0/jruby_windows_9_3_15_0.exe - FROM busybox WORKDIR /app COPY --from=build /app . diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go index facb70e73..baf00bd1a 100644 --- a/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go +++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go @@ -31,7 +31,9 @@ type dotNetBundleHeaderV2 struct { Flags uint64 } -// dotNetFileType represents the type of bundled file in the manifest +// dotNetFileType represents the type of bundled file in the manifest, as of V2 bundles (.NET 5+). +// note: V1 bundles (.NET Core 3.x) predate the unknown member at the head of this enum, so every type there is +// one less than the values below (e.g. deps.json is 2, not 3). see depsJSONFileType(). type dotNetFileType uint8 const ( @@ -121,8 +123,20 @@ func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) { return string(data), nil } +// depsJSONFileType returns the manifest file type that marks deps.json for the given bundle version. V1 bundles +// (.NET Core 3.x) have no unknown member in the file type enum, so all of their types are shifted down by one -- +// reading them with the V2+ values silently matches runtimeconfig.json instead. +func depsJSONFileType(majorVersion uint32) dotNetFileType { + if majorVersion < 2 { + return dotNetFileTypeDepsJSON - 1 + } + return dotNetFileTypeDepsJSON +} + // findDepsJSONInManifest parses manifest entries to find deps.json (for V1 bundles or fallback) func findDepsJSONInManifest(r io.ReadSeeker, numFiles int32, majorVersion uint32) (string, error) { + depsJSONType := depsJSONFileType(majorVersion) + for i := int32(0); i < numFiles; i++ { var offset, size int64 @@ -151,7 +165,7 @@ func findDepsJSONInManifest(r io.ReadSeeker, numFiles int32, majorVersion uint32 return "", err } - if fileType == dotNetFileTypeDepsJSON && size > 0 { + if fileType == depsJSONType && size > 0 { // save current position to resume manifest parsing if needed currentPos, err := r.Seek(0, io.SeekCurrent) if err != nil { diff --git a/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go index d25b33f7c..6bd1f110d 100644 --- a/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go +++ b/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go @@ -13,34 +13,43 @@ func Test_extractDepsJSONFromBundle_Versions(t *testing.T) { fixture string path string wantDepsJSON bool // true if deps.json should be found - wantJSONContain string // string that should be in the JSON (varies by .NET version) + wantJSONContain string // a string unique to the deps.json (and not the runtimeconfig.json) of this fixture }{ { - name: "V1 bundle (.NET Core 3.1)", - fixture: "image-dotnet31-single-file", - path: "/app/hello.exe", - wantDepsJSON: true, - wantJSONContain: "runtimeOptions", // .NET Core 3.1 uses runtimeOptions + name: "V1 bundle (.NET Core 3.1)", + fixture: "image-dotnet31-single-file", + path: "/app/hello.exe", + wantDepsJSON: true, + wantJSONContain: `"runtimeTarget": { + "name": ".NETCoreApp,Version=v3.1/win-x64"`, }, { - name: "V2 bundle (.NET 5)", - fixture: "image-dotnet5-single-file", - path: "/app/hello.exe", - wantDepsJSON: true, - wantJSONContain: "runtimeTarget", // .NET 5+ uses runtimeTarget + name: "V2 bundle (.NET 5)", + fixture: "image-dotnet5-single-file", + path: "/app/hello.exe", + wantDepsJSON: true, + wantJSONContain: `"runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0/win-x64"`, }, { - name: "V6 bundle (.NET 6)", - fixture: "image-dotnet6-single-file", - path: "/app/hello.exe", - wantDepsJSON: true, - wantJSONContain: "runtimeTarget", // .NET 6+ uses runtimeTarget + name: "V6 bundle (.NET 6)", + fixture: "image-dotnet6-single-file", + path: "/app/hello.exe", + wantDepsJSON: true, + wantJSONContain: `"runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0/win-x64"`, }, } + var fixtures []string + for _, tt := range tests { + fixtures = append(fixtures, tt.fixture) + } + resolvers := fixtureResolvers(t, fixtures...) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - reader := fixtureFile(t, tt.fixture, tt.path) + reader := fixtureFile(t, resolvers[tt.fixture], tt.path) defer reader.Close() got, err := Read(reader) diff --git a/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go b/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go index 0fdb62ace..4804de444 100644 --- a/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go +++ b/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go @@ -86,19 +86,23 @@ func Test_Read_DotNetDetection(t *testing.T) { wantErr: require.NoError, }, { - name: "jruby", - path: "/app/jruby_windows_9_3_15_0.exe", + // the apphost is a native launcher, not a managed assembly, so there is no CLR evidence to find in it. + // note the contrast with the single file deployment case below, which is the same apphost with the app + // (and the CLRDEBUGINFO resource) bundled into it. + name: "apphost", + path: "/app/dotnetapp.exe", fixture: "image-net8-app", wantCLR: false, // important! wantVR: map[string]string{ - "CompanyName": "JRuby Dev Team", - "FileDescription": "JRuby", - "FileVersion": "9.3.15.0", - "InternalName": "jruby", - "LegalCopyright": "JRuby Dev Team", - "OriginalFilename": "jruby_windows-x32_9_3_15_0.exe", - "ProductName": "JRuby", - "ProductVersion": "9.3.15.0", + "CompanyName": "dotnetapp", + "FileDescription": "dotnetapp", + "FileVersion": "1.0.0.0", + "InternalName": "dotnetapp.dll", + "LegalCopyright": " ", + "OriginalFilename": "dotnetapp.dll", + "ProductName": "dotnetapp", + "ProductVersion": "1.0.0", + "Assembly Version": "1.0.0.0", }, wantErr: require.NoError, }, @@ -125,13 +129,19 @@ func Test_Read_DotNetDetection(t *testing.T) { }, } + var fixtures []string + for _, tt := range tests { + fixtures = append(fixtures, tt.fixture) + } + resolvers := fixtureResolvers(t, fixtures...) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.wantErr == nil { tt.wantErr = require.NoError } - reader := fixtureFile(t, tt.fixture, tt.path) + reader := fixtureFile(t, resolvers[tt.fixture], tt.path) got, err := Read(reader) tt.wantErr(t, err) @@ -153,16 +163,31 @@ func Test_Read_DotNetDetection(t *testing.T) { } } -func fixtureFile(t *testing.T, fixture, path string) file.LocationReadCloser { - img := imagetest.GetFixtureImage(t, "docker-archive", fixture) +// fixtureResolvers loads each distinct image fixture once, from the parent test, so that subtests reading multiple +// files out of the same image don't each pay for loading the docker archive again (which dominates the runtime of +// these tests, especially under -race). +func fixtureResolvers(t *testing.T, fixtures ...string) map[string]file.Resolver { + resolvers := make(map[string]file.Resolver) + for _, fixture := range fixtures { + if _, ok := resolvers[fixture]; ok { + continue + } - s := stereoscopesource.New(img, stereoscopesource.ImageConfig{ - Reference: fixture, - }) + img := imagetest.GetFixtureImage(t, "docker-archive", fixture) - r, err := s.FileResolver(source.SquashedScope) - require.NoError(t, err) + s := stereoscopesource.New(img, stereoscopesource.ImageConfig{ + Reference: fixture, + }) + r, err := s.FileResolver(source.SquashedScope) + require.NoError(t, err) + + resolvers[fixture] = r + } + return resolvers +} + +func fixtureFile(t *testing.T, r file.Resolver, path string) file.LocationReadCloser { locs, err := r.FilesByPath(path) require.NoError(t, err) diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile index 71fdc97f7..5a1b03e07 100644 --- a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile @@ -1,9 +1,12 @@ +# note: this is published framework-dependent on purpose: the bundle format (what these fixtures exercise) is +# identical either way, but bundling the runtime makes the fixture ~60MB instead of ~160KB, which dominates +# the runtime of the tests that load this image. FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/core/sdk:3.1 AS build WORKDIR /src COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ - -p:SelfContained=true \ + -p:SelfContained=false \ -o /app FROM busybox diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile index d9ab0b574..0edf7deff 100644 --- a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile @@ -1,9 +1,12 @@ +# note: this is published framework-dependent on purpose: the bundle format (what these fixtures exercise) is +# identical either way, but bundling the runtime makes the fixture ~60MB instead of ~160KB, which dominates +# the runtime of the tests that load this image. FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:5.0 AS build WORKDIR /src COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ - -p:SelfContained=true \ + -p:SelfContained=false \ -o /app FROM busybox diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile index bf89e026f..4b6a922d0 100644 --- a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile +++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile @@ -1,9 +1,12 @@ +# note: this is published framework-dependent on purpose: the bundle format (what these fixtures exercise) is +# identical either way, but bundling the runtime makes the fixture ~60MB instead of ~160KB, which dominates +# the runtime of the tests that load this image. FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY src . RUN dotnet publish -c Release -r win-x64 \ -p:PublishSingleFile=true \ - -p:SelfContained=true \ + -p:SelfContained=false \ -o /app FROM busybox