Repository: google/osv-scanner
Pull request #328 (2023-04-02 16:18:05)

chore(deps): update dependency jekyll-feed to v0.17.0
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| jekyll-feed | minor | `0.15.1` -> `0.17.0` |

---

### Configuration

📅 **Schedule**: Branch creation - "before 6am on monday" in timezone Australia/Sydney, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/google/osv-scanner).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS4yMy4zIiwidXBkYXRlZEluVmVyIjoiMzUuMjMuMyJ9-->



Patch:
diff --git a/.github/workflows/lint-action/action.yml b/.github/workflows/lint-action/action.yml
index 8fcee21..4257475 100644
--- a/.github/workflows/lint-action/action.yml
+++ b/.github/workflows/lint-action/action.yml
@@ -22,4 +22,6 @@ runs:
       uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0
       with:
         # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
-        version: v1.51.1
\ No newline at end of file
+        version: v1.51.1
+        # https://github.com/golangci/golangci-lint-action/issues/135
+        skip-pkg-cache: true
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 5aa1864..a8a75a5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,7 +24,7 @@ RUN go mod download
 COPY ./ /src/
 RUN go build -o osv-scanner ./cmd/osv-scanner/
 
-FROM alpine:3.17@sha256:ff6bdca1701f3a8a67e328815ff2346b0e4067d32ec36b7992c1fdc001dc8517
+FROM alpine:3.17@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126
 RUN apk --no-cache add \
     ca-certificates \
     git
diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go
index 7d80646..c0c0df1 100644
--- a/cmd/osv-scanner/main.go
+++ b/cmd/osv-scanner/main.go
@@ -6,8 +6,8 @@ import (
 	"io"
 	"os"
 
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/osvscanner"
+	"github.com/google/osv-scanner/pkg/reporter"
 
 	"github.com/urfave/cli/v2"
 )
@@ -20,10 +20,10 @@ var (
 )
 
 func run(args []string, stdout, stderr io.Writer) int {
-	var r *output.Reporter
+	var r *reporter.Reporter
 
 	cli.VersionPrinter = func(ctx *cli.Context) {
-		r = output.NewReporter(ctx.App.Writer, ctx.App.ErrWriter, "")
+		r = reporter.NewReporter(ctx.App.Writer, ctx.App.ErrWriter, "")
 		r.PrintText(fmt.Sprintf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date))
 	}
 
@@ -109,7 +109,7 @@ func run(args []string, stdout, stderr io.Writer) int {
 				format = "json"
 			}
 
