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

8 changes: 8 additions & 0 deletions docs/docs/coverage/language/dotnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Trivy parses `*.deps.json` files. Trivy currently excludes dev dependencies from
## 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.

### libraries built into .NET Core
[libraries section][net-core-libraries] of `*deps.json` file may contain dependencies that included to `.NET Core` (dependencies with empty `runtime`, `runtimeTarget` and `native` fields in [target][net-core-target] section).
These dependencies are not needed by the runtime, and the `dotnet build` command doesn't create `*.dll` files for them.

Trivy marks these dependencies as the development dependencies and skip them by default. If you need to show them, use the `--include-dev-deps` flag.

## *Packages.props
Trivy parses `*Packages.props` files. Both legacy `Packages.props` and modern `Directory.Packages.props` are supported.

Expand All @@ -49,3 +55,5 @@ Same as [packages.config](#license-detection)
[global-packages]: https://learn.microsoft.com/en-us/nuget/consume-packages/managing-the-global-packages-and-cache-folders
[license]: https://learn.microsoft.com/en-us/nuget/reference/nuspec#license
[license-url]: https://learn.microsoft.com/en-us/nuget/reference/nuspec#licenseurl
[net-core-libraries]: https://github.com/dotnet/sdk/blob/529132850841a6bcfce96799262ce688e3851875/documentation/specs/runtime-configuration-file.md#libraries-section-depsjson
[net-core-target]: https://github.com/dotnet/sdk/blob/529132850841a6bcfce96799262ce688e3851875/documentation/specs/runtime-configuration-file.md#targets-section-depsjson
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]Target `json:"targets"`
}

type dotNetLibrary struct {
Type string `json:"type"`
StartLine int
EndLine int
}

type RuntimeTarget struct {
Name string `json:"name"`
}

type Target 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,14 @@ 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
// Select target for RuntimeTarget
target := depsFile.Targets[depsFile.RuntimeTarget.Name]

var pkgs ftypes.Packages
for nameVer, lib := range depsFile.Libraries {
if !strings.EqualFold(lib.Type, "package") {
continue
Expand All @@ -47,6 +78,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
}

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 @@ -55,20 +87,36 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
EndLine: lib.EndLine,
},
},
// We're still not sure that we need to skip libraries built into .NETCore (or that we detect them correctly).
// So we mark these libraries as Dev to skip the scan by default, but keep the options for displaying these libraries.
Copy link

@ericstj ericstj Jun 27, 2024

Choose a reason for hiding this comment

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

Packages which don't contribute runtime assets to the application cannot contribute to a runtime vulnerability since nothing from the package persists at runtime. Runtime assets can be identified by examining the targets section since that's what the host uses to probe for those.

There are lots of ways packages might be referenced by an app and excluded from runtime.

The package might be referenced with ExcludeAssets="Runtime", the package might have been superseded by the .NET runtime itself and excluded but the build, or the package might have overlapping assets with another package that is preferred. In all cases - if it's bits don't make it to the final app it cannot be a source of a vulnerability in that app.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in general I think the same way.
thank you for detailing this 👍

In all cases - if it's bits don't make it to the final app it cannot be a source of a vulnerability in that app.

There are times when users want to see dependencies that are not used at runtime (we encountered this in nodejs).
That's why we've added options to allow users to see all dependencies (we hide them by default). for example, this may be needed not for scanning vulnerabilities, but for generating a SBOM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The package might be referenced with ExcludeAssets="Runtime"

Do we need to check this case?
Can you write more info about this?

docs don't have info about ExcludeAssets field.

Copy link
Collaborator

Choose a reason for hiding this comment

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

in general I think the same way.

thank you for detailing this 👍

In all cases - if it's bits don't make it to the final app it cannot be a source of a vulnerability in that app.

There are times when users want to see dependencies that are not used at runtime (we encountered this in nodejs).

That's why we've added options to allow users to see all dependencies (we hide them by default). for example, this may be needed not for scanning vulnerabilities, but for generating a SBOM

I would add more contexts. As interest in supply chain security grows, there is a demand to understand vulnerabilities in packages that are used for CI/CD and other purposes, even if they are not included in production applications and are used only for development.

If the library is not used at all, even for development purposes, there is no need to include it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@ericstj Since we're unfamiliar with .NET Core, we need your input. Given the above context, do you think we don't have to save these libraries?

Copy link
Collaborator

@knqyf263 knqyf263 Jul 9, 2024

Choose a reason for hiding this comment

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

