Skip to content

Commit

Permalink
feat: support parsing NuGet packages.lock.json lockfiles (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Jan 12, 2023
1 parent 3035542 commit f3048cf
Show file tree
Hide file tree
Showing 17 changed files with 374 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The detector supports parsing the following lockfiles:
| ----------------------------- | ----------- | ---------- |
| `buildscript-gradle.lockfile` | `Maven` | `gradle` |
| `Cargo.lock` | `crates.io` | `cargo` |
| `packages.lock.json` | `NuGet` | `dotnet` |
| `package-lock.json` | `npm` | `npm` |
| `yarn.lock` | `npm` | `yarn` |
| `pnpm-lock.yaml` | `npm` | `pnpm` |
Expand Down
1 change: 1 addition & 0 deletions internal/reporter/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func TestReporter_PrintKnownEcosystems(t *testing.T) {
expected := strings.Join([]string{
"The detector supports parsing for the following ecosystems:",
" npm",
" NuGet",
" crates.io",
" RubyGems",
" Packagist",
Expand Down
1 change: 1 addition & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func TestRun(t *testing.T) {
gradle.lockfile
mix.lock
package-lock.json
packages.lock.json
pnpm-lock.yaml
poetry.lock
pom.xml
Expand Down
1 change: 1 addition & 0 deletions pkg/lockfile/ecosystems.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lockfile
func KnownEcosystems() []Ecosystem {
return []Ecosystem{
NpmEcosystem,
NuGetEcosystem,
CargoEcosystem,
BundlerEcosystem,
ComposerEcosystem,
Expand Down
4 changes: 4 additions & 0 deletions pkg/lockfile/fixtures/nuget/empty.v0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 0,
"dependencies": {}
}
4 changes: 4 additions & 0 deletions pkg/lockfile/fixtures/nuget/empty.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 1,
"dependencies": {}
}
1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/nuget/not-json.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is not json!
13 changes: 13 additions & 0 deletions pkg/lockfile/fixtures/nuget/one-framework-one-package.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"dependencies": {
"net6.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
}
}
}
}
22 changes: 22 additions & 0 deletions pkg/lockfile/fixtures/nuget/one-framework-two-packages.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": 1,
"dependencies": {
"net6.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
},
"Test.System": {
"type": "Direct",
"requested": "[0.13.0-beta4, )",
"resolved": "0.13.0-beta4",
"contentHash": "5r9yBPe7XOnb4zAQYzyvlt85dpuIJQkPJYEns5hpfv/JbC4uBHVqnrzqiPlTiaWEcXFgmDjjh0ihVB0vvChuCQ==",
"dependencies": {
"Test.Core": "6.0.0"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"dependencies": {
"net6.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
}
},
"net7.0": {
"Test.System": {
"type": "Direct",
"requested": "[0.13.0-beta4, )",
"resolved": "0.13.0-beta4",
"contentHash": "5r9yBPe7XOnb4zAQYzyvlt85dpuIJQkPJYEns5hpfv/JbC4uBHVqnrzqiPlTiaWEcXFgmDjjh0ihVB0vvChuCQ=="
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"dependencies": {
"net6.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
}
},
"net7.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
}
}
}
}
39 changes: 39 additions & 0 deletions pkg/lockfile/fixtures/nuget/two-frameworks-mixed-packages.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"version": 1,
"dependencies": {
"net6.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
},
"Test.System": {
"type": "Direct",
"requested": "[0.13.0-beta4, )",
"resolved": "0.13.0-beta4",
"contentHash": "5r9yBPe7XOnb4zAQYzyvlt85dpuIJQkPJYEns5hpfv/JbC4uBHVqnrzqiPlTiaWEcXFgmDjjh0ihVB0vvChuCQ==",
"dependencies": {
"Test.Core": "6.0.0"
}
}
},
"net7.0": {
"Test.Core": {
"type": "Direct",
"requested": "[6.0.5, )",
"resolved": "6.0.5",
"contentHash": "FwdQVtpj34xt8vKyFUUeNIS+obWlEnSrSW7y1ivRVts/ZsrUsKyOd0bZehgFhWdnB/NBsa9DCWvNFMTO0XDFcg=="
},
"Test.System": {
"type": "Direct",
"requested": "[2.15.0, )",
"resolved": "2.15.0",
"contentHash": "t85dpuIJQkPJYEns5hpfv5r9yBPe7XOnb4zAQYzyvl/cXFgmDjjh0ihVB0vvChuCQJbC4uBHVqnrzqiPlTiaWE==",
"dependencies": {
"Test.Core": "6.0.0"
}
}
}
}
}
155 changes: 155 additions & 0 deletions pkg/lockfile/parse-nuget-lock-v1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package lockfile_test

