diff --git a/pkg/dotnet/core_deps/parse.go b/pkg/dotnet/core_deps/parse.go index 94278094..f8d819a3 100644 --- a/pkg/dotnet/core_deps/parse.go +++ b/pkg/dotnet/core_deps/parse.go @@ -1,10 +1,12 @@ package core_deps import ( - "github.com/liamg/jfather" + "fmt" "io" "strings" + "github.com/liamg/jfather" + "golang.org/x/xerrors" dio "github.com/aquasecurity/go-dep-parser/pkg/io" @@ -19,6 +21,20 @@ func NewParser() types.Parser { return &Parser{} } +func packageID(name, version string) string { + return fmt.Sprintf("%s/%s", name, version) +} + +func splitNameVer(nameVer string) (string, string) { + split := strings.Split(nameVer, "/") + if len(split) != 2 { + // Invalid name + log.Logger.Warnf("Cannot parse .NET library version from: %s", nameVer) + return "", "" + } + return split[0], split[1] +} + func (p *Parser) Parse(r dio.ReadSeekerAt) ([]types.Library, []types.Dependency, error) { var depsFile dotNetDependencies @@ -31,40 +47,60 @@ func (p *Parser) Parse(r dio.ReadSeekerAt) ([]types.Library, []types.Dependency, } var libraries []types.Library - for nameVer, lib := range depsFile.Libraries { - if !strings.EqualFold(lib.Type, "package") { + var deps []types.Dependency + targets := depsFile.Targets[depsFile.RuntimeTarget.Name] + for pkgNameVersion, target := range targets { + name, version := splitNameVer(pkgNameVersion) + if name == "" || version == "" { continue } - split := strings.Split(nameVer, "/") - if len(split) != 2 { - // Invalid name - log.Logger.Warnf("Cannot parse .NET library version from: %s", nameVer) - continue + lib := types.Library{ + ID: packageID(name, version), + Name: name, + Version: version, + Locations: []types.Location{{StartLine: target.StartLine, EndLine: target.EndLine}}, + } + + var childDeps []string + for depName, depVersion := range target.Dependencies { + depID := packageID(depName, depVersion) + if _, ok := targets[depID]; ok { + childDeps = append(childDeps, depID) + } } - libraries = append(libraries, types.Library{ - Name: split[0], - Version: split[1], - Locations: []types.Location{{StartLine: lib.StartLine, EndLine: lib.EndLine}}, - }) + if len(childDeps) > 0 { + deps = append(deps, types.Dependency{ + ID: lib.ID, + DependsOn: childDeps, + }) + } + + libraries = append(libraries, lib) } - return libraries, nil, nil + return libraries, deps, nil } type dotNetDependencies struct { - Libraries map[string]dotNetLibrary `json:"libraries"` + RuntimeTarget dotNetRuntimeTarget `json:"runtimeTarget"` + Targets map[string]map[string]dotNetTarget `json:"targets"` +} + +type dotNetRuntimeTarget struct { + Name string `json:"name"` } -type dotNetLibrary struct { - Type string `json:"type"` - StartLine int - EndLine int +type dotNetTarget struct { + Dependencies map[string]string `json:"dependencies"` + Runtime map[string]struct{} `json:"runtime"` + StartLine int + EndLine int } // UnmarshalJSONWithMetadata needed to detect start and end lines of deps -func (t *dotNetLibrary) UnmarshalJSONWithMetadata(node jfather.Node) error { +func (t *dotNetTarget) UnmarshalJSONWithMetadata(node jfather.Node) error { if err := node.Decode(&t); err != nil { return err } diff --git a/pkg/dotnet/core_deps/parse_test.go b/pkg/dotnet/core_deps/parse_test.go index c1b001b6..e56fb55e 100644 --- a/pkg/dotnet/core_deps/parse_test.go +++ b/pkg/dotnet/core_deps/parse_test.go @@ -15,19 +15,81 @@ import ( func TestParse(t *testing.T) { vectors := []struct { - file string // Test input file - want []types.Library - wantErr string + file string // Test input file + wantLibs []types.Library + wantDeps []types.Dependency + wantErr string }{ + { + file: "testdata/MyExample.deps.json", + wantLibs: []types.Library{ + {ID: "MyWebApp/1.0.0", Name: "MyWebApp", Version: "1.0.0", Locations: []types.Location{{StartLine: 9, EndLine: 16}}}, + {ID: "Microsoft.Extensions.Configuration.Abstractions/2.2.0", Name: "Microsoft.Extensions.Configuration.Abstractions", Version: "2.2.0", Locations: []types.Location{{StartLine: 17, EndLine: 21}}}, + {ID: "Microsoft.Extensions.DependencyInjection.Abstractions/2.2.0", Name: "Microsoft.Extensions.DependencyInjection.Abstractions", Version: "2.2.0", Locations: []types.Location{{StartLine: 22, EndLine: 22}}}, + {ID: "Microsoft.Extensions.FileProviders.Abstractions/2.2.0", Name: "Microsoft.Extensions.FileProviders.Abstractions", Version: "2.2.0", Locations: []types.Location{{StartLine: 23, EndLine: 27}}}, + {ID: "Microsoft.Extensions.Hosting.Abstractions/2.2.0", Name: "Microsoft.Extensions.Hosting.Abstractions", Version: "2.2.0", Locations: []types.Location{{StartLine: 28, EndLine: 35}}}, + {ID: "Microsoft.Extensions.Logging.Abstractions/2.2.0", Name: "Microsoft.Extensions.Logging.Abstractions", Version: "2.2.0", Locations: []types.Location{{StartLine: 36, EndLine: 36}}}, + {ID: "Microsoft.Extensions.Primitives/2.2.0", Name: "Microsoft.Extensions.Primitives", Version: "2.2.0", Locations: []types.Location{{StartLine: 37, EndLine: 42}}}, + {ID: "System.Memory/4.5.1", Name: "System.Memory", Version: "4.5.1", Locations: []types.Location{{StartLine: 43, EndLine: 43}}}, + {ID: "System.Runtime.CompilerServices.Unsafe/4.5.1", Name: "System.Runtime.CompilerServices.Unsafe", Version: "4.5.1", Locations: []types.Location{{StartLine: 44, EndLine: 44}}}, + }, + wantDeps: []types.Dependency{ + { + ID: "MyWebApp/1.0.0", + DependsOn: []string{ + "Microsoft.Extensions.Hosting.Abstractions/2.2.0", + }, + }, + { + ID: "Microsoft.Extensions.Hosting.Abstractions/2.2.0", + DependsOn: []string{ + "Microsoft.Extensions.Configuration.Abstractions/2.2.0", + "Microsoft.Extensions.DependencyInjection.Abstractions/2.2.0", + "Microsoft.Extensions.FileProviders.Abstractions/2.2.0", + "Microsoft.Extensions.Logging.Abstractions/2.2.0", + }, + }, + { + ID: "Microsoft.Extensions.Configuration.Abstractions/2.2.0", + DependsOn: []string{ + "Microsoft.Extensions.Primitives/2.2.0", + }, + }, + { + ID: "Microsoft.Extensions.FileProviders.Abstractions/2.2.0", + DependsOn: []string{ + "Microsoft.Extensions.Primitives/2.2.0", + }, + }, + { + ID: "Microsoft.Extensions.Primitives/2.2.0", + DependsOn: []string{ + "System.Memory/4.5.1", + "System.Runtime.CompilerServices.Unsafe/4.5.1", + }, + }, + }, + }, + { file: "testdata/ExampleApp1.deps.json", - want: []types.Library{ - {Name: "Newtonsoft.Json", Version: "13.0.1", Locations: []types.Location{{StartLine: 33, EndLine: 39}}}, + wantLibs: []types.Library{ + {ID: "Newtonsoft.Json/13.0.1", Name: "Newtonsoft.Json", Version: "13.0.1", Locations: []types.Location{{StartLine: 17, EndLine: 24}}}, + {ID: "ExampleApp1/1.0.0", Name: "ExampleApp1", Version: "1.0.0", Locations: []types.Location{{StartLine: 9, EndLine: 16}}}, + }, + wantDeps: []types.Dependency{ + {ID: "ExampleApp1/1.0.0", DependsOn: []string{"Newtonsoft.Json/13.0.1"}}, }, }, { file: "testdata/NoLibraries.deps.json", - want: nil, + wantLibs: []types.Library{ + {ID: "ExampleApp1/1.0.0", Name: "ExampleApp1", Version: "1.0.0", Locations: types.Locations{types.Location{StartLine: 9, EndLine: 16}}}, + {ID: "Newtonsoft.Json/13.0.1", Name: "Newtonsoft.Json", Version: "13.0.1", Locations: types.Locations{types.Location{StartLine: 17, EndLine: 24}}}, + }, + wantDeps: []types.Dependency{ + {ID: "ExampleApp1/1.0.0", DependsOn: []string{"Newtonsoft.Json/13.0.1"}}, + }, }, { file: "testdata/InvalidJson.deps.json", @@ -40,30 +102,47 @@ func TestParse(t *testing.T) { f, err := os.Open(tt.file) require.NoError(t, err) - got, _, err := NewParser().Parse(f) + gotLibs, gotDeps, err := NewParser().Parse(f) if tt.wantErr != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), tt.wantErr) } else { require.NoError(t, err) - sort.Slice(got, func(i, j int) bool { - ret := strings.Compare(got[i].Name, got[j].Name) + sort.Slice(gotLibs, func(i, j int) bool { + ret := strings.Compare(gotLibs[i].Name, gotLibs[j].Name) if ret == 0 { - return got[i].Version < got[j].Version + return gotLibs[i].Version < gotLibs[j].Version } return ret < 0 }) - sort.Slice(tt.want, func(i, j int) bool { - ret := strings.Compare(tt.want[i].Name, tt.want[j].Name) + sort.Slice(gotDeps, func(i, j int) bool { + return gotDeps[i].ID < gotDeps[j].ID + }) + + for _, dep := range gotDeps { + sort.Strings(dep.DependsOn) + } + + sort.Slice(tt.wantLibs, func(i, j int) bool { + ret := strings.Compare(tt.wantLibs[i].Name, tt.wantLibs[j].Name) if ret == 0 { - return tt.want[i].Version < tt.want[j].Version + return tt.wantLibs[i].Version < tt.wantLibs[j].Version } return ret < 0 }) - assert.Equal(t, tt.want, got) + sort.Slice(tt.wantDeps, func(i, j int) bool { + return tt.wantDeps[i].ID < tt.wantDeps[j].ID + }) + + for _, dep := range tt.wantDeps { + sort.Strings(dep.DependsOn) + } + + assert.Equal(t, tt.wantLibs, gotLibs) + assert.Equal(t, tt.wantDeps, gotDeps) } }) } diff --git a/pkg/dotnet/core_deps/testdata/MyExample.deps.json b/pkg/dotnet/core_deps/testdata/MyExample.deps.json new file mode 100644 index 00000000..32d64856 --- /dev/null +++ b/pkg/dotnet/core_deps/testdata/MyExample.deps.json @@ -0,0 +1,110 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v7.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v7.0": { + "MyWebApp/1.0.0": { + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "2.2.0" + }, + "runtime": { + "MyWebApp.dll": {} + } + }, + "Microsoft.Extensions.Configuration.Abstractions/2.2.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "2.2.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/2.2.0": {}, + "Microsoft.Extensions.FileProviders.Abstractions/2.2.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "2.2.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions/2.2.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "2.2.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.2.0", + "Microsoft.Extensions.Logging.Abstractions": "2.2.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions/2.2.0": {}, + "Microsoft.Extensions.Primitives/2.2.0": { + "dependencies": { + "System.Memory": "4.5.1", + "System.Runtime.CompilerServices.Unsafe": "4.5.1" + } + }, + "System.Memory/4.5.1": {}, + "System.Runtime.CompilerServices.Unsafe/4.5.1": {} + } + }, + "libraries": { + "MyWebApp/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Extensions.Configuration.Abstractions/2.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==", + "path": "microsoft.extensions.configuration.abstractions/2.2.0", + "hashPath": "microsoft.extensions.configuration.abstractions.2.2.0.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/2.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw==", + "path": "microsoft.extensions.dependencyinjection.abstractions/2.2.0", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.2.2.0.nupkg.sha512" + }, + "Microsoft.Extensions.FileProviders.Abstractions/2.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-EcnaSsPTqx2MGnHrmWOD0ugbuuqVT8iICqSqPzi45V5/MA1LjUNb0kwgcxBGqizV1R+WeBK7/Gw25Jzkyk9bIw==", + "path": "microsoft.extensions.fileproviders.abstractions/2.2.0", + "hashPath": "microsoft.extensions.fileproviders.abstractions.2.2.0.nupkg.sha512" + }, + "Microsoft.Extensions.Hosting.Abstractions/2.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-+k4AEn68HOJat5gj1TWa6X28WlirNQO9sPIIeQbia+91n03esEtMSSoekSTpMjUzjqtJWQN3McVx0GvSPFHF/Q==", + "path": "microsoft.extensions.hosting.abstractions/2.2.0", + "hashPath": "microsoft.extensions.hosting.abstractions.2.2.0.nupkg.sha512" + }, + "Microsoft.Extensions.Logging.Abstractions/2.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==", + "path": "microsoft.extensions.logging.abstractions/2.2.0", + "hashPath": "microsoft.extensions.logging.abstractions.2.2.0.nupkg.sha512" + }, + "Microsoft.Extensions.Primitives/2.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==", + "path": "microsoft.extensions.primitives/2.2.0", + "hashPath": "microsoft.extensions.primitives.2.2.0.nupkg.sha512" + }, + "System.Memory/4.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-sDJYJpGtTgx+23Ayu5euxG5mAXWdkDb4+b0rD0Cab0M1oQS9H0HXGPriKcqpXuiJDTV7fTp/d+fMDJmnr6sNvA==", + "path": "system.memory/4.5.1", + "hashPath": "system.memory.4.5.1.nupkg.sha512" + }, + "System.Runtime.CompilerServices.Unsafe/4.5.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Zh8t8oqolRaFa9vmOZfdQm/qKejdqz0J9kr7o2Fu0vPeoH3BL1EOXipKWwkWtLT1JPzjByrF19fGuFlNbmPpiw==", + "path": "system.runtime.compilerservices.unsafe/4.5.1", + "hashPath": "system.runtime.compilerservices.unsafe.4.5.1.nupkg.sha512" + } + } +}