Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions pkg/dependency/parser/dotnet/core_deps/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/set"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
Expand Down Expand Up @@ -101,8 +102,8 @@ func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package,
// collectPackages builds the package map from the `libraries` section, returning the
// packages keyed by ID and the ID of the root project (empty if none was found).
func (p *Parser) collectPackages(depsFile dotNetDependencies, targetLibs map[string]TargetLib, targetLibsFound bool) (map[string]ftypes.Package, string) {
var projectNameVer string
pkgs := make(map[string]ftypes.Package, len(depsFile.Libraries))
var projects []string

for nameVer, lib := range depsFile.Libraries {
name, version, ok := strings.Cut(nameVer, "/")
Expand Down Expand Up @@ -137,32 +138,54 @@ func (p *Parser) collectPackages(depsFile dotNetDependencies, targetLibs map[str
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", id))
continue
}
projectNameVer = id
pkg.Relationship = ftypes.RelationshipRoot
projects = append(projects, id)
pkg.Relationship = ftypes.RelationshipWorkspace
Comment thread
jetersen marked this conversation as resolved.
Outdated
}

pkgs[pkg.ID] = pkg
}

projectNameVer := rootProject(projects, targetLibs)
if projectNameVer != "" {
pkg := pkgs[projectNameVer]
pkg.Relationship = ftypes.RelationshipRoot
pkgs[projectNameVer] = pkg
}

return pkgs, projectNameVer
}

func rootProject(projects []string, targetLibs map[string]TargetLib) string {
referenced := set.New[string]()
for _, lib := range targetLibs {
for name, version := range lib.Dependencies {
referenced.Append(packageID(name, version))
}
}

var roots []string
for _, project := range projects {
if !referenced.Contains(project) {
roots = append(roots, project)
}
}
if len(roots) == 1 {
return roots[0]
}
return ""
}

// buildDependencyGraph fills the Relationship field of each package and builds the
// dependency graph from the `targets` section.
func (p *Parser) buildDependencyGraph(pkgs map[string]ftypes.Package, targetLibs map[string]TargetLib, directDeps []string) ftypes.Dependencies {
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,
// If Root package wasn't found or doesn'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 {
// Root and workspace package relationships are already set.
if len(directDeps) > 0 && pkg.Relationship == ftypes.RelationshipUnknown {
pkg.Relationship = lo.Ternary(slices.Contains(directDeps, pkgID), ftypes.RelationshipDirect, ftypes.RelationshipIndirect)
pkgs[pkgID] = pkg
}
Expand Down
130 changes: 130 additions & 0 deletions pkg/dependency/parser/dotnet/core_deps/parse_test.go
Comment thread
jetersen marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,133 @@ func TestParse(t *testing.T) {
})
}
}

func TestParseMultiProject(t *testing.T) {
f, err := os.Open("testdata/multi-project/Web.deps.json")
require.NoError(t, err)

got, gotDeps, err := NewParser().Parse(t.Context(), f)
require.NoError(t, err)

assert.Equal(t, []ftypes.Package{
{
ID: "Web/1.0.0",
Name: "Web",
Version: "1.0.0",
Relationship: ftypes.RelationshipRoot,
Locations: []ftypes.Location{
{StartLine: 48, EndLine: 52},
},
},
{
ID: "Api/1.0.0",
Name: "Api",
Version: "1.0.0",
Relationship: ftypes.RelationshipWorkspace,
Locations: []ftypes.Location{
{StartLine: 60, EndLine: 64},
},
},
{
ID: "Data/1.0.0",
Name: "Data",
Version: "1.0.0",
Relationship: ftypes.RelationshipWorkspace,
Locations: []ftypes.Location{
{StartLine: 65, EndLine: 69},
},
},
{
ID: "Newtonsoft.Json/13.0.3",
Name: "Newtonsoft.Json",
Version: "13.0.3",
Relationship: ftypes.RelationshipDirect,
Locations: []ftypes.Location{
{StartLine: 53, EndLine: 59},
},
},
}, got)
assert.Equal(t, []ftypes.Dependency{
{
ID: "Api/1.0.0",
DependsOn: []string{"Data/1.0.0"},
},
{
ID: "Web/1.0.0",
DependsOn: []string{"Api/1.0.0", "Newtonsoft.Json/13.0.3"},
},
}, gotDeps)
}

func TestRootProject(t *testing.T) {
tests := []struct {
name string
projects []string
targetLibs map[string]TargetLib
want string
}{
{
name: "unique root",
projects: []string{"Web/1.0.0", "Api/1.0.0", "Data/1.0.0"},
targetLibs: map[string]TargetLib{
"Web/1.0.0": {Dependencies: map[string]string{"Api": "1.0.0"}},
"Api/1.0.0": {Dependencies: map[string]string{"Data": "1.0.0"}},
"Data/1.0.0": {},
},
want: "Web/1.0.0",
},
{
name: "multiple unreferenced projects",
projects: []string{"Web/1.0.0", "Worker/1.0.0"},
targetLibs: map[string]TargetLib{
"Web/1.0.0": {},
"Worker/1.0.0": {},
},
},
{
name: "cyclic projects",
projects: []string{"Web/1.0.0", "Api/1.0.0"},
targetLibs: map[string]TargetLib{
"Web/1.0.0": {Dependencies: map[string]string{"Api": "1.0.0"}},
"Api/1.0.0": {Dependencies: map[string]string{"Web": "1.0.0"}},
},
},
{
name: "no projects",
targetLibs: map[string]TargetLib{"Newtonsoft.Json/13.0.3": {}},
},
{
name: "single project without target references",
projects: []string{"Web/1.0.0"},
want: "Web/1.0.0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, rootProject(tt.projects, tt.targetLibs))
})
}
}

func TestCollectPackagesWithAmbiguousRoot(t *testing.T) {
depsFile := dotNetDependencies{
Libraries: map[string]dotNetLibrary{
"Web/1.0.0": {Type: "project"},
"Worker/1.0.0": {Type: "project"},
"Newtonsoft.Json/13.0.3": {Type: "package"},
},
}
targetLibs := map[string]TargetLib{
"Web/1.0.0": {Runtime: map[string]any{"Web.dll": struct{}{}}},
"Worker/1.0.0": {Runtime: map[string]any{"Worker.dll": struct{}{}}},
"Newtonsoft.Json/13.0.3": {Runtime: map[string]any{"Newtonsoft.Json.dll": struct{}{}}},
}

pkgs, root := NewParser().collectPackages(depsFile, targetLibs, true)

assert.Empty(t, root)
assert.Equal(t, ftypes.RelationshipWorkspace, pkgs["Web/1.0.0"].Relationship)
assert.Equal(t, ftypes.RelationshipWorkspace, pkgs["Worker/1.0.0"].Relationship)
assert.Equal(t, ftypes.RelationshipUnknown, pkgs["Newtonsoft.Json/13.0.3"].Relationship)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v10.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v10.0": {
"Web/1.0.0": {
"dependencies": {
"Api": "1.0.0",
"Newtonsoft.Json": "13.0.3"
},
"runtime": {
"Web.dll": {}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"Api/1.0.0": {
"dependencies": {
"Data": "1.0.0"
},
"runtime": {
"Api.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"Data/1.0.0": {
"runtime": {
"Data.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.0.0"
}
}
}
}
},
"libraries": {
"Web/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"Api/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Data/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
Loading