cmd/govulncheck/govulnchecklib: make main functionality public

We add this package to make integration in gopls easier.
We do not plan to make this API stable.

Change-Id: Ia0ae93520b4ef69fb3ba25ea6eb56ffb2767c673
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/432180
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/cmd/govulncheck/binary_118.go b/cmd/govulncheck/govulnchecklib/binary_118.go
similarity index 94%
rename from cmd/govulncheck/binary_118.go
rename to cmd/govulncheck/govulnchecklib/binary_118.go
index 6c13fb7..32d2529 100644
--- a/cmd/govulncheck/binary_118.go
+++ b/cmd/govulncheck/govulnchecklib/binary_118.go
@@ -5,7 +5,7 @@
 //go:build go1.18
 // +build go1.18
 
-package main
+package govulnchecklib
 
 import (
 	"context"
diff --git a/cmd/govulncheck/binary_not118.go b/cmd/govulncheck/govulnchecklib/binary_not118.go
similarity index 94%
rename from cmd/govulncheck/binary_not118.go
rename to cmd/govulncheck/govulnchecklib/binary_not118.go
index 04bd8a6..8d39ad3 100644
--- a/cmd/govulncheck/binary_not118.go
+++ b/cmd/govulncheck/govulnchecklib/binary_not118.go
@@ -5,7 +5,7 @@
 //go:build !go1.18
 // +build !go1.18
 
-package main
+package govulnchecklib
 
 import (
 	"context"
diff --git a/cmd/govulncheck/errors.go b/cmd/govulncheck/govulnchecklib/errors.go
similarity index 97%
rename from cmd/govulncheck/errors.go
rename to cmd/govulncheck/govulnchecklib/errors.go
index 3008df0..758d63b 100644
--- a/cmd/govulncheck/errors.go
+++ b/cmd/govulncheck/govulnchecklib/errors.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package main
+package govulnchecklib
 
 import (
 	"errors"
diff --git a/cmd/govulncheck/formatting.go b/cmd/govulncheck/govulnchecklib/formatting.go
similarity index 98%
rename from cmd/govulncheck/formatting.go
rename to cmd/govulncheck/govulnchecklib/formatting.go
index 10814a4..eb7e7f1 100644
--- a/cmd/govulncheck/formatting.go
+++ b/cmd/govulncheck/govulnchecklib/formatting.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package main
+package govulnchecklib
 
 import (
 	"bytes"
diff --git a/cmd/govulncheck/formatting_test.go b/cmd/govulncheck/govulnchecklib/formatting_test.go
similarity index 98%
rename from cmd/govulncheck/formatting_test.go
rename to cmd/govulncheck/govulnchecklib/formatting_test.go
index 48fdd9e..9b54794 100644
--- a/cmd/govulncheck/formatting_test.go
+++ b/cmd/govulncheck/govulnchecklib/formatting_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package main
+package govulnchecklib
 
 import (
 	"bytes"
diff --git a/cmd/govulncheck/govulnchecklib/main.go b/cmd/govulncheck/govulnchecklib/main.go
new file mode 100644
index 0000000..9ed974b
--- /dev/null
+++ b/cmd/govulncheck/govulnchecklib/main.go
@@ -0,0 +1,426 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package govulnchecklib defines the main function for the govulncheck command.
+//
+// WARNING: this package is made public only to make it easy to integrate govulncheck
+// in certain tools like (e.g. gopls). The API is not stable and may change any time.
+package govulnchecklib
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"golang.org/x/exp/maps"
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/vuln/client"
+	"golang.org/x/vuln/cmd/govulncheck/internal/govulncheck"
+	"golang.org/x/vuln/osv"
+	"golang.org/x/vuln/vulncheck"
+)
+
+// Config is the configuration for Main.
+type Config struct {
+	// Analysis specifies the vulncheck analysis type. Valid types are "source" and "binary"
+	Analysis string
+	// OutputFormat specifies the result type. Valid types are:
+	//  "text": print human readable compact text output to STDOUT.
+	//  "verbose": print human readable verbose text output to STDOUT.
+	//  "json": print JSON-encoded vulncheck.Result.
+	OutputFormat string
+
+	// Patterns are either the binary path for "binary" analysis mode, or
+	// go package patterns for "source" analysis mode.
+	Patterns []string
+
+	// SourceLoadConfig specifies the package loading configuration.
+	SourceLoadConfig packages.Config
+}
+
+// Main is the main function for the govulncheck command line tool.
+func Main(cfg Config) {
+	dbs := []string{"https://vuln.go.dev"}
+	if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
+		dbs = strings.Split(GOVULNDB, ",")
+	}
+	dbClient, err := client.NewClient(dbs, client.Options{
+		HTTPCache: govulncheck.DefaultCache(),
+	})
+	if err != nil {
+		die("govulncheck: %s", err)
+	}
+	vcfg := &vulncheck.Config{Client: dbClient, SourceGoVersion: goVersion()}
+
+	patterns := cfg.Patterns
+	format := cfg.OutputFormat
+	if format == "text" || format == "verbose" {
+		fmt.Printf(`govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
+
+Scanning for dependencies with known vulnerabilities...
+`)
+	}
+	var (
+		r          *vulncheck.Result
+		pkgs       []*vulncheck.Package
+		unaffected []*vulncheck.Vuln
+		ctx        = context.Background()
+	)
+	switch cfg.Analysis {
+	case "binary":
+		f, err := os.Open(patterns[0])
+		if err != nil {
+			die("govulncheck: %v", err)
+		}
+		defer f.Close()
+		r, err = binary(ctx, f, vcfg)
+		if err != nil {
+			die("govulncheck: %v", err)
+		}
+	case "source":
+		cfg := &cfg.SourceLoadConfig
+		pkgs, err = govulncheck.LoadPackages(cfg, patterns...)
+		if err != nil {
+			// Try to provide a meaningful and actionable error message.
+			if !fileExists(filepath.Join(cfg.Dir, "go.mod")) {
+				die(noGoModErrorMessage)
+			} else if !fileExists(filepath.Join(cfg.Dir, "go.sum")) {
+				die(noGoSumErrorMessage)
+			}
+			die("govulncheck: %v", err)
+		}
+
+		// Sort pkgs so that the PkgNodes returned by vulncheck.Source will be
+		// deterministic.
+		sortPackages(pkgs)
+		r, err = vulncheck.Source(ctx, pkgs, vcfg)
+		if err != nil {
+			die("govulncheck: %v", err)
+		}
+		unaffected = filterUnaffected(r)
+		r.Vulns = filterCalled(r)
+	default:
+		die("govulncheck: invalid analysis mode %q", cfg.Analysis)
+	}
+
+	switch format {
+	case "json":
+		// Following golang.org/x/tools/go/analysis/singlechecker,
+		// return 0 exit code in -json mode.
+		writeJSON(r)
+		os.Exit(0)
+	case "text", "verbose":
+		// set of top-level packages, used to find representative symbols
+		ci := govulncheck.GetCallInfo(r, pkgs)
+		writeText(r, ci, unaffected, format == "verbose")
+	default:
+		die("govulncheck: unrecognized output type %q", cfg.OutputFormat)
+	}
+
+	// Following golang.org/x/tools/go/analysis/singlechecker,
+	// fail with 3 if there are findings (in this case, vulns).
+	exitCode := 0
+	if len(r.Vulns) > 0 {
+		exitCode = 3
+	}
+	os.Exit(exitCode)
+}
+
+// filterCalled returns vulnerabilities where the symbols are actually called.
+func filterCalled(r *vulncheck.Result) []*vulncheck.Vuln {
+	var vulns []*vulncheck.Vuln
+	for _, v := range r.Vulns {
+		if v.CallSink != 0 {
+			vulns = append(vulns, v)
+		}
+	}
+	sortVulns(vulns)
+	return vulns
+}
+
+// filterUnaffected returns vulnerabilities where no symbols are called,
+// grouped by module.
+func filterUnaffected(r *vulncheck.Result) []*vulncheck.Vuln {
+	// It is possible that the same vuln.OSV.ID has vuln.CallSink != 0
+	// for one symbol, but vuln.CallSink == 0 for a different one, so
+	// we need to filter out ones that have been called.
+	called := filterCalled(r)
+	calledIDs := map[string]bool{}
+	for _, vuln := range called {
+		calledIDs[vuln.OSV.ID] = true
+	}
+
+	idToVuln := map[string]*vulncheck.Vuln{}
+	for _, vuln := range r.Vulns {
+		if !calledIDs[vuln.OSV.ID] {
+			idToVuln[vuln.OSV.ID] = vuln
+		}
+	}
+	var output []*vulncheck.Vuln
+	for _, vuln := range idToVuln {
+		output = append(output, vuln)
+	}
+	sortVulns(output)
+	return output
+}
+
+func sortVulns(vulns []*vulncheck.Vuln) {
+	sort.Slice(vulns, func(i, j int) bool {
+		return vulns[i].OSV.ID > vulns[j].OSV.ID
+	})
+}
+
+func sortPackages(pkgs []*vulncheck.Package) {
+	sort.Slice(pkgs, func(i, j int) bool {
+		return pkgs[i].PkgPath < pkgs[j].PkgPath
+	})
+	for _, pkg := range pkgs {
+		sort.Slice(pkg.Imports, func(i, j int) bool {
+			return pkg.Imports[i].PkgPath < pkg.Imports[j].PkgPath
+		})
+	}
+}
+
+func writeJSON(r *vulncheck.Result) {
+	b, err := json.MarshalIndent(r, "", "\t")
+	if err != nil {
+		die("govulncheck: %s", err)
+	}
+	os.Stdout.Write(b)
+	fmt.Println()
+}
+
+const (
+	labelWidth = 16
+	lineLength = 55
+)
+
+func writeText(r *vulncheck.Result, ci *govulncheck.CallInfo, unaffected []*vulncheck.Vuln, verbose bool) {
+	uniqueVulns := map[string]bool{}
+	for _, v := range r.Vulns {
+		uniqueVulns[v.OSV.ID] = true
+	}
+	switch len(uniqueVulns) {
+	case 0:
+		fmt.Println("No vulnerabilities found.")
+	case 1:
+		fmt.Println("Found 1 known vulnerability.")
+	default:
+		fmt.Printf("Found %d known vulnerabilities.\n", len(uniqueVulns))
+	}
+	for idx, vg := range ci.VulnGroups {
+		fmt.Println()
+		// All the vulns in vg have the same PkgPath, ModPath and OSV.
+		// All have a non-zero CallSink.
+		v0 := vg[0]
+		id := v0.OSV.ID
+		details := wrap(v0.OSV.Details, 80-labelWidth)
+		found := foundVersion(v0.ModPath, v0.PkgPath, ci)
+		fixed := fixedVersion(v0.PkgPath, v0.OSV.Affected)
+
+		var stacks string
+		if !verbose {
+			stacks = defaultCallStacks(vg, ci)
+		} else {
+			stacks = verboseCallStacks(vg, ci)
+		}
+		var b strings.Builder
+		if len(stacks) > 0 {
+			b.WriteString(indent("\n\nCall stacks in your code:\n", 2))
+			b.WriteString(indent(stacks, 6))
+		}
+		writeVulnerability(idx+1, id, details, b.String(), found, fixed, platforms(v0.OSV))
+	}
+	if len(unaffected) > 0 {
+		fmt.Printf(`
+=== Informational ===
+
+The vulnerabilities below are in packages that you import, but your code
+doesn't appear to call any vulnerable functions. You may not need to take any
+action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
+for details.
+`)
+		for idx, vuln := range unaffected {
+			found := foundVersion(vuln.ModPath, vuln.PkgPath, ci)
+			fixed := fixedVersion(vuln.PkgPath, vuln.OSV.Affected)
+			fmt.Println()
+			writeVulnerability(idx+1, vuln.OSV.ID, vuln.OSV.Details, "", found, fixed, platforms(vuln.OSV))
+		}
+	}
+}
+
+func writeVulnerability(idx int, id, details, callstack, found, fixed, platforms string) {
+	if fixed == "" {
+		fixed = "N/A"
+	}
+	if platforms != "" {
+		platforms = "  Platforms: " + platforms + "\n"
+	}
+	fmt.Printf(`Vulnerability #%d: %s
+%s%s
+  Found in: %s
+  Fixed in: %s
+%s  More info: https://pkg.go.dev/vuln/%s
+`, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
+}
+
+func foundVersion(modulePath, pkgPath string, ci *govulncheck.CallInfo) string {
+	var found string
+	if v := ci.ModuleVersions[modulePath]; v != "" {
+		found = packageVersionString(pkgPath, v[1:])
+	}
+	return found
+}
+
+func fixedVersion(pkgPath string, affected []osv.Affected) string {
+	fixed := govulncheck.LatestFixed(affected)
+	if fixed != "" {
+		fixed = packageVersionString(pkgPath, fixed)
+	}
+	return fixed
+}
+
+func defaultCallStacks(vg []*vulncheck.Vuln, ci *govulncheck.CallInfo) string {
+	var summaries []string
+	for _, v := range vg {
+		if css := ci.CallStacks[v]; len(css) > 0 {
+			if sum := govulncheck.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath); sum != "" {
+				summaries = append(summaries, strings.TrimSpace(sum))
+			}
+		}
+	}
+	if len(summaries) > 0 {
+		sort.Strings(summaries)
+		summaries = compact(summaries)
+	}
+	var b strings.Builder
+	for _, s := range summaries {
+		b.WriteString(s)
+		b.WriteString("\n")
+	}
+	return b.String()
+}
+
+func verboseCallStacks(vg []*vulncheck.Vuln, ci *govulncheck.CallInfo) string {
+	// Display one full call stack for each vuln.
+	i := 1
+	nMore := 0
+	var b strings.Builder
+	for _, v := range vg {
+		css := ci.CallStacks[v]
+		if len(css) == 0 {
+			continue
+		}
+		b.WriteString(fmt.Sprintf("#%d: for function %s\n", i, v.Symbol))
+		for _, e := range css[0] {
+			b.WriteString(fmt.Sprintf("  %s\n", govulncheck.FuncName(e.Function)))
+			if pos := govulncheck.AbsRelShorter(govulncheck.FuncPos(e.Call)); pos != "" {
+				b.WriteString(fmt.Sprintf("      %s\n", pos))
+			}
+		}
+		i++
+		nMore += len(css) - 1
+	}
+	if nMore > 0 {
+		b.WriteString(fmt.Sprintf("    There are %d more call stacks available.\n", nMore))
+		b.WriteString(fmt.Sprintf("To see all of them, pass the -json flags.\n"))
+	}
+	return b.String()
+}
+
+// platforms returns a string describing the GOOS/GOARCH pairs that the vuln affects.
+// If it affects all of them, it returns the empty string.
+func platforms(e *osv.Entry) string {
+	platforms := map[string]bool{}
+	for _, a := range e.Affected {
+		for _, p := range a.EcosystemSpecific.Imports {
+			for _, os := range p.GOOS {
+				for _, arch := range p.GOARCH {
+					platforms[os+"/"+arch] = true
+				}
+			}
+		}
+	}
+	keys := maps.Keys(platforms)
+	sort.Strings(keys)
+	return strings.Join(keys, ", ")
+}
+
+func isFile(path string) bool {
+	s, err := os.Stat(path)
+	if err != nil {
+		return false
+	}
+	return !s.IsDir()
+}
+
+// compact replaces consecutive runs of equal elements with a single copy.
+// This is like the uniq command found on Unix.
+// compact modifies the contents of the slice s; it does not create a new slice.
+//
+// Modified (generics removed) from exp/slices/slices.go.
+func compact(s []string) []string {
+	if len(s) == 0 {
+		return s
+	}
+	i := 1
+	last := s[0]
+	for _, v := range s[1:] {
+		if v != last {
+			s[i] = v
+			i++
+			last = v
+		}
+	}
+	return s[:i]
+}
+
+func goVersion() string {
+	if v := os.Getenv("GOVERSION"); v != "" {
+		// Unlikely to happen in practice, mostly used for testing.
+		return v
+	}
+	out, err := exec.Command("go", "env", "GOVERSION").Output()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to determine go version; skipping stdlib scanning: %v\n", err)
+		return ""
+	}
+	return string(bytes.TrimSpace(out))
+}
+
+func packageVersionString(packagePath, version string) string {
+	v := "v" + version
+	if importPathInStdlib(packagePath) {
+		v = semverToGoTag(v)
+	}
+	return fmt.Sprintf("%s@%s", packagePath, v)
+}
+
+func die(format string, args ...interface{}) {
+	fmt.Fprintf(os.Stderr, format+"\n", args...)
+	os.Exit(1)
+}
+
+// indent returns the output of prefixing n spaces to s at every line break,
+// except for empty lines. See TestIndent for examples.
+func indent(s string, n int) string {
+	b := []byte(s)
+	var result []byte
+	shouldAppend := true
+	prefix := strings.Repeat(" ", n)
+	for _, c := range b {
+		if shouldAppend && c != '\n' {
+			result = append(result, prefix...)
+		}
+		result = append(result, c)
+		shouldAppend = c == '\n'
+	}
+	return string(result)
+}
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/govulnchecklib/main_test.go
similarity index 99%
rename from cmd/govulncheck/main_test.go
rename to cmd/govulncheck/govulnchecklib/main_test.go
index 93095a5..1cd7319 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/govulnchecklib/main_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package main
+package govulnchecklib
 
 import (
 	"testing"
diff --git a/cmd/govulncheck/stdlib.go b/cmd/govulncheck/govulnchecklib/stdlib.go
similarity index 98%
rename from cmd/govulncheck/stdlib.go
rename to cmd/govulncheck/govulnchecklib/stdlib.go
index a7b5be8..ccd276d 100644
--- a/cmd/govulncheck/stdlib.go
+++ b/cmd/govulncheck/govulnchecklib/stdlib.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package main
+package govulnchecklib
 
 import (
 	"fmt"
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index dcb02e7..2b3dc10 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -5,24 +5,15 @@
 package main
 
 import (
-	"bytes"
-	"context"
-	"encoding/json"
 	"flag"
 	"fmt"
 	"os"
-	"os/exec"
 	"path/filepath"
-	"sort"
 	"strings"
 
-	"golang.org/x/exp/maps"
 	"golang.org/x/tools/go/buildutil"
 	"golang.org/x/tools/go/packages"
-	"golang.org/x/vuln/client"
-	"golang.org/x/vuln/cmd/govulncheck/internal/govulncheck"
-	"golang.org/x/vuln/osv"
-	"golang.org/x/vuln/vulncheck"
+	"golang.org/x/vuln/cmd/govulncheck/govulnchecklib"
 )
 
 var (
@@ -77,7 +68,7 @@
 		buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
 	}
 
-	Main(Config{
+	govulnchecklib.Main(govulnchecklib.Config{
 		Analysis:     mode,
 		OutputFormat: outputType,
 		Patterns:     patterns,
@@ -101,332 +92,6 @@
 	}
 }
 
-// Config is the configuration for Main.
-type Config struct {
-	// Analysis specifies the vulncheck analysis type. Valid types are "source" and "binary"
-	Analysis string
-	// OutputFormat specifies the result type. Valid types are:
-	//  "text": print human readable compact text output to STDOUT.
-	//  "verbose": print human readable verbose text output to STDOUT.
-	//  "json": print JSON-encoded vulncheck.Result.
-	OutputFormat string
-
-	// Patterns are either the binary path for "binary" analysis mode, or
-	// go package patterns for "source" analysis mode.
-	Patterns []string
-
-	// SourceLoadConfig specifies the package loading configuration.
-	SourceLoadConfig packages.Config
-}
-
-// Main is the main function for the govulncheck command line tool.
-func Main(cfg Config) {
-	dbs := []string{"https://vuln.go.dev"}
-	if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
-		dbs = strings.Split(GOVULNDB, ",")
-	}
-	dbClient, err := client.NewClient(dbs, client.Options{
-		HTTPCache: govulncheck.DefaultCache(),
-	})
-	if err != nil {
-		die("govulncheck: %s", err)
-	}
-	vcfg := &vulncheck.Config{Client: dbClient, SourceGoVersion: goVersion()}
-
-	patterns := cfg.Patterns
-	format := cfg.OutputFormat
-	if format == "text" || format == "verbose" {
-		fmt.Printf(`govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
-
-Scanning for dependencies with known vulnerabilities...
-`)
-	}
-	var (
-		r          *vulncheck.Result
-		pkgs       []*vulncheck.Package
-		unaffected []*vulncheck.Vuln
-		ctx        = context.Background()
-	)
-	switch cfg.Analysis {
-	case "binary":
-		f, err := os.Open(patterns[0])
-		if err != nil {
-			die("govulncheck: %v", err)
-		}
-		defer f.Close()
-		r, err = binary(ctx, f, vcfg)
-		if err != nil {
-			die("govulncheck: %v", err)
-		}
-	case "source":
-		cfg := &cfg.SourceLoadConfig
-		pkgs, err = govulncheck.LoadPackages(cfg, patterns...)
-		if err != nil {
-			// Try to provide a meaningful and actionable error message.
-			if !fileExists(filepath.Join(cfg.Dir, "go.mod")) {
-				die(noGoModErrorMessage)
-			} else if !fileExists(filepath.Join(cfg.Dir, "go.sum")) {
-				die(noGoSumErrorMessage)
-			}
-			die("govulncheck: %v", err)
-		}
-
-		// Sort pkgs so that the PkgNodes returned by vulncheck.Source will be
-		// deterministic.
-		sortPackages(pkgs)
-		r, err = vulncheck.Source(ctx, pkgs, vcfg)
-		if err != nil {
-			die("govulncheck: %v", err)
-		}
-		unaffected = filterUnaffected(r)
-		r.Vulns = filterCalled(r)
-	default:
-		die("govulncheck: invalid analysis mode %q", cfg.Analysis)
-	}
-
-	switch format {
-	case "json":
-		// Following golang.org/x/tools/go/analysis/singlechecker,
-		// return 0 exit code in -json mode.
-		writeJSON(r)
-		os.Exit(0)
-	case "text", "verbose":
-		// set of top-level packages, used to find representative symbols
-		ci := govulncheck.GetCallInfo(r, pkgs)
-		writeText(r, ci, unaffected, format == "verbose")
-	default:
-		die("govulncheck: unrecognized output type %q", cfg.OutputFormat)
-	}
-
-	// Following golang.org/x/tools/go/analysis/singlechecker,
-	// fail with 3 if there are findings (in this case, vulns).
-	exitCode := 0
-	if len(r.Vulns) > 0 {
-		exitCode = 3
-	}
-	os.Exit(exitCode)
-}
-
-// filterCalled returns vulnerabilities where the symbols are actually called.
-func filterCalled(r *vulncheck.Result) []*vulncheck.Vuln {
-	var vulns []*vulncheck.Vuln
-	for _, v := range r.Vulns {
-		if v.CallSink != 0 {
-			vulns = append(vulns, v)
-		}
-	}
-	sortVulns(vulns)
-	return vulns
-}
-
-// filterUnaffected returns vulnerabilities where no symbols are called,
-// grouped by module.
-func filterUnaffected(r *vulncheck.Result) []*vulncheck.Vuln {
-	// It is possible that the same vuln.OSV.ID has vuln.CallSink != 0
-	// for one symbol, but vuln.CallSink == 0 for a different one, so
-	// we need to filter out ones that have been called.
-	called := filterCalled(r)
-	calledIDs := map[string]bool{}
-	for _, vuln := range called {
-		calledIDs[vuln.OSV.ID] = true
-	}
-
-	idToVuln := map[string]*vulncheck.Vuln{}
-	for _, vuln := range r.Vulns {
-		if !calledIDs[vuln.OSV.ID] {
-			idToVuln[vuln.OSV.ID] = vuln
-		}
-	}
-	var output []*vulncheck.Vuln
-	for _, vuln := range idToVuln {
-		output = append(output, vuln)
-	}
-	sortVulns(output)
-	return output
-}
-
-func sortVulns(vulns []*vulncheck.Vuln) {
-	sort.Slice(vulns, func(i, j int) bool {
-		return vulns[i].OSV.ID > vulns[j].OSV.ID
-	})
-}
-
-func sortPackages(pkgs []*vulncheck.Package) {
-	sort.Slice(pkgs, func(i, j int) bool {
-		return pkgs[i].PkgPath < pkgs[j].PkgPath
-	})
-	for _, pkg := range pkgs {
-		sort.Slice(pkg.Imports, func(i, j int) bool {
-			return pkg.Imports[i].PkgPath < pkg.Imports[j].PkgPath
-		})
-	}
-}
-
-func writeJSON(r *vulncheck.Result) {
-	b, err := json.MarshalIndent(r, "", "\t")
-	if err != nil {
-		die("govulncheck: %s", err)
-	}
-	os.Stdout.Write(b)
-	fmt.Println()
-}
-
-const (
-	labelWidth = 16
-	lineLength = 55
-)
-
-func writeText(r *vulncheck.Result, ci *govulncheck.CallInfo, unaffected []*vulncheck.Vuln, verbose bool) {
-	uniqueVulns := map[string]bool{}
-	for _, v := range r.Vulns {
-		uniqueVulns[v.OSV.ID] = true
-	}
-	switch len(uniqueVulns) {
-	case 0:
-		fmt.Println("No vulnerabilities found.")
-	case 1:
-		fmt.Println("Found 1 known vulnerability.")
-	default:
-		fmt.Printf("Found %d known vulnerabilities.\n", len(uniqueVulns))
-	}
-	for idx, vg := range ci.VulnGroups {
-		fmt.Println()
-		// All the vulns in vg have the same PkgPath, ModPath and OSV.
-		// All have a non-zero CallSink.
-		v0 := vg[0]
-		id := v0.OSV.ID
-		details := wrap(v0.OSV.Details, 80-labelWidth)
-		found := foundVersion(v0.ModPath, v0.PkgPath, ci)
-		fixed := fixedVersion(v0.PkgPath, v0.OSV.Affected)
-
-		var stacks string
-		if !verbose {
-			stacks = defaultCallStacks(vg, ci)
-		} else {
-			stacks = verboseCallStacks(vg, ci)
-		}
-		var b strings.Builder
-		if len(stacks) > 0 {
-			b.WriteString(indent("\n\nCall stacks in your code:\n", 2))
-			b.WriteString(indent(stacks, 6))
-		}
-		writeVulnerability(idx+1, id, details, b.String(), found, fixed, platforms(v0.OSV))
-	}
-	if len(unaffected) > 0 {
-		fmt.Printf(`
-=== Informational ===
-
-The vulnerabilities below are in packages that you import, but your code
-doesn't appear to call any vulnerable functions. You may not need to take any
-action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
-for details.
-`)
-		for idx, vuln := range unaffected {
-			found := foundVersion(vuln.ModPath, vuln.PkgPath, ci)
-			fixed := fixedVersion(vuln.PkgPath, vuln.OSV.Affected)
-			fmt.Println()
-			writeVulnerability(idx+1, vuln.OSV.ID, vuln.OSV.Details, "", found, fixed, platforms(vuln.OSV))
-		}
-	}
-}
-
-func writeVulnerability(idx int, id, details, callstack, found, fixed, platforms string) {
-	if fixed == "" {
-		fixed = "N/A"
-	}
-	if platforms != "" {
-		platforms = "  Platforms: " + platforms + "\n"
-	}
-	fmt.Printf(`Vulnerability #%d: %s
-%s%s
-  Found in: %s
-  Fixed in: %s
-%s  More info: https://pkg.go.dev/vuln/%s
-`, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
-}
-
-func foundVersion(modulePath, pkgPath string, ci *govulncheck.CallInfo) string {
-	var found string
-	if v := ci.ModuleVersions[modulePath]; v != "" {
-		found = packageVersionString(pkgPath, v[1:])
-	}
-	return found
-}
-
-func fixedVersion(pkgPath string, affected []osv.Affected) string {
-	fixed := govulncheck.LatestFixed(affected)
-	if fixed != "" {
-		fixed = packageVersionString(pkgPath, fixed)
-	}
-	return fixed
-}
-
-func defaultCallStacks(vg []*vulncheck.Vuln, ci *govulncheck.CallInfo) string {
-	var summaries []string
-	for _, v := range vg {
-		if css := ci.CallStacks[v]; len(css) > 0 {
-			if sum := govulncheck.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath); sum != "" {
-				summaries = append(summaries, strings.TrimSpace(sum))
-			}
-		}
-	}
-	if len(summaries) > 0 {
-		sort.Strings(summaries)
-		summaries = compact(summaries)
-	}
-	var b strings.Builder
-	for _, s := range summaries {
-		b.WriteString(s)
-		b.WriteString("\n")
-	}
-	return b.String()
-}
-
-func verboseCallStacks(vg []*vulncheck.Vuln, ci *govulncheck.CallInfo) string {
-	// Display one full call stack for each vuln.
-	i := 1
-	nMore := 0
-	var b strings.Builder
-	for _, v := range vg {
-		css := ci.CallStacks[v]
-		if len(css) == 0 {
-			continue
-		}
-		b.WriteString(fmt.Sprintf("#%d: for function %s\n", i, v.Symbol))
-		for _, e := range css[0] {
-			b.WriteString(fmt.Sprintf("  %s\n", govulncheck.FuncName(e.Function)))
-			if pos := govulncheck.AbsRelShorter(govulncheck.FuncPos(e.Call)); pos != "" {
-				b.WriteString(fmt.Sprintf("      %s\n", pos))
-			}
-		}
-		i++
-		nMore += len(css) - 1
-	}
-	if nMore > 0 {
-		b.WriteString(fmt.Sprintf("    There are %d more call stacks available.\n", nMore))
-		b.WriteString(fmt.Sprintf("To see all of them, pass the -json flags.\n"))
-	}
-	return b.String()
-}
-
-// platforms returns a string describing the GOOS/GOARCH pairs that the vuln affects.
-// If it affects all of them, it returns the empty string.
-func platforms(e *osv.Entry) string {
-	platforms := map[string]bool{}
-	for _, a := range e.Affected {
-		for _, p := range a.EcosystemSpecific.Imports {
-			for _, os := range p.GOOS {
-				for _, arch := range p.GOARCH {
-					platforms[os+"/"+arch] = true
-				}
-			}
-		}
-	}
-	keys := maps.Keys(platforms)
-	sort.Strings(keys)
-	return strings.Join(keys, ", ")
-}
-
 func isFile(path string) bool {
 	s, err := os.Stat(path)
 	if err != nil {
@@ -435,66 +100,7 @@
 	return !s.IsDir()
 }
 
-// compact replaces consecutive runs of equal elements with a single copy.
-// This is like the uniq command found on Unix.
-// compact modifies the contents of the slice s; it does not create a new slice.
-//
-// Modified (generics removed) from exp/slices/slices.go.
-func compact(s []string) []string {
-	if len(s) == 0 {
-		return s
-	}
-	i := 1
-	last := s[0]
-	for _, v := range s[1:] {
-		if v != last {
-			s[i] = v
-			i++
-			last = v
-		}
-	}
-	return s[:i]
-}
-
-func goVersion() string {
-	if v := os.Getenv("GOVERSION"); v != "" {
-		// Unlikely to happen in practice, mostly used for testing.
-		return v
-	}
-	out, err := exec.Command("go", "env", "GOVERSION").Output()
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "failed to determine go version; skipping stdlib scanning: %v\n", err)
-		return ""
-	}
-	return string(bytes.TrimSpace(out))
-}
-
-func packageVersionString(packagePath, version string) string {
-	v := "v" + version
-	if importPathInStdlib(packagePath) {
-		v = semverToGoTag(v)
-	}
-	return fmt.Sprintf("%s@%s", packagePath, v)
-}
-
 func die(format string, args ...interface{}) {
 	fmt.Fprintf(os.Stderr, format+"\n", args...)
 	os.Exit(1)
 }
-
-// indent returns the output of prefixing n spaces to s at every line break,
-// except for empty lines. See TestIndent for examples.
-func indent(s string, n int) string {
-	b := []byte(s)
-	var result []byte
-	shouldAppend := true
-	prefix := strings.Repeat(" ", n)
-	for _, c := range b {
-		if shouldAppend && c != '\n' {
-			result = append(result, prefix...)
-		}
-		result = append(result, c)
-		shouldAppend = c == '\n'
-	}
-	return string(result)
-}