diff --git a/docs/docs/coverage/language/dotnet.md b/docs/docs/coverage/language/dotnet.md index 0a05454365e9..311e5c010b2d 100644 --- a/docs/docs/coverage/language/dotnet.md +++ b/docs/docs/coverage/language/dotnet.md @@ -21,6 +21,9 @@ The following table provides an outline of the features Trivy offers. ## *.deps.json Trivy parses `*.deps.json` files. Trivy currently excludes dev dependencies from the report. +!!! note + Trivy only includes runtime dependencies in the report. + ## packages.config Trivy only finds dependency names and versions from `packages.config` files. To build dependency graph, it is better to use `packages.lock.json` files. diff --git a/integration/testdata/dotnet.json.golden b/integration/testdata/dotnet.json.golden index 778b1270fcf2..4c76a08fc300 100644 --- a/integration/testdata/dotnet.json.golden +++ b/integration/testdata/dotnet.json.golden @@ -22,10 +22,11 @@ "Type": "dotnet-core", "Packages": [ { + "ID": "Newtonsoft.Json/9.0.1", "Name": "Newtonsoft.Json", "Identifier": { "PURL": "pkg:nuget/Newtonsoft.Json@9.0.1", - "UID": "19955f480b8a6340" + "UID": "e678401f5d07418a" }, "Version": "9.0.1", "Layer": {}, @@ -40,10 +41,11 @@ "Vulnerabilities": [ { "VulnerabilityID": "GHSA-5crp-9r3c-p9vr", + "PkgID": "Newtonsoft.Json/9.0.1", "PkgName": "Newtonsoft.Json", "PkgIdentifier": { "PURL": "pkg:nuget/Newtonsoft.Json@9.0.1", - "UID": "19955f480b8a6340" + "UID": "e678401f5d07418a" }, "InstalledVersion": "9.0.1", "FixedVersion": "13.0.1", diff --git a/pkg/dependency/id.go b/pkg/dependency/id.go index 577ed5d0ac41..77dd85bed3e0 100644 --- a/pkg/dependency/id.go +++ b/pkg/dependency/id.go @@ -20,7 +20,8 @@ func ID(ltype types.LangType, name, version string) string { sep := "@" switch ltype { - case types.Conan: + // cf. https://github.com/dotnet/sdk/blob/529132850841a6bcfce96799262ce688e3851875/documentation/specs/runtime-configuration-file.md#targets-section-depsjson + case types.Conan, types.DotNetCore: sep = "/" case types.GoModule, types.GoBinary: // Return a module ID according the Go way. diff --git a/pkg/dependency/parser/dotnet/core_deps/parse.go b/pkg/dependency/parser/dotnet/core_deps/parse.go index 4314e9af9b3d..7fc8d3df5d5e 100644 --- a/pkg/dependency/parser/dotnet/core_deps/parse.go +++ b/pkg/dependency/parser/dotnet/core_deps/parse.go @@ -2,23 +2,51 @@ package core_deps import ( "io" + "sort" "strings" + "sync" "github.com/liamg/jfather" + "github.com/samber/lo" "golang.org/x/xerrors" + "github.com/aquasecurity/trivy/pkg/dependency" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" xio "github.com/aquasecurity/trivy/pkg/x/io" ) +type dotNetDependencies struct { + Libraries map[string]dotNetLibrary `json:"libraries"` + RuntimeTarget RuntimeTarget `json:"runtimeTarget"` + Targets map[string]map[string]TargetLib `json:"targets"` +} + +type dotNetLibrary struct { + Type string `json:"type"` + StartLine int + EndLine int +} + +type RuntimeTarget struct { + Name string `json:"name"` +} + +type TargetLib struct { + Runtime any `json:"runtime"` + RuntimeTargets any `json:"runtimeTargets"` + Native any `json:"native"` +} + type Parser struct { logger *log.Logger + once sync.Once } func NewParser() *Parser { return &Parser{ logger: log.WithPrefix("dotnet"), + once: sync.Once{}, } } @@ -29,11 +57,11 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc if err != nil { return nil, nil, xerrors.Errorf("read error: %w", err) } - if err := jfather.Unmarshal(input, &depsFile); err != nil { + if err = jfather.Unmarshal(input, &depsFile); err != nil { return nil, nil, xerrors.Errorf("failed to decode .deps.json file: %w", err) } - var pkgs []ftypes.Package + var pkgs ftypes.Packages for nameVer, lib := range depsFile.Libraries { if !strings.EqualFold(lib.Type, "package") { continue @@ -46,7 +74,20 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc continue } + // Take target libraries for RuntimeTarget + if targetLibs, ok := depsFile.Targets[depsFile.RuntimeTarget.Name]; !ok { + // If the target is not found, take all dependencies + p.once.Do(func() { + p.logger.Debug("Unable to find `Target` for Runtime Target Name. All dependencies from `libraries` section will be included in the report", log.String("Runtime Target Name", depsFile.RuntimeTarget.Name)) + }) + } else if !p.isRuntimeLibrary(targetLibs, nameVer) { + // Skip non-runtime libraries + // cf. https://github.com/aquasecurity/trivy/pull/7039#discussion_r1674566823 + continue + } + pkgs = append(pkgs, ftypes.Package{ + ID: dependency.ID(ftypes.DotNetCore, split[0], split[1]), Name: split[0], Version: split[1], Locations: []ftypes.Location{ @@ -58,17 +99,24 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc }) } + sort.Sort(pkgs) return pkgs, nil, nil } -type dotNetDependencies struct { - Libraries map[string]dotNetLibrary `json:"libraries"` -} - -type dotNetLibrary struct { - Type string `json:"type"` - StartLine int - EndLine int +// isRuntimeLibrary returns true if library contains `runtime`, `runtimeTarget` or `native` sections, or if the library is missing from `targetLibs`. +// See https://github.com/aquasecurity/trivy/discussions/4282#discussioncomment-8830365 for more details. +func (p *Parser) isRuntimeLibrary(targetLibs map[string]TargetLib, library string) bool { + lib, ok := targetLibs[library] + // Selected target doesn't contain library + // Mark these libraries as runtime to avoid mistaken omission + if !ok { + p.once.Do(func() { + p.logger.Debug("Unable to determine that this is runtime library. Library not found in `Target` section.", log.String("Library", library)) + }) + return true + } + // Check that `runtime`, `runtimeTarget` and `native` sections are not empty + return !lo.IsEmpty(lib) } // UnmarshalJSONWithMetadata needed to detect start and end lines of deps diff --git a/pkg/dependency/parser/dotnet/core_deps/parse_test.go b/pkg/dependency/parser/dotnet/core_deps/parse_test.go index a495fe0d61fe..82bf0e0a1d47 100644 --- a/pkg/dependency/parser/dotnet/core_deps/parse_test.go +++ b/pkg/dependency/parser/dotnet/core_deps/parse_test.go @@ -2,7 +2,6 @@ package core_deps import ( "os" - "path" "sort" "testing" @@ -13,29 +12,82 @@ import ( ) func TestParse(t *testing.T) { - vectors := []struct { + tests := []struct { + name string file string // Test input file want []ftypes.Package wantErr string }{ { - file: "testdata/ExampleApp1.deps.json", + name: "happy path", + file: "testdata/happy.deps.json", want: []ftypes.Package{ - {Name: "Newtonsoft.Json", Version: "13.0.1", Locations: []ftypes.Location{{StartLine: 33, EndLine: 39}}}, + { + ID: "Newtonsoft.Json/13.0.1", + Name: "Newtonsoft.Json", + Version: "13.0.1", + Locations: []ftypes.Location{ + { + StartLine: 33, + EndLine: 39, + }, + }, + }, }, }, { - file: "testdata/NoLibraries.deps.json", + name: "happy path with skipped libs", + file: "testdata/without-runtime.deps.json", + want: []ftypes.Package{ + { + ID: "JsonDiffPatch/2.0.61", + Name: "JsonDiffPatch", + Version: "2.0.61", + Locations: []ftypes.Location{ + { + StartLine: 66, + EndLine: 72, + }, + }, + }, + { + ID: "Libuv/1.9.1", + Name: "Libuv", + Version: "1.9.1", + Locations: []ftypes.Location{ + { + StartLine: 73, + EndLine: 79, + }, + }, + }, + { + ID: "System.Collections.Immutable/1.3.0", + Name: "System.Collections.Immutable", + Version: "1.3.0", + Locations: []ftypes.Location{ + { + StartLine: 101, + EndLine: 107, + }, + }, + }, + }, + }, + { + name: "happy path without libs", + file: "testdata/no-libraries.deps.json", want: nil, }, { - file: "testdata/InvalidJson.deps.json", + name: "sad path", + file: "testdata/invalid.deps.json", wantErr: "failed to decode .deps.json file: EOF", }, } - for _, tt := range vectors { - t.Run(path.Base(tt.file), func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { f, err := os.Open(tt.file) require.NoError(t, err) diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/ExampleApp1.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/happy.deps.json similarity index 100% rename from pkg/dependency/parser/dotnet/core_deps/testdata/ExampleApp1.deps.json rename to pkg/dependency/parser/dotnet/core_deps/testdata/happy.deps.json diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/InvalidJson.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/invalid.deps.json similarity index 100% rename from pkg/dependency/parser/dotnet/core_deps/testdata/InvalidJson.deps.json rename to pkg/dependency/parser/dotnet/core_deps/testdata/invalid.deps.json diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/NoLibraries.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/no-libraries.deps.json similarity index 100% rename from pkg/dependency/parser/dotnet/core_deps/testdata/NoLibraries.deps.json rename to pkg/dependency/parser/dotnet/core_deps/testdata/no-libraries.deps.json diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/without-runtime.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/without-runtime.deps.json new file mode 100644 index 000000000000..ba882c61cb5f --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/testdata/without-runtime.deps.json @@ -0,0 +1,116 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "hello2/1.0.0": { + "dependencies": { + "JsonDiffPatch": "2.0.61" + }, + "runtime": { + "hello2.dll": {} + } + }, + "JsonDiffPatch/2.0.61": { + "dependencies": { + "Microsoft.NETCore.App": "1.1.2" + }, + "runtime": { + "lib/netcoreapp1.1/JsonDiffPatch.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + }, + "Libuv/1.9.1": { + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + }, + "runtimeTargets": { + "runtimes/debian-x64/native/libuv.so": { + "rid": "debian-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/fedora-x64/native/libuv.so": { + "rid": "fedora-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "Microsoft.NETCore.App/1.1.2": { + "dependencies": { + "Libuv": "1.9.1", + "System.Collections.Immutable": "1.3.0" + } + }, + "Microsoft.NETCore.Platforms/1.1.0": {}, + "NETStandard.Library/1.6.0": { + "dependencies": { + "System.Net.Http": "4.1.0" + } + }, + "System.Net.Http/4.1.0": {} + } + }, + "libraries": { + "hello2/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "JsonDiffPatch/2.0.61": { + "type": "package", + "serviceable": true, + "sha512": "sha512-nZ4QtcU3jR+CBT69qcJBvCcWi5uKgPRrrvSMm4V8Z76ljJ/MFo1P55qXk/nQY0q0WC4v94m5qH4SDhovFfci+Q==", + "path": "jsondiffpatch/2.0.61", + "hashPath": "jsondiffpatch.2.0.61.nupkg.sha512" + }, + "Libuv/1.9.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-uqX2Frwf9PW8MaY7PRNY6HM5BpW1D8oj1EdqzrmbEFD5nH63Yat3aEjN/tws6Tw6Fk7LwmLBvtUh32tTeTaHiA==", + "path": "libuv/1.9.1", + "hashPath": "libuv.1.9.1.nupkg.sha512" + }, + "Microsoft.NETCore.App/1.1.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fcN0Ob6rjY7Zu0770cA5l9wRJvj7+ltJPPdryUidejkkhao+y2AYrtezBTlP9nCSFXLmYR9BtaknORT17x8reA==", + "path": "microsoft.netcore.app/1.1.2", + "hashPath": "microsoft.netcore.app.1.1.2.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/1.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", + "path": "microsoft.netcore.platforms/1.1.0", + "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" + }, + "NETStandard.Library/1.6.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ypsCvIdCZ4IoYASJHt6tF2fMo7N30NLgV1EbmC+snO490OMl9FvVxmumw14rhReWU3j3g7BYudG6YCrchwHJlA==", + "path": "netstandard.library/1.6.0", + "hashPath": "netstandard.library.1.6.0.nupkg.sha512" + }, + "System.Collections.Immutable/1.3.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zukBRPUuNxwy9m4TGWLxKAnoiMc9+B+8VXeXVyPiBPvOd7yLgAlZ1DlsRWJjMx4VsvhhF2+6q6kO2GRbPja6hA==", + "path": "system.collections.immutable/1.3.0", + "hashPath": "system.collections.immutable.1.3.0.nupkg.sha512" + }, + "System.Net.Http/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ULq9g3SOPVuupt+Y3U+A37coXzdNisB1neFCSKzBwo182u0RDddKJF8I5+HfyXqK6OhJPgeoAwWXrbiUXuRDsg==", + "path": "system.net.http/4.1.0", + "hashPath": "system.net.http.4.1.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go b/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go index c91d4467320e..d6a86c78084e 100644 --- a/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go +++ b/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go @@ -29,6 +29,7 @@ func Test_depsLibraryAnalyzer_Analyze(t *testing.T) { FilePath: "testdata/datacollector.deps.json", Packages: types.Packages{ { + ID: "Newtonsoft.Json/9.0.1", Name: "Newtonsoft.Json", Version: "9.0.1", Locations: []types.Location{