Repository: google/osv-scanner
Pull request #325 (2023-04-02 12:21:18)

Support PNPM v6+ Lockfile
A proposed fix for https://github.com/google/osv-scanner/issues/324 issue.

dbtedman: @oliverchang @G-Rath anything else you need from my end to be able to review this PR?

Patch:
diff --git a/pkg/lockfile/fixtures/pnpm/one-package-v6-lockfile.yaml b/pkg/lockfile/fixtures/pnpm/one-package-v6-lockfile.yaml
new file mode 100644
index 0000000..44e228b
--- /dev/null
+++ b/pkg/lockfile/fixtures/pnpm/one-package-v6-lockfile.yaml
@@ -0,0 +1,14 @@
+lockfileVersion: '6.0'
+
+dependencies:
+  acorn:
+    specifier: 8.7.0
+    version: 8.7.0
+
+packages:
+
+  /acorn@8.7.0:
+    resolution: {integrity: sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+    dev: false
diff --git a/pkg/lockfile/fixtures/pnpm/scoped-packages-v6-lockfile.yaml b/pkg/lockfile/fixtures/pnpm/scoped-packages-v6-lockfile.yaml
new file mode 100644
index 0000000..2beb037
--- /dev/null
+++ b/pkg/lockfile/fixtures/pnpm/scoped-packages-v6-lockfile.yaml
@@ -0,0 +1,13 @@
+lockfileVersion: '6.0'
+
+dependencies:
+  '@typescript-eslint/types':
+    specifier: ^5.0.0
+    version: 5.57.1
+
+packages:
+
+  /@typescript-eslint/types@5.57.1:
+    resolution: {integrity: sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    dev: false
diff --git a/pkg/lockfile/parse-pnpm-lock.go b/pkg/lockfile/parse-pnpm-lock.go
index 29f0628..37a6331 100644
--- a/pkg/lockfile/parse-pnpm-lock.go
+++ b/pkg/lockfile/parse-pnpm-lock.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"os"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"gopkg.in/yaml.v3"
@@ -27,6 +28,30 @@ type PnpmLockfile struct {
 	Packages map[string]PnpmLockPackage `yaml:"packages,omitempty"`
 }
 
+type pnpmLockfileV6 struct {
+	Version  string                     `yaml:"lockfileVersion"`
+	Packages map[string]PnpmLockPackage `yaml:"packages,omitempty"`
+}
+
+func (l *PnpmLockfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var lockfileV6 pnpmLockfileV6
+
+	if err := unmarshal(&lockfileV6); err != nil {
+		return err
+	}
+
+	parsedVersion, err := strconv.ParseFloat(lockfileV6.Version, 64)
+
+	if err != nil {
+		return err
+	}
+
+	l.Version = parsedVersion
+	l.Packages = lockfileV6.Packages
+
+	return nil
+}
+
 const PnpmEcosystem = NpmEcosystem
 
 func startsWithNumber(str string) bool {
@@ -64,6 +89,10 @@ func extractPnpmPackageNameAndVersion(dependencyPath string) (string, string) {
 		version = parts[0]
 	}
 
+	if version == "" {
+		name, version = parseNameAtVersion(name)
+	}
+
 	if version == "" || !startsWithNumber(version) {
 		return "", ""
 	}
@@ -77,6 +106,17 @@ func extractPnpmPackageNameAndVersion(dependencyPath string) (string, string) {
 	return name, version
 }
 
+func parseNameAtVersion(value string) (name string, version string) {
+	// look for pattern "name@version", where name is allowed to contain zero or more "@"
+	matches := regexp.MustCompile(`^(.+)@([\d.]+)$`).FindStringSubmatch(value)
+
+	if len(matches) != 3 {
+		return name, ""
+	}
+
+	return matches[1], matches[2]
+}
+
 func parsePnpmLock(lockfile PnpmLockfile) []PackageDetails {
 	packages := make([]PackageDetails, 0, len(lockfile.Packages))
 


Test patch:
diff --git a/pkg/lockfile/parse-pnpm-lock_test.go b/pkg/lockfile/parse-pnpm-lock_test.go
index a572328..72638f4 100644
--- a/pkg/lockfile/parse-pnpm-lock_test.go
+++ b/pkg/lockfile/parse-pnpm-lock_test.go
@@ -54,6 +54,25 @@ func TestParsePnpmLock_OnePackage(t *testing.T) {
 	})
 }
 
+func TestParsePnpmLock_OnePackageV6Lockfile(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParsePnpmLock("fixtures/pnpm/one-package-v6-lockfile.yaml")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "acorn",
+			Version:   "8.7.0",
+			Ecosystem: lockfile.PnpmEcosystem,
+			CompareAs: lockfile.PnpmEcosystem,
+		},
+	})
+}
+
 func TestParsePnpmLock_OnePackageDev(t *testing.T) {
 	t.Parallel()
 
@@ -92,6 +111,25 @@ func TestParsePnpmLock_ScopedPackages(t *testing.T) {
 	})
 }
 
+func TestParsePnpmLock_ScopedPackagesV6Lockfile(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParsePnpmLock("fixtures/pnpm/scoped-packages-v6-lockfile.yaml")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "@typescript-eslint/types",
+			Version:   "5.57.1",
+			Ecosystem: lockfile.PnpmEcosystem,
+			CompareAs: lockfile.PnpmEcosystem,
+		},
+	})
+}
+
 func TestParsePnpmLock_PeerDependencies(t *testing.T) {
 	t.Parallel()