cmd/govulncheck: inline LegacyRun into govulncheck main
This is the second in the series of CLs that attempts to refactor
govulncheck. The changes in this CL are:
- inline the logic of LegacyRun into cmd/govulncheck/main.go
- delete LegacyConfig and related data structures
- move all the accompanying logic to cmd/govulncheck
- expose db caching logic
This effectively results in the following structure.
internal/govulncheck contains the core logic for Source and Binary
analysis (including db caching). cmd/govulncheck contains the logic for
parsing the flags and printing the results.
The follow-up refactoring CLs will mainly focus on renaming of things
and addressing TODOs.
For golang/go#56042
Change-Id: Ib1fc26951420ae6d04e48205765a1019d117946f
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/447855
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/govulncheck/errors.go b/cmd/govulncheck/errors.go
similarity index 85%
rename from internal/govulncheck/errors.go
rename to cmd/govulncheck/errors.go
index 100ac75..04fe666 100644
--- a/internal/govulncheck/errors.go
+++ b/cmd/govulncheck/errors.go
@@ -2,27 +2,19 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package govulncheck
+package main
import (
"errors"
+ "fmt"
"os"
"strings"
+ "golang.org/x/tools/go/packages"
"golang.org/x/vuln/vulncheck"
)
var (
- // ErrContainsVulnerabilties is used to indicate that vulerabilities were
- // found in the output of Run.
- ErrContainsVulnerabilties = errors.New("module contains vulnerabilities")
-
- // ErrInvalidAnalysisType indicates that an unsupported AnalysisType was passed to Config.
- ErrInvalidAnalysisType = errors.New("invalid analysis type")
-
- // ErrInvalidOutputType indicates that an unsupported OutputType was passed to Config.
- ErrInvalidOutputType = errors.New("invalid output type")
-
// ErrErrGoVersionMismatch is used to indicate that there is a mismatch between
// the Go version used to build govulncheck and the one currently on PATH.
ErrGoVersionMismatch = errors.New(`Loading packages failed, possibly due to a mismatch between the Go version
@@ -53,6 +45,20 @@
See https://go.dev/doc/modules/managing-dependencies for more information.`)
)
+// A PackageError contains errors from loading a set of packages.
+type PackageError struct {
+ Errors []packages.Error
+}
+
+func (e *PackageError) Error() string {
+ var b strings.Builder
+ fmt.Fprintln(&b, "Packages contain errors:")
+ for _, e := range e.Errors {
+ fmt.Fprintln(&b, e)
+ }
+ return b.String()
+}
+
// fileExists checks if file path exists. Returns true
// if the file exists or it cannot prove that it does
// not exist. Otherwise, returns false.
diff --git a/internal/govulncheck/formatting.go b/cmd/govulncheck/formatting.go
similarity index 98%
rename from internal/govulncheck/formatting.go
rename to cmd/govulncheck/formatting.go
index c2254da..10814a4 100644
--- a/internal/govulncheck/formatting.go
+++ b/cmd/govulncheck/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 govulncheck
+package main
import (
"bytes"
diff --git a/internal/govulncheck/formatting_test.go b/cmd/govulncheck/formatting_test.go
similarity index 98%
rename from internal/govulncheck/formatting_test.go
rename to cmd/govulncheck/formatting_test.go
index d8de82b..48fdd9e 100644
--- a/internal/govulncheck/formatting_test.go
+++ b/cmd/govulncheck/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 govulncheck
+package main
import (
"bytes"
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 9cdd971..6f06b25 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -6,7 +6,6 @@
import (
"context"
- "errors"
"flag"
"fmt"
"os"
@@ -15,7 +14,10 @@
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/packages"
- "golang.org/x/vuln/internal/govulncheck"
+ "golang.org/x/vuln/client"
+ "golang.org/x/vuln/exp/govulncheck"
+ gvc "golang.org/x/vuln/internal/govulncheck"
+ "golang.org/x/vuln/vulncheck"
)
var (
@@ -32,6 +34,11 @@
flag.Var(&tagsFlag, "tags", "comma-separated `list` of build tags")
}
+const (
+ envGOVULNDB = "GOVULNDB"
+ vulndbHost = "https://vuln.go.dev"
+)
+
func main() {
flag.Usage = func() {
fmt.Fprint(os.Stderr, `usage:
@@ -53,53 +60,103 @@
patterns := flag.Args()
- mode := govulncheck.AnalysisTypeSource
+ sourceAnalysis := true
if len(patterns) == 1 && isFile(patterns[0]) {
- mode = govulncheck.AnalysisTypeBinary
+ sourceAnalysis = false
}
- validateFlags(mode)
+ validateFlags(sourceAnalysis)
- outputType := govulncheck.OutputTypeText
- if *jsonFlag {
- outputType = govulncheck.OutputTypeJSON
- } else if *verboseFlag {
- outputType = govulncheck.OutputTypeVerbose
- }
-
- var buildFlags []string
- if tagsFlag != nil {
- buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
- }
-
- ctx := context.Background()
- _, err := govulncheck.LegacyRun(ctx, govulncheck.LegacyConfig{
- AnalysisType: mode,
- OutputType: outputType,
- Patterns: patterns,
- SourceLoadConfig: &packages.Config{
- Dir: filepath.FromSlash(dirFlag),
- Tests: *testFlag,
- BuildFlags: buildFlags,
- },
- })
- if outputType == govulncheck.OutputTypeJSON {
- // The current behavior is to not print any errors.
- return
- }
- if errors.Is(err, govulncheck.ErrContainsVulnerabilties) {
- // This follows the style from
- // golang.org/x/tools/go/analysis/singlechecker,
- // which fails with 3 if there are findings (in this case, vulns).
- os.Exit(3)
- }
- if err != nil {
+ if err := doGovulncheck(patterns, sourceAnalysis); err != nil {
die(fmt.Sprintf("govulncheck: %v", err))
}
}
-func validateFlags(mode string) {
- switch mode {
- case govulncheck.AnalysisTypeBinary:
+// doGovulncheck performs main govulncheck functionality and exits the
+// program upon success with an appropriate exit status. Otherwise,
+// returns an error.
+func doGovulncheck(patterns []string, sourceAnalysis bool) error {
+ ctx := context.Background()
+ dir := filepath.FromSlash(dirFlag)
+
+ dbs := []string{vulndbHost}
+ if db := os.Getenv(envGOVULNDB); db != "" {
+ dbs = strings.Split(db, ",")
+ }
+ dbClient, err := client.NewClient(dbs, client.Options{
+ HTTPCache: govulncheck.DefaultCache(),
+ })
+ if err != nil {
+ return err
+ }
+
+ if !*jsonFlag {
+ // Print intro message when in text or verbose mode
+ fmt.Println(introMessage)
+ }
+
+ // config GoVersion is "", which means use current
+ // Go version at path.
+ cfg := &govulncheck.Config{Client: dbClient}
+ var res *govulncheck.Result
+ if sourceAnalysis {
+ pkgs, err := loadPackages(patterns, dir)
+ if err != nil {
+ // Try to provide a meaningful and actionable error message.
+ if !fileExists(filepath.Join(dir, "go.mod")) {
+ return ErrNoGoMod
+ }
+ if !fileExists(filepath.Join(dir, "go.sum")) {
+ return ErrNoGoSum
+ }
+ if isGoVersionMismatchError(err) {
+ return fmt.Errorf("%v\n\n%v", ErrGoVersionMismatch, err)
+ }
+ return err
+ }
+ res, err = govulncheck.Source(ctx, cfg, pkgs)
+ } else {
+ f, err := os.Open(patterns[0])
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ res, err = gvc.Binary(ctx, cfg, f)
+ }
+ if err != nil {
+ return err
+ }
+
+ if *jsonFlag {
+ // Following golang.org/x/tools/go/analysis/singlechecker,
+ // return 0 exit code in -json mode.
+ if err := printJSON(res); err != nil {
+ return err
+ }
+ os.Exit(0)
+ }
+
+ printText(res, *verboseFlag, sourceAnalysis)
+ // Return exit status -3 if some vulnerabilities are actually
+ // called in source mode or just present in binary mode.
+ //
+ // This follows the style from
+ // golang.org/x/tools/go/analysis/singlechecker,
+ // which fails with 3 if there are some findings.
+ if sourceAnalysis {
+ for _, v := range res.Vulns {
+ if v.IsCalled() {
+ os.Exit(3)
+ }
+ }
+ } else if len(res.Vulns) > 0 {
+ os.Exit(3)
+ }
+ os.Exit(0)
+ return nil
+}
+
+func validateFlags(source bool) {
+ if !source {
if *testFlag {
die("govulncheck: the -test flag is invalid for binaries")
}
@@ -121,3 +178,34 @@
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
+
+// loadPackages loads the packages matching patterns at dir using build tags
+// provided by tagsFlag. Uses load mode needed for vulncheck analysis. If the
+// packages contain errors, a PackageError is returned containing a list of
+// the errors, along with the packages themselves.
+func loadPackages(patterns []string, dir string) ([]*vulncheck.Package, error) {
+ var buildFlags []string
+ if tagsFlag != nil {
+ buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
+ }
+
+ cfg := &packages.Config{Dir: dir, Tests: *testFlag}
+ cfg.Mode |= packages.NeedName | packages.NeedImports | packages.NeedTypes |
+ packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
+ packages.NeedModule
+ cfg.BuildFlags = buildFlags
+
+ pkgs, err := packages.Load(cfg, patterns...)
+ vpkgs := vulncheck.Convert(pkgs)
+ if err != nil {
+ return nil, err
+ }
+ var perrs []packages.Error
+ packages.Visit(pkgs, nil, func(p *packages.Package) {
+ perrs = append(perrs, p.Errors...)
+ })
+ if len(perrs) > 0 {
+ err = &PackageError{perrs}
+ }
+ return vpkgs, err
+}
diff --git a/cmd/govulncheck/message.go b/cmd/govulncheck/message.go
index 61ff8af..07e8977 100644
--- a/cmd/govulncheck/message.go
+++ b/cmd/govulncheck/message.go
@@ -4,38 +4,15 @@
package main
-import (
- "fmt"
-
- "golang.org/x/vuln/internal/govulncheck"
-)
-
const (
- noGoModErrorMessage = `govulncheck only works Go with modules. To make your project a module, run go mod init.
+ introMessage = `govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
-See https://go.dev/doc/modules/managing-dependencies for more information.`
+Scanning for dependencies with known vulnerabilities...`
- noGoSumErrorMessage = `Your module is missing a go.sum file. Try running go mod tidy.
+ informationalMessage = `=== Informational ===
-See https://go.dev/doc/modules/managing-dependencies for more information.`
-
- goVersionMismatchErrorMessage = `Loading packages failed, possibly due to a mismatch between the Go version
-used to build govulncheck and the Go version on PATH. Consider rebuilding
-govulncheck with the current Go version.`
+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.`
)
-
-var errToMessage = map[error]string{
- govulncheck.ErrNoGoMod: noGoModErrorMessage,
- govulncheck.ErrNoGoSum: noGoSumErrorMessage,
- govulncheck.ErrGoVersionMismatch: goVersionMismatchErrorMessage,
- govulncheck.ErrInvalidAnalysisType: "",
- govulncheck.ErrInvalidOutputType: "",
-}
-
-func messageForError(err error) (out string) {
- msg, ok := errToMessage[err]
- if !ok {
- return ""
- }
- return fmt.Sprintf("govulncheck: %v\n\n%s", err, msg)
-}
diff --git a/internal/govulncheck/print.go b/cmd/govulncheck/print.go
similarity index 91%
rename from internal/govulncheck/print.go
rename to cmd/govulncheck/print.go
index f503661..d3e4be5 100644
--- a/internal/govulncheck/print.go
+++ b/cmd/govulncheck/print.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 govulncheck
+package main
import (
"encoding/json"
@@ -12,10 +12,12 @@
"strings"
"golang.org/x/exp/maps"
+ "golang.org/x/vuln/exp/govulncheck"
+ "golang.org/x/vuln/internal"
"golang.org/x/vuln/osv"
)
-func printJSON(r *Result) error {
+func printJSON(r *govulncheck.Result) error {
b, err := json.MarshalIndent(r, "", "\t")
if err != nil {
return err
@@ -30,10 +32,10 @@
lineLength = 55
)
-func printText(r *Result, verbose, source bool) {
+func printText(r *govulncheck.Result, verbose, source bool) {
// unaffected are (imported) OSVs none of
// which vulnerabilities are called.
- var unaffected []*Vuln
+ var unaffected []*govulncheck.Vuln
uniqueVulns := 0
for _, v := range r.Vulns {
if !source || v.IsCalled() {
@@ -117,7 +119,7 @@
`, idx, id, indent(details, 2), callstack, found, fixed, platforms, id)
}
-func defaultCallStacks(css []CallStack) string {
+func defaultCallStacks(css []govulncheck.CallStack) string {
var summaries []string
for _, cs := range css {
summaries = append(summaries, cs.Summary)
@@ -137,15 +139,15 @@
return b.String()
}
-func verboseCallStacks(css []CallStack) string {
+func verboseCallStacks(css []govulncheck.CallStack) string {
// Display one full call stack for each vuln.
i := 1
var b strings.Builder
for _, cs := range css {
b.WriteString(fmt.Sprintf("#%d: for function %s\n", i, cs.Symbol))
for _, e := range cs.Frames {
- b.WriteString(fmt.Sprintf(" %s\n", funcName(e)))
- if pos := AbsRelShorter(funcPos(e)); pos != "" {
+ b.WriteString(fmt.Sprintf(" %s\n", e.Name()))
+ if pos := internal.AbsRelShorter(e.Pos()); pos != "" {
b.WriteString(fmt.Sprintf(" %s\n", pos))
}
}
diff --git a/cmd/govulncheck/print_test.go b/cmd/govulncheck/print_test.go
new file mode 100644
index 0000000..2f8b089
--- /dev/null
+++ b/cmd/govulncheck/print_test.go
@@ -0,0 +1,88 @@
+// 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 main
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/vuln/osv"
+)
+
+func TestPlatforms(t *testing.T) {
+ for _, test := range []struct {
+ entry *osv.Entry
+ want string
+ }{
+ {
+ entry: &osv.Entry{ID: "All"},
+ want: "",
+ },
+ {
+ entry: &osv.Entry{
+ ID: "one-import",
+ Affected: []osv.Affected{{
+ Package: osv.Package{Name: "golang.org/vmod"},
+ Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Imports: []osv.EcosystemSpecificImport{{
+ GOOS: []string{"windows", "linux"},
+ GOARCH: []string{"amd64", "wasm"},
+ }},
+ },
+ }},
+ },
+ want: "linux/amd64, linux/wasm, windows/amd64, windows/wasm",
+ },
+ {
+ entry: &osv.Entry{
+ ID: "two-imports",
+ Affected: []osv.Affected{{
+ Package: osv.Package{Name: "golang.org/vmod"},
+ Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
+ EcosystemSpecific: osv.EcosystemSpecific{
+ Imports: []osv.EcosystemSpecificImport{
+ {
+ GOOS: []string{"windows"},
+ GOARCH: []string{"amd64"},
+ },
+ {
+ GOOS: []string{"linux"},
+ GOARCH: []string{"amd64"},
+ },
+ },
+ },
+ }},
+ },
+ want: "linux/amd64, windows/amd64",
+ },
+ } {
+ t.Run(test.entry.ID, func(t *testing.T) {
+ got := platforms(test.entry)
+ if got != test.want {
+ t.Errorf("got %q, want %q", got, test.want)
+ }
+ })
+ }
+}
+
+func TestIndent(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ s string
+ n int
+ want string
+ }{
+ {"short", "hello", 2, " hello"},
+ {"multi", "mulit\nline\nstring", 1, " mulit\n line\n string"},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ got := indent(test.s, test.n)
+ if diff := cmp.Diff(test.want, got); diff != "" {
+ t.Fatalf("mismatch (-want, +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/exp/govulncheck/govulncheck.go b/exp/govulncheck/govulncheck.go
index ee36839..d09b0d0 100644
--- a/exp/govulncheck/govulncheck.go
+++ b/exp/govulncheck/govulncheck.go
@@ -7,8 +7,13 @@
import "golang.org/x/vuln/internal/govulncheck"
-// Source reports vulnerabilities that affect the analyzed packages.
-var Source = govulncheck.Source
+var (
+ // Source reports vulnerabilities that affect the analyzed packages.
+ Source = govulncheck.Source
+
+ // DefaultCache constructs cache for a vulnerability database client.
+ DefaultCache = govulncheck.DefaultCache
+)
type (
// Config is the configuration for Main.
diff --git a/internal/govulncheck/filepath.go b/internal/filepath.go
similarity index 97%
rename from internal/govulncheck/filepath.go
rename to internal/filepath.go
index 7f447fd..870af76 100644
--- a/internal/govulncheck/filepath.go
+++ b/internal/filepath.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 govulncheck
+package internal
import (
"path/filepath"
diff --git a/internal/govulncheck/filepath_test.go b/internal/filepath_test.go
similarity index 97%
rename from internal/govulncheck/filepath_test.go
rename to internal/filepath_test.go
index 3f59ab4..1f64d77 100644
--- a/internal/govulncheck/filepath_test.go
+++ b/internal/filepath_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 govulncheck
+package internal
import (
"os"
diff --git a/internal/govulncheck/config.go b/internal/govulncheck/config.go
deleted file mode 100644
index 33ac937..0000000
--- a/internal/govulncheck/config.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// 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 govulncheck
-
-import "golang.org/x/tools/go/packages"
-
-const (
- // AnalysisTypeBinary is used for binary analysis with vulncheck.Binary.
- AnalysisTypeBinary = "binary"
-
- // AnalysisTypeSource is used for source code analysis with vulncheck.Source.
- AnalysisTypeSource = "source"
-)
-
-const (
- // OutputTypeText is the default output type for `govulncheck`.
- OutputTypeText = "text"
-
- // OutputTypeVerbose is the output type for `govulncheck -v`.
- OutputTypeVerbose = "verbose"
-
- // OutputTypeJSON is the output type for `govulncheck -json`, which will print
- // the JSON-encoded vulncheck.Result.
- OutputTypeJSON = "json"
-)
-
-const (
- envGOVULNDB = "GOVULNDB"
- vulndbHost = "https://vuln.go.dev"
-)
-
-// LegacyConfig is the configuration for Main.
-type LegacyConfig struct {
- // AnalysisType specifies the vulncheck analysis type.
- AnalysisType string
-
- // OutputType specifies the output format type.
- OutputType 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
-
- // GoVersion specifies the go version used when analyzing source code.
- // The default is the version of the go command found from the PATH (Path).
- GoVersion string
-}
diff --git a/internal/govulncheck/filter.go b/internal/govulncheck/filter.go
deleted file mode 100644
index b37d298..0000000
--- a/internal/govulncheck/filter.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// 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 govulncheck
-
-import (
- "sort"
-
- "golang.org/x/vuln/vulncheck"
-)
-
-// 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
- })
- }
-}
diff --git a/internal/govulncheck/legacy_run.go b/internal/govulncheck/legacy_run.go
deleted file mode 100644
index b69bb68..0000000
--- a/internal/govulncheck/legacy_run.go
+++ /dev/null
@@ -1,137 +0,0 @@
-// 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 govulncheck
-
-import (
- "context"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "golang.org/x/tools/go/packages"
- "golang.org/x/vuln/client"
- "golang.org/x/vuln/vulncheck"
-)
-
-// LegacyRun is the main function for the govulncheck command line tool.
-//
-// TODO: inline into cmd/govulncheck. This will effectively remove the
-// need for having additional (Legacy)Config.
-func LegacyRun(ctx context.Context, lcfg LegacyConfig) (*Result, error) {
- dbs := []string{vulndbHost}
- if db := os.Getenv(envGOVULNDB); db != "" {
- dbs = strings.Split(db, ",")
- }
- dbClient, err := client.NewClient(dbs, client.Options{
- HTTPCache: DefaultCache(),
- })
- if err != nil {
- return nil, err
- }
-
- format := lcfg.OutputType
- if format == OutputTypeText || format == OutputTypeVerbose {
- fmt.Println(introMessage)
- }
-
- cfg := &Config{Client: dbClient, GoVersion: lcfg.GoVersion}
- var res *Result
- switch lcfg.AnalysisType {
- case AnalysisTypeBinary:
- f, err := os.Open(lcfg.Patterns[0])
- if err != nil {
- return nil, err
- }
- defer f.Close()
- res, err = Binary(ctx, cfg, f)
- case AnalysisTypeSource:
- pkgs, err := loadPackages(lcfg)
- if err != nil {
- // Try to provide a meaningful and actionable error message.
- if !fileExists(filepath.Join(lcfg.SourceLoadConfig.Dir, "go.mod")) {
- return nil, ErrNoGoMod
- }
- if !fileExists(filepath.Join(lcfg.SourceLoadConfig.Dir, "go.sum")) {
- return nil, ErrNoGoSum
- }
- if isGoVersionMismatchError(err) {
- return nil, fmt.Errorf("%v\n\n%v", ErrGoVersionMismatch, err)
- }
- return nil, err
- }
- res, err = Source(ctx, cfg, pkgs)
- default:
- return nil, fmt.Errorf("%w: %s", ErrInvalidAnalysisType, lcfg.AnalysisType)
- }
- if err != nil {
- return nil, err
- }
-
- switch lcfg.OutputType {
- case OutputTypeJSON:
- // Following golang.org/x/tools/go/analysis/singlechecker,
- // return 0 exit code in -json mode.
- if err := printJSON(res); err != nil {
- return nil, err
- }
- return res, nil
- case OutputTypeText, OutputTypeVerbose:
- source := lcfg.AnalysisType == AnalysisTypeSource
- printText(res, lcfg.OutputType == OutputTypeVerbose, source)
- // Return error if some vulnerabilities are actually called.
- if source {
- for _, v := range res.Vulns {
- if v.IsCalled() {
- return nil, ErrContainsVulnerabilties
- }
- }
- } else if len(res.Vulns) > 0 {
- return nil, ErrContainsVulnerabilties
- }
- return res, nil
- default:
- return nil, fmt.Errorf("%w: %s", ErrInvalidOutputType, lcfg.OutputType)
- }
-}
-
-// A PackageError contains errors from loading a set of packages.
-type PackageError struct {
- Errors []packages.Error
-}
-
-func (e *PackageError) Error() string {
- var b strings.Builder
- fmt.Fprintln(&b, "Packages contain errors:")
- for _, e := range e.Errors {
- fmt.Fprintln(&b, e)
- }
- return b.String()
-}
-
-// loadPackages loads the packages matching patterns using cfg, after setting
-// the cfg mode flags that vulncheck needs for analysis.
-// If the packages contain errors, a PackageError is returned containing a list of the errors,
-// along with the packages themselves.
-func loadPackages(cfg LegacyConfig) ([]*vulncheck.Package, error) {
- patterns := cfg.Patterns
- cfg.SourceLoadConfig.Mode |= packages.NeedName | packages.NeedImports | packages.NeedTypes |
- packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps |
- packages.NeedModule
-
- pkgs, err := packages.Load(cfg.SourceLoadConfig, patterns...)
- vpkgs := vulncheck.Convert(pkgs)
- if err != nil {
- return nil, err
- }
- var perrs []packages.Error
- packages.Visit(pkgs, nil, func(p *packages.Package) {
- perrs = append(perrs, p.Errors...)
- })
- if len(perrs) > 0 {
- err = &PackageError{perrs}
- }
- return vpkgs, err
-}
diff --git a/internal/govulncheck/message.go b/internal/govulncheck/message.go
deleted file mode 100644
index 737d182..0000000
--- a/internal/govulncheck/message.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 govulncheck
-
-const (
- introMessage = `govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
-
-Scanning for dependencies with known vulnerabilities...`
-
- informationalMessage = `=== 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.`
-)
diff --git a/internal/govulncheck/result.go b/internal/govulncheck/result.go
index 9e5a991..b0a3128 100644
--- a/internal/govulncheck/result.go
+++ b/internal/govulncheck/result.go
@@ -6,7 +6,9 @@
package govulncheck
import (
+ "fmt"
"go/token"
+ "strings"
"golang.org/x/tools/go/packages"
"golang.org/x/vuln/client"
@@ -30,11 +32,6 @@
//
// By default, GoVersion is the go command version found from the PATH.
GoVersion string
-
- // Verbosity controls the stdout and stderr output when running Source.
- //
- // TODO(https://go.dev/issue/56042): make this an enum.
- Verbosity string
}
// Result is the result of executing Source or Binary.
@@ -158,3 +155,24 @@
// A Position is valid if the line number is > 0.
Position token.Position
}
+
+// Name returns the full qualified function name from sf,
+// adjusted to remove pointer annotations.
+func (sf *StackFrame) Name() string {
+ var n string
+ if sf.RecvType == "" {
+ n = fmt.Sprintf("%s.%s", sf.PkgPath, sf.FuncName)
+ } else {
+ n = fmt.Sprintf("%s.%s", sf.RecvType, sf.FuncName)
+ }
+ return strings.TrimPrefix(n, "*")
+}
+
+// Pos returns the position of the call in sf as string.
+// If position is not available, return "".
+func (sf *StackFrame) Pos() string {
+ if sf.Position.IsValid() {
+ return sf.Position.String()
+ }
+ return ""
+}
diff --git a/internal/govulncheck/run_test.go b/internal/govulncheck/run_test.go
index 4a6481d..bd93c57 100644
--- a/internal/govulncheck/run_test.go
+++ b/internal/govulncheck/run_test.go
@@ -108,82 +108,6 @@
}
}
-func TestPlatforms(t *testing.T) {
- for _, test := range []struct {
- entry *osv.Entry
- want string
- }{
- {
- entry: &osv.Entry{ID: "All"},
- want: "",
- },
- {
- entry: &osv.Entry{
- ID: "one-import",
- Affected: []osv.Affected{{
- Package: osv.Package{Name: "golang.org/vmod"},
- Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
- EcosystemSpecific: osv.EcosystemSpecific{
- Imports: []osv.EcosystemSpecificImport{{
- GOOS: []string{"windows", "linux"},
- GOARCH: []string{"amd64", "wasm"},
- }},
- },
- }},
- },
- want: "linux/amd64, linux/wasm, windows/amd64, windows/wasm",
- },
- {
- entry: &osv.Entry{
- ID: "two-imports",
- Affected: []osv.Affected{{
- Package: osv.Package{Name: "golang.org/vmod"},
- Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.2.0"}}}},
- EcosystemSpecific: osv.EcosystemSpecific{
- Imports: []osv.EcosystemSpecificImport{
- {
- GOOS: []string{"windows"},
- GOARCH: []string{"amd64"},
- },
- {
- GOOS: []string{"linux"},
- GOARCH: []string{"amd64"},
- },
- },
- },
- }},
- },
- want: "linux/amd64, windows/amd64",
- },
- } {
- t.Run(test.entry.ID, func(t *testing.T) {
- got := platforms(test.entry)
- if got != test.want {
- t.Errorf("got %q, want %q", got, test.want)
- }
- })
- }
-}
-
-func TestIndent(t *testing.T) {
- for _, test := range []struct {
- name string
- s string
- n int
- want string
- }{
- {"short", "hello", 2, " hello"},
- {"multi", "mulit\nline\nstring", 1, " mulit\n line\n string"},
- } {
- t.Run(test.name, func(t *testing.T) {
- got := indent(test.s, test.n)
- if diff := cmp.Diff(test.want, got); diff != "" {
- t.Fatalf("mismatch (-want, +got):\n%s", diff)
- }
- })
- }
-}
-
func TestUniqueCallStack(t *testing.T) {
a := &vulncheck.FuncNode{Name: "A"}
b := &vulncheck.FuncNode{Name: "B"}
diff --git a/internal/govulncheck/util.go b/internal/govulncheck/util.go
index 3c750a3..f713444 100644
--- a/internal/govulncheck/util.go
+++ b/internal/govulncheck/util.go
@@ -82,7 +82,6 @@
return topPkgs[e.PkgPath]
})
if iTop < 0 {
- print("1\n")
return ""
}
// Find the highest function in the vulnerable package that is below iTop.
@@ -90,21 +89,20 @@
return e.PkgPath == vulnPkg
})
if iVuln < 0 {
- print("2\n")
return ""
}
iVuln += iTop + 1 // adjust for slice in call to highest.
- topName := funcName(cs.Frames[iTop])
- topPos := AbsRelShorter(funcPos(cs.Frames[iTop]))
+ topName := cs.Frames[iTop].Name()
+ topPos := internal.AbsRelShorter(cs.Frames[iTop].Pos())
if topPos != "" {
topPos += ": "
}
- vulnName := funcName(cs.Frames[iVuln])
+ vulnName := cs.Frames[iVuln].Name()
if iVuln == iTop+1 {
return fmt.Sprintf("%s%s calls %s", topPos, topName, vulnName)
}
return fmt.Sprintf("%s%s calls %s, which eventually calls %s",
- topPos, topName, funcName(cs.Frames[iTop+1]), vulnName)
+ topPos, topName, cs.Frames[iTop+1].Name(), vulnName)
}
// highest returns the highest (one with the smallest index) entry in the call
@@ -142,24 +140,3 @@
}
return s
}
-
-// funcName returns the full qualified function name from fn,
-// adjusted to remove pointer annotations.
-func funcName(sf *StackFrame) string {
- var n string
- if sf.RecvType == "" {
- n = fmt.Sprintf("%s.%s", sf.PkgPath, sf.FuncName)
- } else {
- n = fmt.Sprintf("%s.%s", sf.RecvType, sf.FuncName)
- }
- return strings.TrimPrefix(n, "*")
-}
-
-// funcPos returns the position of the call in sf as string.
-// If position is not available, return "".
-func funcPos(sf *StackFrame) string {
- if sf.Position.IsValid() {
- return sf.Position.String()
- }
- return ""
-}