cmd,internal/govulncheck: change Run to return error
Run is updated to return an error, instead of terminating inside the
function on error.
Change-Id: I66ea88a146a95f79090891a01fbca090569ead6e
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/437781
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Julie Qiu <julieqiu@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
Run-TryBot: Julie Qiu <julieqiu@google.com>
diff --git a/cmd/govulncheck/main.go b/cmd/govulncheck/main.go
index 3c30502..c2aab15 100644
--- a/cmd/govulncheck/main.go
+++ b/cmd/govulncheck/main.go
@@ -5,6 +5,7 @@
package main
import (
+ "errors"
"flag"
"fmt"
"os"
@@ -73,7 +74,7 @@
buildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tagsFlag, ","))}
}
- govulncheck.Run(govulncheck.Config{
+ err := govulncheck.Run(govulncheck.Config{
AnalysisType: mode,
OutputType: outputType,
Patterns: patterns,
@@ -83,6 +84,19 @@
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 {
+ die(fmt.Sprintf("govulncheck: %v", err))
+ }
}
func validateFlags(mode string) {
diff --git a/cmd/govulncheck/message.go b/cmd/govulncheck/message.go
new file mode 100644
index 0000000..61ff8af
--- /dev/null
+++ b/cmd/govulncheck/message.go
@@ -0,0 +1,41 @@
+// 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 (
+ "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.
+
+See https://go.dev/doc/modules/managing-dependencies for more information.`
+
+ noGoSumErrorMessage = `Your module is missing a go.sum file. Try running go mod tidy.
+
+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.`
+)
+
+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/exp/govulncheck/govulncheck.go b/exp/govulncheck/govulncheck.go
index a74a380..d9512ae 100644
--- a/exp/govulncheck/govulncheck.go
+++ b/exp/govulncheck/govulncheck.go
@@ -5,12 +5,14 @@
// Package govulncheck has experimental govulncheck API.
package govulncheck
-import "golang.org/x/vuln/internal/govulncheck"
+import (
+ "golang.org/x/vuln/internal/govulncheck"
+)
// Config is the configuration for Main.
type Config = govulncheck.Config
// Run is the main function for the govulncheck command line tool.
-func Run(cfg Config) {
- govulncheck.Run(cfg)
+func Run(cfg Config) error {
+ return govulncheck.Run(cfg)
}
diff --git a/internal/govulncheck/errors.go b/internal/govulncheck/errors.go
index 5f993a8..ab63872 100644
--- a/internal/govulncheck/errors.go
+++ b/internal/govulncheck/errors.go
@@ -10,17 +10,37 @@
"strings"
)
-const noGoModErrorMessage = `govulncheck: no go.mod file
+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
+used to build govulncheck and the Go version on PATH. Consider rebuilding
+govulncheck with the current Go version.`)
+
+ // ErrNoGoSum indicates that a go.mod file was not found in this module.
+ ErrNoGoMod = errors.New(`no go.mod file
govulncheck only works Go with modules. To make your project a module, run go mod init.
-See https://go.dev/doc/modules/managing-dependencies for more information.`
+See https://go.dev/doc/modules/managing-dependencies for more information.`)
-const noGoSumErrorMessage = `govulncheck: no go.sum file
+ // ErrNoGoSum indicates that a go.sum file was not found in this module.
+ ErrNoGoSum = errors.New(`no go.sum file
Your module is missing a go.sum file. Try running go mod tidy.
-See https://go.dev/doc/modules/managing-dependencies for more information.`
+See https://go.dev/doc/modules/managing-dependencies for more information.`)
+)
// fileExists checks if file path exists. Returns true
// if the file exists or it cannot prove that it does
@@ -36,12 +56,6 @@
return true
}
-const goVersionMismatchErrorMessage = `govulncheck: Go version mismatch
-
-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.`
-
// isGoVersionMismatchError checks if err is due to mismatch between
// the Go version used to build govulncheck and the one currently
// on PATH.
diff --git a/internal/govulncheck/run.go b/internal/govulncheck/run.go
index 6cd171d..2d98a2f 100644
--- a/internal/govulncheck/run.go
+++ b/internal/govulncheck/run.go
@@ -21,7 +21,7 @@
)
// Run is the main function for the govulncheck command line tool.
-func Run(cfg Config) {
+func Run(cfg Config) error {
dbs := []string{vulndbHost}
if db := os.Getenv(envGOVULNDB); db != "" {
dbs = strings.Split(db, ",")
@@ -30,7 +30,7 @@
HTTPCache: DefaultCache(),
})
if err != nil {
- die("govulncheck: %s", err)
+ return err
}
vcfg := &vulncheck.Config{Client: dbClient, SourceGoVersion: internal.GoVersion()}
@@ -52,12 +52,12 @@
case AnalysisTypeBinary:
f, err := os.Open(patterns[0])
if err != nil {
- die("govulncheck: %v", err)
+ return err
}
defer f.Close()
r, err = binary(ctx, f, vcfg)
if err != nil {
- die("govulncheck: %v", err)
+ return err
}
case AnalysisTypeSource:
cfg := &cfg.SourceLoadConfig
@@ -65,13 +65,15 @@
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)
- } else if isGoVersionMismatchError(err) {
- die(fmt.Sprintf("%s\n\n%v", goVersionMismatchErrorMessage, err))
+ return ErrNoGoMod
}
- die("govulncheck: %v", err)
+ if !fileExists(filepath.Join(cfg.Dir, "go.sum")) {
+ return ErrNoGoSum
+ }
+ if isGoVersionMismatchError(err) {
+ return fmt.Errorf("%v\n\n%v", ErrGoVersionMismatch, err)
+ }
+ return err
}
// Sort pkgs so that the PkgNodes returned by vulncheck.Source will be
@@ -79,48 +81,43 @@
sortPackages(pkgs)
r, err = vulncheck.Source(ctx, pkgs, vcfg)
if err != nil {
- die("govulncheck: %v", err)
+ return err
}
unaffected = filterUnaffected(r)
r.Vulns = filterCalled(r)
default:
- die("govulncheck: invalid analysis mode %q", cfg.AnalysisType)
+ return fmt.Errorf("%w: %s", ErrInvalidAnalysisType, cfg.AnalysisType)
}
switch format {
case OutputTypeJSON:
// Following golang.org/x/tools/go/analysis/singlechecker,
// return 0 exit code in -json mode.
- writeJSON(r)
- os.Exit(0)
+ return writeJSON(r)
case OutputTypeText, OutputTypeVerbose:
// set of top-level packages, used to find representative symbols
ci := GetCallInfo(r, pkgs)
writeText(r, ci, unaffected, format == OutputTypeVerbose)
case OutputTypeSummary:
ci := GetCallInfo(r, pkgs)
- writeJSON(summary(ci, unaffected))
- os.Exit(0)
+ return writeJSON(summary(ci, unaffected))
default:
- die("govulncheck: unrecognized output type %q", cfg.OutputType)
+ return fmt.Errorf("%w: %s", ErrInvalidOutputType, cfg.OutputType)
}
-
- // 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
+ return ErrContainsVulnerabilties
}
- os.Exit(exitCode)
+ return nil
}
-func writeJSON(r any) {
+func writeJSON(r any) error {
b, err := json.MarshalIndent(r, "", "\t")
if err != nil {
- die("govulncheck: %s", err)
+ return err
}
os.Stdout.Write(b)
fmt.Println()
+ return nil
}
const (
@@ -316,11 +313,6 @@
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 {