Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dotnet): don't include non-runtime libraries into report for *.deps.json files #7039

3 changes: 3 additions & 0 deletions docs/docs/coverage/language/dotnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions integration/testdata/dotnet.json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@
"Type": "dotnet-core",
"Packages": [
{
"ID": "Newtonsoft.Json/9.0.1",
"Name": "Newtonsoft.Json",
"Identifier": {
"PURL": "pkg:nuget/[email protected]",
"UID": "19955f480b8a6340"
"UID": "e678401f5d07418a"
},
"Version": "9.0.1",
"Layer": {},
Expand All @@ -40,10 +41,11 @@
"Vulnerabilities": [
{
"VulnerabilityID": "GHSA-5crp-9r3c-p9vr",
"PkgID": "Newtonsoft.Json/9.0.1",
"PkgName": "Newtonsoft.Json",
"PkgIdentifier": {
"PURL": "pkg:nuget/[email protected]",
"UID": "19955f480b8a6340"
"UID": "e678401f5d07418a"
},
"InstalledVersion": "9.0.1",
"FixedVersion": "13.0.1",
Expand Down
3 changes: 2 additions & 1 deletion pkg/dependency/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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.
Expand Down
68 changes: 58 additions & 10 deletions pkg/dependency/parser/dotnet/core_deps/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
}
}

Expand All @@ -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
Expand All @@ -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{
Expand All @@ -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
Expand Down
68 changes: 60 additions & 8 deletions pkg/dependency/parser/dotnet/core_deps/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package core_deps

import (
"os"
"path"
"sort"
"testing"

Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add another testcase that references some of the commonly reported false positives and proves that those aren't flagged? For example - a deps file listing a vulnerable version of System.Net.Http etc. Here's a sample deps file:
testRuntimeDeps.deps.json

This was created with

dotnet new console -f net6.0
dotnet add package NETStandard.Library --version 1.6.0
dotnet build

This shows that all the old packages (some vulnerable) making up the NETStandard surface area are all excluded when targeting the latest frameworks which provide inbox support for all those package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your test file is too large.
So I only inserted System.Net.Http and NETStandard.Library libraries into test file. - ca87c12

"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"
}
}
}
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/language/dotnet/deps/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down