-			r = output.NewReporter(stdout, stderr, format)
+			r = reporter.NewReporter(stdout, stderr, format)
 
 			vulnResult, err := osvscanner.DoScan(osvscanner.ScannerActions{
 				LockfilePaths:            context.StringSlice("lockfile"),
@@ -133,7 +133,7 @@ func run(args []string, stdout, stderr io.Writer) int {
 
 	if err := app.Run(args); err != nil {
 		if r == nil {
-			r = output.NewReporter(stdout, stderr, "")
+			r = reporter.NewReporter(stdout, stderr, "")
 		}
 		if errors.Is(err, osvscanner.VulnerabilitiesFoundErr) {
 			return 1
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
index c8507e0..e6138cf 100644
--- a/docs/Gemfile.lock
+++ b/docs/Gemfile.lock
@@ -6,14 +6,14 @@ GEM
       i18n (>= 1.6, < 2)
       minitest (>= 5.1)
       tzinfo (~> 2.0)
-    addressable (2.8.1)
+    addressable (2.8.2)
       public_suffix (>= 2.0.2, < 6.0)
     coffee-script (2.4.1)
       coffee-script-source
       execjs
     coffee-script-source (1.11.1)
     colorator (1.1.0)
-    commonmarker (0.23.8)
+    commonmarker (0.23.9)
     concurrent-ruby (1.2.2)
     dnsruby (1.61.9)
       simpleidn (~> 0.1)
@@ -210,7 +210,7 @@ GEM
       jekyll-feed (~> 0.9)
       jekyll-seo-tag (~> 2.1)
     minitest (5.18.0)
-    nokogiri (1.14.1-x86_64-linux)
+    nokogiri (1.14.3-x86_64-linux)
       racc (~> 1.4)
     octokit (4.25.1)
       faraday (>= 1, < 3)
diff --git a/docs/usage.md b/docs/usage.md
index 4c268b3..385f01e 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -95,7 +95,8 @@ it should infer the parser based on the filename:
 osv-scanner --lockfile ':/path/to/my:projects/package-lock.json'
 ```
 
-### Scanning with call analysis
+### Scanning with call analysis  
+
 {: .note }
 Features and flags with the `experimental` prefix might change or be removed with only a minor version update.
 
diff --git a/internal/sbom/cyclonedx.go b/internal/sbom/cyclonedx.go
index 9091c38..069b629 100644
--- a/internal/sbom/cyclonedx.go
+++ b/internal/sbom/cyclonedx.go
@@ -1,6 +1,7 @@
 package sbom
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"path/filepath"
@@ -11,10 +12,21 @@ import (
 
 type CycloneDX struct{}
 
+type cyclonedxType struct {
+	name    string
+	bomType cyclonedx.BOMFileFormat
+}
+
 var (
-	cycloneDXTypes = []cyclonedx.BOMFileFormat{
-		cyclonedx.BOMFileFormatJSON,
-		cyclonedx.BOMFileFormatXML,
+	cycloneDXTypes = []cyclonedxType{
+		{
+			name:    "json",
+			bomType: cyclonedx.BOMFileFormatJSON,
+		},
+		{
+			name:    "xml",
+			bomType: cyclonedx.BOMFileFormatXML,
+		},
 	}
 )
 
@@ -77,6 +89,8 @@ func (c *CycloneDX) enumeratePackages(bom *cyclonedx.BOM, callback func(Identifi
 }
 
 func (c *CycloneDX) GetPackages(r io.ReadSeeker, callback func(Identifier) error) error {
+	//nolint:prealloc // Not sure how many there will be in advance.
+	var errs []error
 	var bom cyclonedx.BOM
 
 	for _, formatType := range cycloneDXTypes {
@@ -84,12 +98,21 @@ func (c *CycloneDX) GetPackages(r io.ReadSeeker, callback func(Identifier) error
 		if err != nil {
 			return fmt.Errorf("failed to seek to start of file: %w", err)
 		}
-		decoder := cyclonedx.NewBOMDecoder(r, formatType)
+		decoder := cyclonedx.NewBOMDecoder(r, formatType.bomType)
 		err = decoder.Decode(&bom)
-		if err == nil && (bom.BOMFormat == "CycloneDX" || strings.HasPrefix(bom.XMLNS, "http://cyclonedx.org/schema/bom")) {
-			return c.enumeratePackages(&bom, callback)
+		if err == nil {
+			if bom.BOMFormat == "CycloneDX" || strings.HasPrefix(bom.XMLNS, "http://cyclonedx.org/schema/bom") {
+				return c.enumeratePackages(&bom, callback)
+			} else {
+				err = errors.New("invalid BOMFormat")
+			}
 		}
+
+		errs = append(errs, fmt.Errorf("failed trying %s: %w", formatType.name, err))
 	}
 
-	return ErrInvalidFormat
+	return InvalidFormatError{
+		msg:  "failed to parse CycloneDX",
+		errs: errs,
+	}
 }
diff --git a/internal/sbom/sbom.go b/internal/sbom/sbom.go
index a53e3cc..d948195 100644
--- a/internal/sbom/sbom.go
+++ b/internal/sbom/sbom.go
@@ -1,8 +1,9 @@
 package sbom
 
 import (
-	"errors"
+	"fmt"
 	"io"
+	"strings"
 )
 
 // Identifier is the identifier extracted from the SBOM.
@@ -18,13 +19,23 @@ type SBOMReader interface {
 	GetPackages(io.ReadSeeker, func(Identifier) error) error
 }
 
-var (
-	ErrInvalidFormat = errors.New("invalid format")
-)
-
 var (
 	Providers = []SBOMReader{
 		&SPDX{},
 		&CycloneDX{},
 	}
 )
+
+type InvalidFormatError struct {
+	msg  string
+	errs []error
+}
+
+func (e InvalidFormatError) Error() string {
+	errStrings := make([]string, 0, len(e.errs))
+	for _, e := range e.errs {
+		errStrings = append(errStrings, "\t"+e.Error())
+	}
+
+	return fmt.Sprintf("%s:\n%s", e.msg, strings.Join(errStrings, "\n"))
+}
diff --git a/internal/sbom/spdx.go b/internal/sbom/spdx.go
index 70e5860..191e728 100644
--- a/internal/sbom/spdx.go
+++ b/internal/sbom/spdx.go
@@ -16,11 +16,25 @@ import (
 type SPDX struct{}
 type spdxLoader func(io.Reader) (*v2_3.Document, error)
 
+type loader struct {
+	name   string
+	loader spdxLoader
+}
+
 var (
-	spdxLoaders = []spdxLoader{
-		spdx_json.Load2_3,
-		rdfloader.Load2_3,
-		tvloader.Load2_3,
+	spdxLoaders = []loader{
+		{
+			name:   "json",
+			loader: spdx_json.Load2_3,
+		},
+		{
+			name:   "rdf",
+			loader: rdfloader.Load2_3,
+		},
+		{
+			name:   "tv",
+			loader: tvloader.Load2_3,
+		},
 	}
 )
 
@@ -52,16 +66,22 @@ func (s *SPDX) enumeratePackages(doc *v2_3.Document, callback func(Identifier) e
 }
 
 func (s *SPDX) GetPackages(r io.ReadSeeker, callback func(Identifier) error) error {
+	//nolint:prealloc // Not sure how many there will be in advance.
+	var errs []error
 	for _, loader := range spdxLoaders {
 		_, err := r.Seek(0, io.SeekStart)
 		if err != nil {
 			return fmt.Errorf("failed to seek to start of file: %w", err)
 		}
-		doc, err := loader(r)
+		doc, err := loader.loader(r)
 		if err == nil {
 			return s.enumeratePackages(doc, callback)
 		}
+		errs = append(errs, fmt.Errorf("failed trying %s: %w", loader.name, err))
 	}
 
-	return ErrInvalidFormat
+	return InvalidFormatError{
+		msg:  "failed to parse SPDX",
+		errs: errs,
+	}
 }
diff --git a/internal/sourceanalysis/go.go b/internal/sourceanalysis/go.go
index a2a599a..88ae0d5 100644
--- a/internal/sourceanalysis/go.go
+++ b/internal/sourceanalysis/go.go
@@ -5,13 +5,13 @@ import (
 	"path/filepath"
 
 	"github.com/google/osv-scanner/internal/govulncheckshim"
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/models"
+	"github.com/google/osv-scanner/pkg/reporter"
 	"golang.org/x/exp/slices"
 	"golang.org/x/vuln/exp/govulncheck"
 )
 
-func goAnalysis(r *output.Reporter, pkgs []models.PackageVulns, source models.SourceInfo) {
+func goAnalysis(r *reporter.Reporter, pkgs []models.PackageVulns, source models.SourceInfo) {
 	vulns, vulnsByID := vulnsFromAllPkgs(pkgs)
 	res, err := govulncheckshim.RunGoVulnCheck(filepath.Dir(source.Path), vulns)
 	if err != nil {
diff --git a/internal/sourceanalysis/sourceanalysis.go b/internal/sourceanalysis/sourceanalysis.go
index 121233b..885a2d0 100644
--- a/internal/sourceanalysis/sourceanalysis.go
+++ b/internal/sourceanalysis/sourceanalysis.go
@@ -3,8 +3,8 @@ package sourceanalysis
 import (
 	"path/filepath"
 
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/models"
+	"github.com/google/osv-scanner/pkg/reporter"
 )
 
 // vulnsFromAllPkgs returns the flattened list of unique vulnerabilities
@@ -25,7 +25,7 @@ func vulnsFromAllPkgs(pkgs []models.PackageVulns) ([]models.Vulnerability, map[s
 }
 
 // Run runs the language specific analyzers on the code given packages and source info
-func Run(r *output.Reporter, source models.SourceInfo, pkgs []models.PackageVulns) {
+func Run(r *reporter.Reporter, source models.SourceInfo, pkgs []models.PackageVulns) {
 	// GoVulnCheck
 	if source.Type == "lockfile" && filepath.Base(source.Path) == "go.mod" {
 		goAnalysis(r, pkgs, source)
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 91c3226..cf076d1 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -6,9 +6,8 @@ import (
 	"path/filepath"
 	"time"
 
-	"github.com/google/osv-scanner/internal/output"
-
 	"github.com/BurntSushi/toml"
+	"github.com/google/osv-scanner/pkg/reporter"
 	"golang.org/x/exp/slices"
 )
 
@@ -64,7 +63,7 @@ func (c *ConfigManager) UseOverride(configPath string) error {
 }
 
 // Attempts to get the config
-func (c *ConfigManager) Get(r *output.Reporter, targetPath string) Config {
+func (c *ConfigManager) Get(r *reporter.Reporter, targetPath string) Config {
 	if c.OverrideConfig != nil {
 		return *c.OverrideConfig
 	}
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))
 
diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go
index c502302..25fa041 100644
--- a/pkg/osvscanner/osvscanner.go
+++ b/pkg/osvscanner/osvscanner.go
@@ -9,12 +9,12 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/internal/sbom"
 	"github.com/google/osv-scanner/pkg/config"
 	"github.com/google/osv-scanner/pkg/lockfile"
 	"github.com/google/osv-scanner/pkg/models"
 	"github.com/google/osv-scanner/pkg/osv"
+	"github.com/google/osv-scanner/pkg/reporter"
 
 	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/osfs"
@@ -52,7 +52,7 @@ var OnlyUncalledVulnerabilitiesFoundErr = errors.New("only uncalled vulnerabilit
 //   - Any lockfiles with scanLockfile
 //   - Any SBOM files with scanSBOMFile
 //   - Any git repositories with scanGit
-func scanDir(r *output.Reporter, query *osv.BatchedQuery, dir string, skipGit bool, recursive bool, useGitIgnore bool) error {
+func scanDir(r *reporter.Reporter, query *osv.BatchedQuery, dir string, skipGit bool, recursive bool, useGitIgnore bool) error {
 	var ignoreMatcher *gitIgnoreMatcher
 	if useGitIgnore {
 		var err error
@@ -114,7 +114,7 @@ func scanDir(r *output.Reporter, query *osv.BatchedQuery, dir string, skipGit bo
 			// No need to check for error
 			// If scan fails, it means it isn't a valid SBOM file,
 			// so just move onto the next file
-			_ = scanSBOMFile(r, query, path)
+			_ = scanSBOMFile(r, query, path, true)
 		}
 
 		if !root && !recursive && info.IsDir() {
@@ -180,7 +180,7 @@ func (m *gitIgnoreMatcher) match(absPath string, isDir bool) (bool, error) {
 
 // scanLockfile will load, identify, and parse the lockfile path passed in, and add the dependencies specified
 // within to `query`
-func scanLockfile(r *output.Reporter, query *osv.BatchedQuery, path string, parseAs string) error {
+func scanLockfile(r *reporter.Reporter, query *osv.BatchedQuery, path string, parseAs string) error {
 	var err error
 	var parsedLockfile lockfile.Lockfile
 
@@ -221,10 +221,15 @@ func scanLockfile(r *output.Reporter, query *osv.BatchedQuery, path string, pars
 
 // scanSBOMFile will load, identify, and parse the SBOM path passed in, and add the dependencies specified
 // within to `query`
-func scanSBOMFile(r *output.Reporter, query *osv.BatchedQuery, path string) error {
+func scanSBOMFile(r *reporter.Reporter, query *osv.BatchedQuery, path string, fromFSScan bool) error {
+	var errs []error
 	for _, provider := range sbom.Providers {
-		if !provider.MatchesRecognizedFileNames(path) {
-			// Skip if filename is not usually a sbom file of this format
+		if fromFSScan && !provider.MatchesRecognizedFileNames(path) {
+			// Skip if filename is not usually a sbom file of this format.
+			// Only do this if this is being done in a filesystem scanning context, where we need to be
+			// careful about spending too much time attempting to parse unrelated files.
+			// If this is coming from an explicit scan argument, be more relaxed here since it's common for
+			// filenames to not conform to expected filename standards.
 			continue
 		}
 
@@ -265,13 +270,23 @@ func scanSBOMFile(r *output.Reporter, query *osv.BatchedQuery, path string) erro
 			return nil
 		}
 
-		if errors.Is(err, sbom.ErrInvalidFormat) {
+		var formatErr sbom.InvalidFormatError
+		if errors.As(err, &formatErr) {
+			errs = append(errs, err)
 			continue
 		}
 
 		return err
 	}
 
+	// Don't log these errors if we're coming from a FS scan, since it can get very noisy.
+	if !fromFSScan {
+		r.PrintText("Failed to parse SBOM using all supported formats:\n")
+		for _, err := range errs {
+			r.PrintText(err.Error() + "\n")
+		}
+	}
+
 	return nil
 }
 
@@ -289,7 +304,7 @@ func getCommitSHA(repoDir string) (string, error) {
 }
 
 // Scan git repository. Expects repoDir to end with /
-func scanGit(r *output.Reporter, query *osv.BatchedQuery, repoDir string) error {
+func scanGit(r *reporter.Reporter, query *osv.BatchedQuery, repoDir string) error {
 	commit, err := getCommitSHA(repoDir)
 	if err != nil {
 		return err
@@ -310,7 +325,7 @@ func scanGitCommit(query *osv.BatchedQuery, commit string, source string) error
 	return nil
 }
 
-func scanDebianDocker(r *output.Reporter, query *osv.BatchedQuery, dockerImageName string) error {
+func scanDebianDocker(r *reporter.Reporter, query *osv.BatchedQuery, dockerImageName string) error {
 	cmd := exec.Command("docker", "run", "--rm", "--entrypoint", "/usr/bin/dpkg-query", dockerImageName, "-f", "${Package}###${Version}\\n", "-W")
 	stdout, err := cmd.StdoutPipe()
 
@@ -358,7 +373,7 @@ func scanDebianDocker(r *output.Reporter, query *osv.BatchedQuery, dockerImageNa
 }
 
 // Filters results according to config, preserving order. Returns total number of vulnerabilities removed.
-func filterResults(r *output.Reporter, results *models.VulnerabilityResults, configManager *config.ConfigManager) int {
+func filterResults(r *reporter.Reporter, results *models.VulnerabilityResults, configManager *config.ConfigManager) int {
 	removedCount := 0
 	newResults := []models.PackageSource{} // Want 0 vulnerabilities to show in JSON as an empty list, not null.
 	for _, pkgSrc := range results.Results {
@@ -384,7 +399,7 @@ func filterResults(r *output.Reporter, results *models.VulnerabilityResults, con
 }
 
 // Filters package-grouped vulnerabilities according to config, preserving ordering. Returns filtered package vulnerabilities.
-func filterPackageVulns(r *output.Reporter, pkgVulns models.PackageVulns, configToUse config.Config) models.PackageVulns {
+func filterPackageVulns(r *reporter.Reporter, pkgVulns models.PackageVulns, configToUse config.Config) models.PackageVulns {
 	ignoredVulns := map[string]struct{}{}
 	// Iterate over groups first to remove all aliases of ignored vulnerabilities.
 	var newGroups []models.GroupInfo
@@ -441,9 +456,9 @@ func parseLockfilePath(lockfileElem string) (string, string) {
 }
 
 // Perform osv scanner action, with optional reporter to output information
-func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityResults, error) {
+func DoScan(actions ScannerActions, r *reporter.Reporter) (models.VulnerabilityResults, error) {
 	if r == nil {
-		r = output.NewVoidReporter()
+		r = reporter.NewVoidReporter()
 	}
 
 	configManager := config.ConfigManager{
@@ -485,7 +500,7 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
 		if err != nil {
 			return models.VulnerabilityResults{}, fmt.Errorf("failed to resolved path with error %w", err)
 		}
-		err = scanSBOMFile(r, &query, sbomElem)
+		err = scanSBOMFile(r, &query, sbomElem, false)
 		if err != nil {
 			return models.VulnerabilityResults{}, err
 		}
diff --git a/pkg/osvscanner/vulnerability_result.go b/pkg/osvscanner/vulnerability_result.go
index fab5381..a477a5f 100644
--- a/pkg/osvscanner/vulnerability_result.go
+++ b/pkg/osvscanner/vulnerability_result.go
@@ -4,16 +4,16 @@ import (
 	"fmt"
 	"sort"
 
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/internal/sourceanalysis"
 	"github.com/google/osv-scanner/pkg/grouper"
 	"github.com/google/osv-scanner/pkg/models"
 	"github.com/google/osv-scanner/pkg/osv"
+	"github.com/google/osv-scanner/pkg/reporter"
 )
 
 // groupResponseBySource converts raw OSV API response into structured vulnerability information
 // grouped by source location.
-func groupResponseBySource(r *output.Reporter, query osv.BatchedQuery, resp *osv.HydratedBatchedResponse, callAnalysis bool) models.VulnerabilityResults {
+func groupResponseBySource(r *reporter.Reporter, query osv.BatchedQuery, resp *osv.HydratedBatchedResponse, callAnalysis bool) models.VulnerabilityResults {
 	output := models.VulnerabilityResults{
 		Results: []models.PackageSource{},
 	}
diff --git a/internal/output/reporter.go b/pkg/reporter/reporter.go
similarity index 86%
rename from internal/output/reporter.go
rename to pkg/reporter/reporter.go
index f37234a..0cf7171 100644
--- a/internal/output/reporter.go
+++ b/pkg/reporter/reporter.go
@@ -1,10 +1,11 @@
-package output
+package reporter
 
 import (
 	"fmt"
 	"io"
 	"strings"
 
+	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/models"
 )
 
@@ -60,11 +61,11 @@ func (r *Reporter) PrintText(msg string) {
 func (r *Reporter) PrintResult(vulnResult *models.VulnerabilityResults) error {
 	switch r.format {
 	case "json":
-		return PrintJSONResults(vulnResult, r.stdout)
+		return output.PrintJSONResults(vulnResult, r.stdout)
 	case "markdown":
-		PrintMarkdownTableResults(vulnResult, r.stdout)
+		output.PrintMarkdownTableResults(vulnResult, r.stdout)
 	case "table":
-		PrintTableResults(vulnResult, r.stdout)
+		output.PrintTableResults(vulnResult, r.stdout)
 	}
 
 	return nil


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()
 
diff --git a/pkg/osvscanner/osvscanner_internal_test.go b/pkg/osvscanner/osvscanner_internal_test.go
index fa7f310..f45e38d 100644
--- a/pkg/osvscanner/osvscanner_internal_test.go
+++ b/pkg/osvscanner/osvscanner_internal_test.go
@@ -5,10 +5,10 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/internal/testutility"
 	"github.com/google/osv-scanner/pkg/config"
 	"github.com/google/osv-scanner/pkg/models"
+	"github.com/google/osv-scanner/pkg/reporter"
 )
 
 func Test_filterResults(t *testing.T) {
@@ -51,7 +51,7 @@ func Test_filterResults(t *testing.T) {
 		tt := tt // Reinitialize for t.Parallel()
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			r := output.NewVoidReporter()
+			r := reporter.NewVoidReporter()
 			// ConfigManager looks for osv-scanner.toml in the source path.
 			// Sources in the test input should point to files/folders in the text fixture folder for this to work correctly.
 			configManager := config.ConfigManager{
diff --git a/pkg/osvscanner/vulnerability_result_internal_test.go b/pkg/osvscanner/vulnerability_result_internal_test.go
index 79e860b..212b5ff 100644
--- a/pkg/osvscanner/vulnerability_result_internal_test.go
+++ b/pkg/osvscanner/vulnerability_result_internal_test.go
@@ -4,15 +4,15 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/models"
 	"github.com/google/osv-scanner/pkg/osv"
+	"github.com/google/osv-scanner/pkg/reporter"
 )
 
 func Test_groupResponseBySource(t *testing.T) {
 	t.Parallel()
 	type args struct {
-		r            *output.Reporter
+		r            *reporter.Reporter
 		query        osv.BatchedQuery
 		resp         *osv.HydratedBatchedResponse
 		callAnalysis bool