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

Add dotnet deps parsing #258

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
76 changes: 56 additions & 20 deletions pkg/dotnet/core_deps/parse.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -19,6 +21,20 @@ func NewParser() types.Parser {
return &Parser{}
}

func packageID(name, version string) string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
func packageID(name, version string) string {
// .NET uses `<name>/<version>` ID format:
// https://github.com/dotnet/cli/blob/f0ee1e44114f161ce9268103d18ce32d2a995d0f/Documentation/specs/runtime-configuration-file.md?plain=1#L87
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

Expand All @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Libraries and Dependencies can be sorted.

Suggested change
return libraries, deps, nil
sort.Sort(types.Libraries(libs))
sort.Sort(types.Dependencies(deps))
return libraries, deps, nil

After these changes you can remove sorting from tests.

}

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
}
Expand Down
107 changes: 93 additions & 14 deletions pkg/dotnet/core_deps/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
noqcks marked this conversation as resolved.
Show resolved Hide resolved
{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",
Expand All @@ -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)
}
})
}
Expand Down
110 changes: 110 additions & 0 deletions pkg/dotnet/core_deps/testdata/MyExample.deps.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}