mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-13 00:00:19 -08:00
feat(dotnet): add dependency graph support for .deps.json files (#9726)
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
@@ -13,7 +13,7 @@ The following table provides an outline of the features Trivy offers.
|
||||
|
||||
| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
|
||||
|:---------------:|--------------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
|
||||
| .Net Core | *.deps.json | ✓ | Excluded | - | ✓ |
|
||||
| .Net Core | *.deps.json | ✓ | Excluded | ✓ | ✓ |
|
||||
| NuGet | packages.config | ✓ | Excluded | - | - |
|
||||
| NuGet | *Packages.props | - | Excluded | - | - |
|
||||
| NuGet | packages.lock.json | ✓ | Included | ✓ | ✓ |
|
||||
|
||||
16
integration/testdata/dotnet.json.golden
vendored
16
integration/testdata/dotnet.json.golden
vendored
@@ -10,6 +10,22 @@
|
||||
"Class": "lang-pkgs",
|
||||
"Type": "dotnet-core",
|
||||
"Packages": [
|
||||
{
|
||||
"ID": "Microsoft.VisualStudio.TestPlatform.Common/17.2.0-release-20220408-11",
|
||||
"Name": "Microsoft.VisualStudio.TestPlatform.Common",
|
||||
"Identifier": {
|
||||
"PURL": "pkg:nuget/Microsoft.VisualStudio.TestPlatform.Common@17.2.0-release-20220408-11",
|
||||
"UID": "2d904514f21a2664"
|
||||
},
|
||||
"Version": "17.2.0-release-20220408-11",
|
||||
"Relationship": "root",
|
||||
"Locations": [
|
||||
{
|
||||
"StartLine": 15,
|
||||
"EndLine": 19
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "Newtonsoft.Json/9.0.1",
|
||||
"Name": "Newtonsoft.Json",
|
||||
|
||||
@@ -2,6 +2,7 @@ package core_deps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -32,9 +33,10 @@ type RuntimeTarget struct {
|
||||
}
|
||||
|
||||
type TargetLib struct {
|
||||
Runtime any `json:"runtime"`
|
||||
RuntimeTargets any `json:"runtimeTargets"`
|
||||
Native any `json:"native"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
Runtime any `json:"runtime"`
|
||||
RuntimeTargets any `json:"runtimeTargets"`
|
||||
Native any `json:"native"`
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
@@ -55,41 +57,109 @@ func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package,
|
||||
return nil, nil, xerrors.Errorf("failed to decode .deps.json file: %w", err)
|
||||
}
|
||||
|
||||
var pkgs ftypes.Packages
|
||||
for nameVer, lib := range depsFile.Libraries {
|
||||
if !strings.EqualFold(lib.Type, "package") {
|
||||
continue
|
||||
}
|
||||
// Get target libraries for RuntimeTarget
|
||||
targetLibs, targetLibsFound := depsFile.Targets[depsFile.RuntimeTarget.Name]
|
||||
if !targetLibsFound {
|
||||
// If the target is not found, take all dependencies
|
||||
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))
|
||||
}
|
||||
|
||||
split := strings.Split(nameVer, "/")
|
||||
if len(split) != 2 {
|
||||
// First pass: collect all packages
|
||||
var projectNameVer string
|
||||
pkgs := make(map[string]ftypes.Package, len(depsFile.Libraries))
|
||||
|
||||
for nameVer, lib := range depsFile.Libraries {
|
||||
name, version, ok := strings.Cut(nameVer, "/")
|
||||
if !ok {
|
||||
// Invalid name
|
||||
p.logger.Warn("Cannot parse .NET library version", log.String("library", nameVer))
|
||||
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 unsupported library types
|
||||
if !strings.EqualFold(lib.Type, "package") && !strings.EqualFold(lib.Type, "project") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip non-runtime libraries if target libraries are available
|
||||
if targetLibsFound && !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],
|
||||
pkg := ftypes.Package{
|
||||
ID: packageID(name, version),
|
||||
Name: name,
|
||||
Version: version,
|
||||
Locations: []ftypes.Location{ftypes.Location(lib.Location)},
|
||||
})
|
||||
}
|
||||
|
||||
// Identify root package
|
||||
if strings.EqualFold(lib.Type, "project") {
|
||||
if projectNameVer != "" {
|
||||
p.logger.Warn("Multiple root projects found in .deps.json", log.String("existing_root", projectNameVer), log.String("new_root", nameVer))
|
||||
continue
|
||||
}
|
||||
projectNameVer = nameVer
|
||||
pkg.Relationship = ftypes.RelationshipRoot
|
||||
}
|
||||
|
||||
pkgs[pkg.ID] = pkg
|
||||
}
|
||||
|
||||
sort.Sort(pkgs)
|
||||
return pkgs, nil, nil
|
||||
if len(pkgs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// If target libraries are not found, return all collected packages without dependencies
|
||||
if !targetLibsFound {
|
||||
pkgSlice := lo.Values(pkgs)
|
||||
sort.Sort(ftypes.Packages(pkgSlice))
|
||||
return pkgSlice, nil, nil
|
||||
}
|
||||
|
||||
directDeps := lo.MapToSlice(targetLibs[projectNameVer].Dependencies, packageID)
|
||||
|
||||
// Second pass: build dependency graph + fill Relationships from targets section
|
||||
var deps ftypes.Dependencies
|
||||
for pkgID, pkg := range pkgs {
|
||||
// Fill relationship field for package
|
||||
// If Root package didn't find or don't have dependencies, skip setting Relationship,
|
||||
// because most likely file is broken.
|
||||
// Root package Relationship is already set
|
||||
if len(directDeps) > 0 && pkg.Relationship != ftypes.RelationshipRoot {
|
||||
pkg.Relationship = lo.Ternary(slices.Contains(directDeps, pkgID), ftypes.RelationshipDirect, ftypes.RelationshipIndirect)
|
||||
pkgs[pkgID] = pkg
|
||||
}
|
||||
|
||||
// Build dependency graph
|
||||
dependencies, ok := targetLibs[pkgID]
|
||||
// Package doesn't have dependencies
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var dependsOn []string
|
||||
for depName, depVersion := range dependencies.Dependencies {
|
||||
depID := packageID(depName, depVersion)
|
||||
// Only create dependencies for packages that exist in package lists
|
||||
if _, exists := pkgs[depID]; exists {
|
||||
dependsOn = append(dependsOn, depID)
|
||||
}
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
deps = append(deps, ftypes.Dependency{
|
||||
ID: pkgID,
|
||||
DependsOn: dependsOn,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pkgSlice := lo.Values(pkgs)
|
||||
sort.Sort(ftypes.Packages(pkgSlice))
|
||||
sort.Sort(deps)
|
||||
return pkgSlice, deps, nil
|
||||
}
|
||||
|
||||
// isRuntimeLibrary returns true if library contains `runtime`, `runtimeTarget` or `native` sections, or if the library is missing from `targetLibs`.
|
||||
@@ -105,5 +175,9 @@ func (p *Parser) isRuntimeLibrary(targetLibs map[string]TargetLib, library strin
|
||||
return true
|
||||
}
|
||||
// Check that `runtime`, `runtimeTarget` and `native` sections are not empty
|
||||
return !lo.IsEmpty(lib)
|
||||
return !lo.IsEmpty(lib.Runtime) || !lo.IsEmpty(lib.RuntimeTargets) || !lo.IsEmpty(lib.Native)
|
||||
}
|
||||
|
||||
func packageID(name, version string) string {
|
||||
return dependency.ID(ftypes.DotNetCore, name, version)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package core_deps
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -13,15 +12,130 @@ import (
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
file string // Test input file
|
||||
want []ftypes.Package
|
||||
wantErr string
|
||||
name string
|
||||
file string // Test input file
|
||||
want []ftypes.Package
|
||||
wantDeps []ftypes.Dependency
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
file: "testdata/happy.deps.json",
|
||||
want: []ftypes.Package{
|
||||
{
|
||||
ID: "ExampleApp1/1.0.0",
|
||||
Name: "ExampleApp1",
|
||||
Version: "1.0.0",
|
||||
Relationship: ftypes.RelationshipRoot,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 28,
|
||||
EndLine: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "Newtonsoft.Json/13.0.1",
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.1",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 33,
|
||||
EndLine: 39,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantDeps: []ftypes.Dependency{
|
||||
{
|
||||
ID: "ExampleApp1/1.0.0",
|
||||
DependsOn: []string{"Newtonsoft.Json/13.0.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with skipped libs",
|
||||
file: "testdata/without-runtime.deps.json",
|
||||
want: []ftypes.Package{
|
||||
{
|
||||
ID: "hello2/1.0.0",
|
||||
Name: "hello2",
|
||||
Version: "1.0.0",
|
||||
Relationship: ftypes.RelationshipRoot,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 61,
|
||||
EndLine: 65,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "JsonDiffPatch/2.0.61",
|
||||
Name: "JsonDiffPatch",
|
||||
Version: "2.0.61",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 66,
|
||||
EndLine: 72,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "Libuv/1.9.1",
|
||||
Name: "Libuv",
|
||||
Version: "1.9.1",
|
||||
Relationship: ftypes.RelationshipIndirect,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 73,
|
||||
EndLine: 79,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "System.Collections.Immutable/1.3.0",
|
||||
Name: "System.Collections.Immutable",
|
||||
Version: "1.3.0",
|
||||
Relationship: ftypes.RelationshipIndirect,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 101,
|
||||
EndLine: 107,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantDeps: []ftypes.Dependency{
|
||||
{
|
||||
ID: "hello2/1.0.0",
|
||||
DependsOn: []string{"JsonDiffPatch/2.0.61"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path without libs",
|
||||
file: "testdata/no-libraries.deps.json",
|
||||
want: nil,
|
||||
wantDeps: nil,
|
||||
},
|
||||
{
|
||||
name: "target libs not found",
|
||||
file: "testdata/missing-target.deps.json",
|
||||
want: []ftypes.Package{
|
||||
{
|
||||
ID: "ExampleApp1/1.0.0",
|
||||
Name: "ExampleApp1",
|
||||
Version: "1.0.0",
|
||||
Relationship: ftypes.RelationshipRoot,
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 28,
|
||||
EndLine: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "Newtonsoft.Json/13.0.1",
|
||||
Name: "Newtonsoft.Json",
|
||||
@@ -34,50 +148,7 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
wantDeps: nil,
|
||||
},
|
||||
{
|
||||
name: "sad path",
|
||||
@@ -91,17 +162,15 @@ func TestParse(t *testing.T) {
|
||||
f, err := os.Open(tt.file)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, _, err := NewParser().Parse(t.Context(), f)
|
||||
got, gotDeps, err := NewParser().Parse(t.Context(), f)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Sort(ftypes.Packages(got))
|
||||
sort.Sort(ftypes.Packages(tt.want))
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
assert.Equal(t, tt.wantDeps, gotDeps)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
41
pkg/dependency/parser/dotnet/core_deps/testdata/missing-target.deps.json
vendored
Normal file
41
pkg/dependency/parser/dotnet/core_deps/testdata/missing-target.deps.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v5.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v6.0": {
|
||||
"ExampleApp1/1.0.0": {
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"ExampleApp1.dll": {}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.1": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.1.25517"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"ExampleApp1/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Newtonsoft.Json/13.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==",
|
||||
"path": "newtonsoft.json/13.0.1",
|
||||
"hashPath": "newtonsoft.json.13.0.1.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,18 @@ func Test_depsLibraryAnalyzer_Analyze(t *testing.T) {
|
||||
Type: types.DotNetCore,
|
||||
FilePath: "testdata/datacollector.deps.json",
|
||||
Packages: types.Packages{
|
||||
{
|
||||
ID: "Microsoft.VisualStudio.TestPlatform.Common/17.2.0-release-20220408-11",
|
||||
Name: "Microsoft.VisualStudio.TestPlatform.Common",
|
||||
Version: "17.2.0-release-20220408-11",
|
||||
Relationship: types.RelationshipRoot,
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 15,
|
||||
EndLine: 19,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "Newtonsoft.Json/9.0.1",
|
||||
Name: "Newtonsoft.Json",
|
||||
|
||||
Reference in New Issue
Block a user