Repository: google/osv-scanner

Issue:
Scan nuget 
Report on nuget package usage that has a vulnerability.  
- packages.config
- packagereference nodes in 
  - *.*proj 
  - *.props files

https://devblogs.microsoft.com/nuget/how-to-scan-nuget-packages-for-security-vulnerabilities/

Hints from issue comments:
@vbjay: Wow. Thanks.  Need to take on a spin. If we can run as a build step in ci/cd pipeline this will rock.  I love GitHub's dependabot but we have azure DevOps repos. So making this a build output updater and potentially a build failure cause would be awesome. @carlin-q-scott: @vbjay [BetterScan](https://github.com/marcinguy/betterscan-ce#azure-devops-integration) uses osv-scanner and integrates with Azure DevOps. So as soon as this feature gets released, we can bug Marcinguy to pull in the update.
@carlin-q-scott: @oliverchang When are we likely to see this get released?@oliverchang: > @oliverchang When are we likely to see this get released?

@another-rex Let's cut a release this week! @G-Rath: fwiw ideally google/osv-scanner#124 should be addressed [before a new release is cut](https://github.com/google/osv-scanner/pull/107#discussion_r1064271670)@G-Rath: oh and https://github.com/google/osv-scanner/pull/132 too 😅 @oliverchang: This is now released! https://github.com/google/osv-scanner/releases/tag/v1.1.0

Note that this release only includes support for `package.lock.json`. I'm not familiar with NuGet as an ecosystem at all, but from the original issue it sounds like there might be more ways to specify dependencies ? (e.g. *.sln, *.props)@piraces: Hi! First of all thank you for the awesome work!
I want to say that I think this issue should not be closed, as @vbjay stated, it should review references in:

- `packages.config`
- `packagereference` nodes in
  - `*.*proj`
  - `*.props` files

With the latest version, using `osv-scanner` in dotnet projects does not detect vulnerable dependencies as the dotnet CLI does.
As a dotnet developer I can say also it's not so common to have `package.lock.json` in projects (unfortunately) 😢 @vbjay: > Hi! First of all thank you for the awesome work!
> I want to say that I think this issue should not be closed, as @vbjay stated, it should review references in:
> 
> - `packages.config`
> - `packagereference` nodes in
>   - `*.*proj`
>   - `*.props` files
> 
> With the latest version, using `osv-scanner` in dotnet projects does not detect vulnerable dependencies as the dotnet CLI does.
> As a dotnet developer I can say also it's not so common to have `package.lock.json` in projects (unfortunately) 😢 

Can you create a  new issue linking  to this one test demoing this discrepancy along with osv results vs `dotnet list package --vulnerable` and does the dotnet command handle all nuget ref types?@vbjay: Basically trying to determine if the scanner just needs to pull results from dotnet  command run or if both have limitations. @oliverchang: > types

+1, please open a new issue so we can track it :) @piraces: Opened in https://github.com/google/osv-scalibr/issues/618 , let me know if some things are not so well explained 👍 
Thanks!