import (
"github.com/g-rath/osv-detector/pkg/lockfile"
"testing"
)

func TestParseNuGetLock_v1_FileDoesNotExist(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/does-not-exist")

expectErrContaining(t, err, "could not read")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseNuGetLock_v1_InvalidJson(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/not-json.txt")

expectErrContaining(t, err, "could not parse")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseNuGetLock_v1_NoPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/empty.v1.json")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseNuGetLock_v1_OneFramework_OnePackage(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/one-framework-one-package.v1.json")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "Test.Core",
Version: "6.0.5",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
})
}

func TestParseNuGetLock_v1_OneFramework_TwoPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/one-framework-two-packages.v1.json")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "Test.Core",
Version: "6.0.5",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
{
Name: "Test.System",
Version: "0.13.0-beta4",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
})
}

func TestParseNuGetLock_v1_TwoFrameworks_MixedPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/two-frameworks-mixed-packages.v1.json")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "Test.Core",
Version: "6.0.5",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
{
Name: "Test.System",
Version: "0.13.0-beta4",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
{
Name: "Test.System",
Version: "2.15.0",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
})
}

func TestParseNuGetLock_v1_TwoFrameworks_DifferentPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/two-frameworks-different-packages.v1.json")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "Test.Core",
Version: "6.0.5",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
{
Name: "Test.System",
Version: "0.13.0-beta4",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
})
}

func TestParseNuGetLock_v1_TwoFrameworks_DuplicatePackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseNuGetLock("fixtures/nuget/two-frameworks-duplicate-packages.v1.json")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "Test.Core",
Version: "6.0.5",
Ecosystem: lockfile.NuGetEcosystem,
CompareAs: lockfile.NuGetEcosystem,
},
})
}
73 changes: 73 additions & 0 deletions pkg/lockfile/parse-nuget-lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package lockfile

import (
"encoding/json"
"errors"
"fmt"
"os"
)

type NuGetLockPackage struct {
Resolved string `json:"resolved"`
}

// NuGetLockfile contains the required dependency information as defined in
// https://github.com/NuGet/NuGet.Client/blob/6.5.0.136/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileFormat.cs
type NuGetLockfile struct {
Version int `json:"version"`
Dependencies map[string]map[string]NuGetLockPackage `json:"dependencies"`
}

const NuGetEcosystem Ecosystem = "NuGet"

func parseNuGetLockDependencies(dependencies map[string]NuGetLockPackage) map[string]PackageDetails {
details := map[string]PackageDetails{}

for name, dependency := range dependencies {
details[name+"@"+dependency.Resolved] = PackageDetails{
Name: name,
Version: dependency.Resolved,
Ecosystem: NuGetEcosystem,
CompareAs: NuGetEcosystem,
}
}

return details
}

func parseNuGetLock(lockfile NuGetLockfile) ([]PackageDetails, error) {
details := map[string]PackageDetails{}

// go through the dependencies for each framework, e.g. `net6.0` and parse
// its dependencies, there might be different or duplicate dependencies
// between frameworks
for _, dependencies := range lockfile.Dependencies {
details = mergePkgDetailsMap(details, parseNuGetLockDependencies(dependencies))
}

return pkgDetailsMapToSlice(details), nil
}

var ErrNuGetUnsupportedLockfileVersion = errors.New("unsupported lockfile version")

func ParseNuGetLock(pathToLockfile string) ([]PackageDetails, error) {
var parsedLockfile *NuGetLockfile

lockfileContents, err := os.ReadFile(pathToLockfile)

if err != nil {
return []PackageDetails{}, fmt.Errorf("could not read %s: %w", pathToLockfile, err)
}

err = json.Unmarshal(lockfileContents, &parsedLockfile)

if err != nil {
return []PackageDetails{}, fmt.Errorf("could not parse %s: %w", pathToLockfile, err)
}

if parsedLockfile.Version != 1 {
return []PackageDetails{}, fmt.Errorf("could not parse %s: %w", pathToLockfile, ErrNuGetUnsupportedLockfileVersion)
}

return parseNuGetLock(*parsedLockfile)
}

0 comments on commit f3048cf

Please sign in to comment.