@ericstj In other ecosystems, it is common to include test assertion libraries as development libraries. And while these libraries are not used at runtime, their vulnerability is important.

For instance - a build target that inserts code into the binary. That package might appear without any runtime assets, however such a package is not the norm. .

This example you gave is certainly not legitimate, but are libraries used for development purposes in general use such as the one I gave not included in deps.json?

Copy link

@ericstj ericstj Jul 11, 2024

Choose a reason for hiding this comment

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

deps.json isn't the file to see all dependencies that might be used at development time. It's used for loading binaries at runtime.

At development time projects use PackageReference (direct references) in the project, which gets computed to the full package graph in project.assets.json. The old format packages.config had a flattened graph in the single file and was really just a log for the package modifications that were made to the project file separately. These two files are better for identifying development dependencies - and are used by component governance for that purpose. They are present on the build machine / source control. There's also the new SBOM infrastructure which is better suited as a catch all.

This example you gave is certainly not legitimate, but are libraries used for development purposes in general use such as the one I gave not included in deps.json?

They may or may not be included, it depends on the type of package and how its referenced. deps.json is only used for telling the runtime what to load. When a library is present you can be certain it's used - so if that library/package is vulnerable it should be flagged. If the package is listed in the deps file, you can be certain it was at least referenced - but it may have been eliminated by the build and not loadable at runtime - so it should not be flagged. You can't assume that a package being absent means it wasn't used at dev time since the deps.json doesn't list all development time dependencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm.. It looks like that we can remove non-runtime dependencies.
Also we need to write in docs that Trivy only detects runtime deps from *.deps.json

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for your explanation. It might be similar to Go binary scanning. It doesn't have any development modules, while go.mod includes it.

@DmitriyLewen We can just delete non-runtime dependencies and improve it later if we see any feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done 👍
@knqyf263 @ericstj can you review this PR?

Dev: p.isLibraryBuiltIntoNetCore(target, depsFile.RuntimeTarget.Name, nameVer),
})
}

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
// isLibraryBuiltIntoNetCore returns true if library doesn't contain `runtime`, `runtimeTarget` and `native` sections.
// See https://github.com/aquasecurity/trivy/discussions/4282#discussioncomment-8830365 for more details.
func (p *Parser) isLibraryBuiltIntoNetCore(target map[string]Target, runtimeTargetName, library string) bool {
// `Target` for `RuntimeTarget.Name` not found
if target == nil {
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("RuntimeTarget", runtimeTargetName))
})
return false
}
lib, ok := target[library]
// Selected target doesn't contain library
if !ok {
p.once.Do(func() {
p.logger.Debug("Unable to determine that the library is built into .NET Core. Library not found in `Target` section.", log.String("RuntimeTarget", runtimeTargetName), log.String("Library", library))
})
return false
}
// Check that `runtime`, `runtimeTarget` and `native` sections are empty
Copy link

Choose a reason for hiding this comment

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

Looks to me like it checks if they are not empty.

Suggested change
// Check that `runtime`, `runtimeTarget` and `native` sections are empty
// Check that `runtime`, `runtimeTarget` and `native` sections are not empty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in afe12b4

return lo.IsEmpty(lib)
}

// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
Expand Down
88 changes: 80 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,102 @@ 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: 60, EndLine: 66,
},
},
},
{
ID: "Libuv/1.9.1",
Name: "Libuv",
Version: "1.9.1",
Locations: []ftypes.Location{
{
StartLine: 67, EndLine: 73,
},
},
},
{
ID: "Microsoft.NETCore.App/1.1.2",
Name: "Microsoft.NETCore.App",
Version: "1.1.2",
Locations: []ftypes.Location{
{
StartLine: 74, EndLine: 80,
},
},
Dev: true,
},
{
ID: "Microsoft.NETCore.Platforms/1.1.0",
Name: "Microsoft.NETCore.Platforms",
Version: "1.1.0",
Locations: []ftypes.Location{
{
StartLine: 81, EndLine: 87,
},
},
Dev: true,
},
{
ID: "System.Collections.Immutable/1.3.0",
Name: "System.Collections.Immutable",
Version: "1.3.0",
Locations: []ftypes.Location{
{
StartLine: 88, EndLine: 94,
},
},
// Is false because `targets[.NETCoreApp,Version=v6.0]` doesn't contain this library
Dev: false,
},
},
},
{
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,96 @@
{
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": {}
}
},
"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"
},
"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"
}
}
}
Loading