Solution patch:
diff --git a/README.md b/README.md
index fceaeb4a..aff358b9 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,7 @@ A wide range of lockfiles are supported by utilizing this [lockfile package](htt
 
 - `Cargo.lock`        
 - `package-lock.json` 
+- `packages.lock.json`
 - `yarn.lock`         
 - `pnpm-lock.yaml`    
 - `composer.lock`     
diff --git a/pkg/lockfile/ecosystems.go b/pkg/lockfile/ecosystems.go
index adc5dc78..17a79d92 100644
--- a/pkg/lockfile/ecosystems.go
+++ b/pkg/lockfile/ecosystems.go
@@ -3,6 +3,7 @@ package lockfile
 func KnownEcosystems() []Ecosystem {
 	return []Ecosystem{
 		NpmEcosystem,
+		NuGetEcosystem,
 		CargoEcosystem,
 		BundlerEcosystem,
 		ComposerEcosystem,
diff --git a/pkg/lockfile/fixtures/nuget/empty.v0.json b/pkg/lockfile/fixtures/nuget/empty.v0.json
new file mode 100644
index 00000000..d18d88b1
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/empty.v0.json
@@ -0,0 +1,4 @@
+{
+  "version": 0,
+  "dependencies": {}
+}
diff --git a/pkg/lockfile/fixtures/nuget/empty.v1.json b/pkg/lockfile/fixtures/nuget/empty.v1.json
new file mode 100644
index 00000000..0a838b33
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/empty.v1.json
@@ -0,0 +1,4 @@
+{
+  "version": 1,
+  "dependencies": {}
+}
diff --git a/pkg/lockfile/fixtures/nuget/not-json.txt b/pkg/lockfile/fixtures/nuget/not-json.txt
new file mode 100644
index 00000000..3ae3a213
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/not-json.txt
@@ -0,0 +1 @@
+this is not json!
diff --git a/pkg/lockfile/fixtures/nuget/one-framework-one-package.v1.json b/pkg/lockfile/fixtures/nuget/one-framework-one-package.v1.json
new file mode 100644
index 00000000..9450ebfe
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/one-framework-one-package.v1.json
@@ -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=="
+      }
+    }
+  }
+}
diff --git a/pkg/lockfile/fixtures/nuget/one-framework-two-packages.v1.json b/pkg/lockfile/fixtures/nuget/one-framework-two-packages.v1.json
new file mode 100644
index 00000000..9bfe17ed
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/one-framework-two-packages.v1.json
@@ -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"
+        }
+      }
+    }
+  }
+}
diff --git a/pkg/lockfile/fixtures/nuget/two-frameworks-different-packages.v1.json b/pkg/lockfile/fixtures/nuget/two-frameworks-different-packages.v1.json
new file mode 100644
index 00000000..551a863c
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/two-frameworks-different-packages.v1.json
@@ -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=="
+      }
+    }
+  }
+}
diff --git a/pkg/lockfile/fixtures/nuget/two-frameworks-duplicate-packages.v1.json b/pkg/lockfile/fixtures/nuget/two-frameworks-duplicate-packages.v1.json
new file mode 100644
index 00000000..f84c3fc5
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/two-frameworks-duplicate-packages.v1.json
@@ -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=="
+      }
+    }
+  }
+}
diff --git a/pkg/lockfile/fixtures/nuget/two-frameworks-mixed-packages.v1.json b/pkg/lockfile/fixtures/nuget/two-frameworks-mixed-packages.v1.json
new file mode 100644
index 00000000..19ac4409
--- /dev/null
+++ b/pkg/lockfile/fixtures/nuget/two-frameworks-mixed-packages.v1.json
@@ -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"
+        }
+      }
+    }
+  }
+}
diff --git a/pkg/lockfile/parse-nuget-lock.go b/pkg/lockfile/parse-nuget-lock.go
new file mode 100644
index 00000000..6d65d2a8
--- /dev/null
+++ b/pkg/lockfile/parse-nuget-lock.go
@@ -0,0 +1,70 @@
+package lockfile
+
+import (
+	"encoding/json"
+	"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
+}
+
+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: unsupported lock file version", pathToLockfile)
+	}
+
+	return parseNuGetLock(*parsedLockfile)
+}
diff --git a/pkg/lockfile/parse.go b/pkg/lockfile/parse.go
index 6c474098..6b24c9ec 100644
--- a/pkg/lockfile/parse.go
+++ b/pkg/lockfile/parse.go
@@ -24,6 +24,7 @@ var parsers = map[string]PackageDetailsParser{
 	"go.mod":                      ParseGoLock,
 	"mix.lock":                    ParseMixLock,
 	"package-lock.json":           ParseNpmLock,
+	"packages.lock.json":          ParseNuGetLock,
 	"pnpm-lock.yaml":              ParsePnpmLock,
 	"poetry.lock":                 ParsePoetryLock,
 	"pom.xml":                     ParseMavenLock,



Test patch:
diff --git a/pkg/lockfile/parse-nuget-lock-v1_test.go b/pkg/lockfile/parse-nuget-lock-v1_test.go
new file mode 100644
index 00000000..262fcd42
--- /dev/null
+++ b/pkg/lockfile/parse-nuget-lock-v1_test.go
@@ -0,0 +1,155 @@
+package lockfile_test
+
+import (
+	"github.com/google/osv-scanner/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,
+		},
+	})
+}
diff --git a/pkg/lockfile/parse-nuget-lock_test.go b/pkg/lockfile/parse-nuget-lock_test.go
new file mode 100644
index 00000000..84f77850
--- /dev/null
+++ b/pkg/lockfile/parse-nuget-lock_test.go
@@ -0,0 +1,15 @@
+package lockfile_test
+
+import (
+	"github.com/google/osv-scanner/pkg/lockfile"
+	"testing"
+)
+
+func TestParseNuGetLock_InvalidVersion(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseNuGetLock("fixtures/nuget/empty.v0.json")
+
+	expectErrContaining(t, err, "unsupported lock file version")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
diff --git a/pkg/lockfile/parse_test.go b/pkg/lockfile/parse_test.go
index a6153398..3ffd59c7 100644
--- a/pkg/lockfile/parse_test.go
+++ b/pkg/lockfile/parse_test.go
@@ -44,6 +44,7 @@ func TestFindParser(t *testing.T) {
 		"buildscript-gradle.lockfile",
 		"Cargo.lock",
 		"package-lock.json",
+		"packages.lock.json",
 		"yarn.lock",
 		"pnpm-lock.yaml",
 		"composer.lock",
@@ -91,6 +92,7 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
 		"buildscript-gradle.lockfile",
 		"Cargo.lock",
 		"package-lock.json",
+		"packages.lock.json",
 		"yarn.lock",
 		"pnpm-lock.yaml",
 		"composer.lock",