whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,103 @@
// 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.
//go:build go1.18
// +build go1.18
package scan
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"runtime/debug"
"golang.org/x/vuln/internal/buildinfo"
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/derrors"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/vulncheck"
)
// runBinary detects presence of vulnerable symbols in an executable or its minimal blob representation.
func runBinary(ctx context.Context, handler govulncheck.Handler, cfg *config, client *client.Client) (err error) {
defer derrors.Wrap(&err, "govulncheck")
bin, err := createBin(cfg.patterns[0])
if err != nil {
return err
}
p := &govulncheck.Progress{Message: binaryProgressMessage}
if err := handler.Progress(p); err != nil {
return err
}
return vulncheck.Binary(ctx, handler, bin, &cfg.Config, client)
}
func createBin(path string) (*vulncheck.Bin, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
// First check if the path points to a Go binary. Otherwise, blob
// parsing might json decode a Go binary which takes time.
//
// TODO(#64716): use fingerprinting to make this precise, clean, and fast.
mods, packageSymbols, bi, err := buildinfo.ExtractPackagesAndSymbols(f)
if err == nil {
return &vulncheck.Bin{
Modules: mods,
PkgSymbols: packageSymbols,
GoVersion: bi.GoVersion,
GOOS: findSetting("GOOS", bi),
GOARCH: findSetting("GOARCH", bi),
}, nil
}
// Otherwise, see if the path points to a valid blob.
bin := parseBlob(f)
if bin != nil {
return bin, nil
}
return nil, errors.New("unrecognized binary format")
}
// parseBlob extracts vulncheck.Bin from a valid blob. If it
// cannot recognize a valid blob, returns nil.
func parseBlob(from io.Reader) *vulncheck.Bin {
dec := json.NewDecoder(from)
var h header
if err := dec.Decode(&h); err != nil {
return nil // no header
} else if h.Name != extractModeID || h.Version != extractModeVersion {
return nil // invalid header
}
var b vulncheck.Bin
if err := dec.Decode(&b); err != nil {
return nil // no body
}
if dec.More() {
return nil // we want just header and body, nothing else
}
return &b
}
// findSetting returns value of setting from bi if present.
// Otherwise, returns "".
func findSetting(setting string, bi *debug.BuildInfo) string {
for _, s := range bi.Settings {
if s.Key == setting {
return s.Value
}
}
return ""
}
@@ -0,0 +1,97 @@
// 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 scan
const (
// These are all the constants for the terminal escape strings
colorEscape = "\033["
colorEnd = "m"
colorReset = colorEscape + "0" + colorEnd
colorBold = colorEscape + "1" + colorEnd
colorFaint = colorEscape + "2" + colorEnd
colorUnderline = colorEscape + "4" + colorEnd
colorBlink = colorEscape + "5" + colorEnd
fgBlack = colorEscape + "30" + colorEnd
fgRed = colorEscape + "31" + colorEnd
fgGreen = colorEscape + "32" + colorEnd
fgYellow = colorEscape + "33" + colorEnd
fgBlue = colorEscape + "34" + colorEnd
fgMagenta = colorEscape + "35" + colorEnd
fgCyan = colorEscape + "36" + colorEnd
fgWhite = colorEscape + "37" + colorEnd
bgBlack = colorEscape + "40" + colorEnd
bgRed = colorEscape + "41" + colorEnd
bgGreen = colorEscape + "42" + colorEnd
bgYellow = colorEscape + "43" + colorEnd
bgBlue = colorEscape + "44" + colorEnd
bgMagenta = colorEscape + "45" + colorEnd
bgCyan = colorEscape + "46" + colorEnd
bgWhite = colorEscape + "47" + colorEnd
fgBlackHi = colorEscape + "90" + colorEnd
fgRedHi = colorEscape + "91" + colorEnd
fgGreenHi = colorEscape + "92" + colorEnd
fgYellowHi = colorEscape + "93" + colorEnd
fgBlueHi = colorEscape + "94" + colorEnd
fgMagentaHi = colorEscape + "95" + colorEnd
fgCyanHi = colorEscape + "96" + colorEnd
fgWhiteHi = colorEscape + "97" + colorEnd
bgBlackHi = colorEscape + "100" + colorEnd
bgRedHi = colorEscape + "101" + colorEnd
bgGreenHi = colorEscape + "102" + colorEnd
bgYellowHi = colorEscape + "103" + colorEnd
bgBlueHi = colorEscape + "104" + colorEnd
bgMagentaHi = colorEscape + "105" + colorEnd
bgCyanHi = colorEscape + "106" + colorEnd
bgWhiteHi = colorEscape + "107" + colorEnd
)
const (
_ = colorReset
_ = colorBold
_ = colorFaint
_ = colorUnderline
_ = colorBlink
_ = fgBlack
_ = fgRed
_ = fgGreen
_ = fgYellow
_ = fgBlue
_ = fgMagenta
_ = fgCyan
_ = fgWhite
_ = fgBlackHi
_ = fgRedHi
_ = fgGreenHi
_ = fgYellowHi
_ = fgBlueHi
_ = fgMagentaHi
_ = fgCyanHi
_ = fgWhiteHi
_ = bgBlack
_ = bgRed
_ = bgGreen
_ = bgYellow
_ = bgBlue
_ = bgMagenta
_ = bgCyan
_ = bgWhite
_ = bgBlackHi
_ = bgRedHi
_ = bgGreenHi
_ = bgYellowHi
_ = bgBlueHi
_ = bgMagentaHi
_ = bgCyanHi
_ = bgWhiteHi
)
@@ -0,0 +1,67 @@
// 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 scan
import (
"errors"
"strings"
)
//lint:file-ignore ST1005 Ignore staticcheck message about error formatting
var (
// ErrVulnerabilitiesFound indicates that vulnerabilities were detected
// when running govulncheck. This returns exit status 3 when running
// without the -json flag.
errVulnerabilitiesFound = &exitCodeError{message: "vulnerabilities found", code: 3}
// errHelp indicates that usage help was requested.
errHelp = &exitCodeError{message: "help requested", code: 0}
// errUsage indicates that there was a usage error on the command line.
//
// In this case, we assume that the user does not know how to run
// govulncheck, and print the usage message with exit status 2.
errUsage = &exitCodeError{message: "invalid usage", code: 2}
// errGoVersionMismatch 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.`)
// errNoGoMod indicates that a go.mod file was not found in this module.
errNoGoMod = errors.New(`no go.mod file
govulncheck only works with Go modules. Try navigating to your module directory.
Otherwise, run go mod init to make your project a module.
See https://go.dev/doc/modules/managing-dependencies for more information.`)
// errNoBinaryFlag indicates that govulncheck was run on a file, without
// the -mode=binary flag.
errNoBinaryFlag = errors.New(`By default, govulncheck runs source analysis on Go modules.
Did you mean to run govulncheck with -mode=binary?
For details, run govulncheck -h.`)
)
type exitCodeError struct {
message string
code int
}
func (e *exitCodeError) Error() string { return e.message }
func (e *exitCodeError) ExitCode() int { return e.code }
// isGoVersionMismatchError checks if err is due to mismatch between
// the Go version used to build govulncheck and the one currently
// on PATH.
func isGoVersionMismatchError(err error) bool {
msg := err.Error()
// See golang.org/x/tools/go/packages/packages.go.
return strings.Contains(msg, "This application uses version go") &&
strings.Contains(msg, "It may fail to process source files")
}
@@ -0,0 +1,63 @@
// Copyright 2023 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.
//go:build go1.18
// +build go1.18
package scan
import (
"encoding/json"
"fmt"
"io"
"sort"
"golang.org/x/vuln/internal/derrors"
"golang.org/x/vuln/internal/vulncheck"
)
const (
// extractModeID is the unique name of the extract mode protocol
extractModeID = "govulncheck-extract"
extractModeVersion = "0.1.0"
)
// header information for the blob output.
type header struct {
Name string `json:"name"`
Version string `json:"version"`
}
// runExtract dumps the extracted abstraction of binary at cfg.patterns to out.
// It prints out exactly two blob messages, one with the header and one with
// the vulncheck.Bin as the body.
func runExtract(cfg *config, out io.Writer) (err error) {
defer derrors.Wrap(&err, "govulncheck")
bin, err := createBin(cfg.patterns[0])
if err != nil {
return err
}
sortBin(bin) // sort for easier testing and validation
header := header{
Name: extractModeID,
Version: extractModeVersion,
}
enc := json.NewEncoder(out)
if err := enc.Encode(header); err != nil {
return fmt.Errorf("marshaling blob header: %v", err)
}
if err := enc.Encode(bin); err != nil {
return fmt.Errorf("marshaling blob body: %v", err)
}
return nil
}
func sortBin(bin *vulncheck.Bin) {
sort.SliceStable(bin.PkgSymbols, func(i, j int) bool {
return bin.PkgSymbols[i].Pkg+"."+bin.PkgSymbols[i].Name < bin.PkgSymbols[j].Pkg+"."+bin.PkgSymbols[j].Name
})
}
@@ -0,0 +1,35 @@
// 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 scan
import (
"path/filepath"
"strings"
)
// AbsRelShorter takes path and returns its path relative
// to the current directory, if shorter. Returns path
// when path is an empty string or upon any error.
func AbsRelShorter(path string) string {
if path == "" {
return ""
}
c, err := filepath.Abs(".")
if err != nil {
return path
}
r, err := filepath.Rel(c, path)
if err != nil {
return path
}
rSegments := strings.Split(r, string(filepath.Separator))
pathSegments := strings.Split(path, string(filepath.Separator))
if len(rSegments) < len(pathSegments) {
return r
}
return path
}
@@ -0,0 +1,26 @@
// 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 scan
import (
"path/filepath"
"testing"
)
func TestAbsRelShorter(t *testing.T) {
thisFileAbs, _ := filepath.Abs("filepath_test.go")
for _, test := range []struct {
l string
want string
}{
{"filepath_test.go", "filepath_test.go"},
{thisFileAbs, "filepath_test.go"},
} {
if got := AbsRelShorter(test.l); got != test.want {
t.Errorf("want %s; got %s", test.want, got)
}
}
}
@@ -0,0 +1,197 @@
// 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 scan
import (
"flag"
"fmt"
"io"
"os"
"strings"
"golang.org/x/tools/go/buildutil"
"golang.org/x/vuln/internal/govulncheck"
)
type config struct {
govulncheck.Config
patterns []string
mode string
db string
json bool
dir string
tags []string
test bool
show []string
env []string
}
const (
modeBinary = "binary"
modeSource = "source"
modeConvert = "convert" // only intended for use by gopls
modeQuery = "query" // only intended for use by gopls
modeExtract = "extract" // currently, only binary extraction is supported
)
func parseFlags(cfg *config, stderr io.Writer, args []string) error {
var tagsFlag buildutil.TagsFlag
var showFlag showFlag
var version bool
flags := flag.NewFlagSet("", flag.ContinueOnError)
flags.SetOutput(stderr)
flags.BoolVar(&cfg.json, "json", false, "output JSON")
flags.BoolVar(&cfg.test, "test", false, "analyze test files (only valid for source mode, default false)")
flags.StringVar(&cfg.dir, "C", "", "change to `dir` before running govulncheck")
flags.StringVar(&cfg.db, "db", "https://vuln.go.dev", "vulnerability database `url`")
flags.StringVar(&cfg.mode, "mode", modeSource, "supports source or binary")
flags.Var(&tagsFlag, "tags", "comma-separated `list` of build tags")
flags.Var(&showFlag, "show", "enable display of additional information specified by the comma separated `list`\nThe supported values are 'traces','color', 'version', and 'verbose'")
flags.BoolVar(&version, "version", false, "print the version information")
scanLevel := flags.String("scan", "symbol", "set the scanning level desired, one of module, package or symbol")
flags.Usage = func() {
fmt.Fprint(flags.Output(), `Govulncheck reports known vulnerabilities in dependencies.
Usage:
govulncheck [flags] [patterns]
govulncheck -mode=binary [flags] [binary]
`)
flags.PrintDefaults()
fmt.Fprintf(flags.Output(), "\n%s\n", detailsMessage)
}
if err := flags.Parse(args); err != nil {
if err == flag.ErrHelp {
return errHelp
}
return err
}
cfg.patterns = flags.Args()
cfg.tags = tagsFlag
cfg.show = showFlag
if version {
cfg.show = append(cfg.show, "version")
}
cfg.ScanLevel = govulncheck.ScanLevel(*scanLevel)
if err := validateConfig(cfg); err != nil {
fmt.Fprintln(flags.Output(), err)
return errUsage
}
return nil
}
var supportedModes = map[string]bool{
modeSource: true,
modeBinary: true,
modeConvert: true,
modeQuery: true,
modeExtract: true,
}
var supportedLevels = map[string]bool{
govulncheck.ScanLevelModule: true,
govulncheck.ScanLevelPackage: true,
govulncheck.ScanLevelSymbol: true,
}
func validateConfig(cfg *config) error {
if _, ok := supportedModes[cfg.mode]; !ok {
return fmt.Errorf("%q is not a valid mode", cfg.mode)
}
if _, ok := supportedLevels[string(cfg.ScanLevel)]; !ok {
return fmt.Errorf("%q is not a valid scan level", cfg.ScanLevel)
}
switch cfg.mode {
case modeSource:
if len(cfg.patterns) == 1 && isFile(cfg.patterns[0]) {
return fmt.Errorf("%q is a file.\n\n%v", cfg.patterns[0], errNoBinaryFlag)
}
if cfg.ScanLevel == govulncheck.ScanLevelModule && len(cfg.patterns) != 0 {
return fmt.Errorf("patterns are not accepted for module only scanning")
}
case modeBinary:
if cfg.test {
return fmt.Errorf("the -test flag is not supported in binary mode")
}
if len(cfg.tags) > 0 {
return fmt.Errorf("the -tags flag is not supported in binary mode")
}
if len(cfg.patterns) != 1 {
return fmt.Errorf("only 1 binary can be analyzed at a time")
}
if !isFile(cfg.patterns[0]) {
return fmt.Errorf("%q is not a file", cfg.patterns[0])
}
case modeExtract:
if cfg.test {
return fmt.Errorf("the -test flag is not supported in extract mode")
}
if len(cfg.tags) > 0 {
return fmt.Errorf("the -tags flag is not supported in extract mode")
}
if len(cfg.patterns) != 1 {
return fmt.Errorf("only 1 binary can be extracted at a time")
}
if cfg.json {
return fmt.Errorf("the -json flag must be off in extract mode")
}
if !isFile(cfg.patterns[0]) {
return fmt.Errorf("%q is not a file (source extraction is not supported)", cfg.patterns[0])
}
case modeConvert:
if len(cfg.patterns) != 0 {
return fmt.Errorf("patterns are not accepted in convert mode")
}
if cfg.dir != "" {
return fmt.Errorf("the -C flag is not supported in convert mode")
}
if cfg.test {
return fmt.Errorf("the -test flag is not supported in convert mode")
}
if len(cfg.tags) > 0 {
return fmt.Errorf("the -tags flag is not supported in convert mode")
}
case modeQuery:
if cfg.test {
return fmt.Errorf("the -test flag is not supported in query mode")
}
if len(cfg.tags) > 0 {
return fmt.Errorf("the -tags flag is not supported in query mode")
}
if !cfg.json {
return fmt.Errorf("the -json flag must be set in query mode")
}
for _, pattern := range cfg.patterns {
// Parse the input here so that we can catch errors before
// outputting the Config.
if _, _, err := parseModuleQuery(pattern); err != nil {
return err
}
}
}
if cfg.json && len(cfg.show) > 0 {
return fmt.Errorf("the -show flag is not supported for JSON output")
}
return nil
}
func isFile(path string) bool {
s, err := os.Stat(path)
if err != nil {
return false
}
return !s.IsDir()
}
type showFlag []string
func (v *showFlag) Set(s string) error {
*v = append(*v, strings.Split(s, ",")...)
return nil
}
func (f *showFlag) Get() interface{} { return *f }
func (f *showFlag) String() string { return "<options>" }
@@ -0,0 +1,79 @@
// 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 scan_test
import (
"bytes"
"flag"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/scan"
)
var update = flag.Bool("update", false, "update test files with results")
func TestPrinting(t *testing.T) {
testdata := os.DirFS("testdata")
inputs, err := fs.Glob(testdata, "*.json")
if err != nil {
t.Fatal(err)
}
for _, input := range inputs {
name := strings.TrimSuffix(input, ".json")
rawJSON, _ := fs.ReadFile(testdata, input)
textfiles, err := fs.Glob(testdata, name+"*.txt")
if err != nil {
t.Fatal(err)
}
for _, textfile := range textfiles {
textname := strings.TrimSuffix(textfile, ".txt")
t.Run(textname, func(t *testing.T) {
wantText, _ := fs.ReadFile(testdata, textfile)
got := &bytes.Buffer{}
handler := scan.NewTextHandler(got)
handler.Show(strings.Split(textname, "_")[1:])
testRunHandler(t, rawJSON, handler)
if diff := cmp.Diff(string(wantText), got.String()); diff != "" {
if *update {
// write the output back to the file
os.WriteFile(filepath.Join("testdata", textfile), got.Bytes(), 0644)
return
}
t.Errorf("Readable mismatch (-want, +got):\n%s", diff)
}
})
}
t.Run(name+"_json", func(t *testing.T) {
// this effectively tests that we can round trip the json
got := &strings.Builder{}
testRunHandler(t, rawJSON, govulncheck.NewJSONHandler(got))
if diff := cmp.Diff(strings.TrimSpace(string(rawJSON)), strings.TrimSpace(got.String())); diff != "" {
t.Errorf("JSON mismatch (-want, +got):\n%s", diff)
}
})
}
}
func testRunHandler(t *testing.T, rawJSON []byte, handler govulncheck.Handler) {
if err := govulncheck.HandleJSON(bytes.NewReader(rawJSON), handler); err != nil {
t.Fatal(err)
}
err := scan.Flush(handler)
switch e := err.(type) {
case nil:
case interface{ ExitCode() int }:
if e.ExitCode() != 0 && e.ExitCode() != 3 {
// not success or vulnerabilities found
t.Fatal(err)
}
default:
t.Fatal(err)
}
}
@@ -0,0 +1,74 @@
// Copyright 2023 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 scan
import (
"context"
"fmt"
"regexp"
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/govulncheck"
isem "golang.org/x/vuln/internal/semver"
)
// runQuery reports vulnerabilities that apply to the queries in the config.
func runQuery(ctx context.Context, handler govulncheck.Handler, cfg *config, c *client.Client) error {
reqs := make([]*client.ModuleRequest, len(cfg.patterns))
for i, query := range cfg.patterns {
mod, ver, err := parseModuleQuery(query)
if err != nil {
return err
}
if err := handler.Progress(queryProgressMessage(mod, ver)); err != nil {
return err
}
reqs[i] = &client.ModuleRequest{
Path: mod, Version: ver,
}
}
resps, err := c.ByModules(ctx, reqs)
if err != nil {
return err
}
ids := make(map[string]bool)
for _, resp := range resps {
for _, entry := range resp.Entries {
if _, ok := ids[entry.ID]; !ok {
err := handler.OSV(entry)
if err != nil {
return err
}
ids[entry.ID] = true
}
}
}
return nil
}
func queryProgressMessage(module, version string) *govulncheck.Progress {
return &govulncheck.Progress{
Message: fmt.Sprintf("Looking up vulnerabilities in %s at %s...", module, version),
}
}
var modQueryRegex = regexp.MustCompile(`(.+)@(.+)`)
func parseModuleQuery(pattern string) (_ string, _ string, err error) {
matches := modQueryRegex.FindStringSubmatch(pattern)
// matches should be [module@version, module, version]
if len(matches) != 3 {
return "", "", fmt.Errorf("invalid query %s: must be of the form module@version", pattern)
}
mod, ver := matches[1], matches[2]
if !isem.Valid(ver) {
return "", "", fmt.Errorf("version %s is not valid semver", ver)
}
return mod, ver, nil
}
@@ -0,0 +1,177 @@
// Copyright 2023 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 scan
import (
"context"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/osv"
"golang.org/x/vuln/internal/test"
)
func TestRunQuery(t *testing.T) {
e := &osv.Entry{
ID: "GO-1999-0001",
Affected: []osv.Affected{{
Module: osv.Module{Path: "bad.com"},
Ranges: []osv.Range{{
Type: osv.RangeTypeSemver,
Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.2.3"}},
}},
EcosystemSpecific: osv.EcosystemSpecific{
Packages: []osv.Package{{
Path: "bad.com",
}, {
Path: "bad.com/bad",
}},
},
}, {
Module: osv.Module{Path: "unfixable.com"},
Ranges: []osv.Range{{
Type: osv.RangeTypeSemver,
Events: []osv.RangeEvent{{Introduced: "0"}}, // no fix
}},
EcosystemSpecific: osv.EcosystemSpecific{
Packages: []osv.Package{{
Path: "unfixable.com",
}},
},
}},
}
e2 := &osv.Entry{
ID: "GO-1999-0002",
Affected: []osv.Affected{{
Module: osv.Module{Path: "bad.com"},
Ranges: []osv.Range{{
Type: osv.RangeTypeSemver,
Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.2.0"}},
}},
EcosystemSpecific: osv.EcosystemSpecific{
Packages: []osv.Package{{
Path: "bad.com/pkg",
},
},
},
}},
}
stdlib := &osv.Entry{
ID: "GO-2000-0003",
Affected: []osv.Affected{{
Module: osv.Module{Path: "stdlib"},
Ranges: []osv.Range{{
Type: osv.RangeTypeSemver,
Events: []osv.RangeEvent{{Introduced: "0"}, {Fixed: "1.19.4"}},
}},
EcosystemSpecific: osv.EcosystemSpecific{
Packages: []osv.Package{{
Path: "net/http",
}},
},
}},
}
c, err := client.NewInMemoryClient([]*osv.Entry{e, e2, stdlib})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
for _, tc := range []struct {
query []string
want []*osv.Entry
}{
{
query: []string{"stdlib@go1.18"},
want: []*osv.Entry{stdlib},
},
{
query: []string{"stdlib@1.18"},
want: []*osv.Entry{stdlib},
},
{
query: []string{"stdlib@v1.18.0"},
want: []*osv.Entry{stdlib},
},
{
query: []string{"bad.com@1.2.3"},
want: nil,
},
{
query: []string{"bad.com@v1.1.0"},
want: []*osv.Entry{e, e2},
},
{
query: []string{"unfixable.com@2.0.0"},
want: []*osv.Entry{e},
},
{
// each entry should only show up once
query: []string{"bad.com@1.1.0", "unfixable.com@2.0.0"},
want: []*osv.Entry{e, e2},
},
{
query: []string{"stdlib@1.18", "unfixable.com@2.0.0"},
want: []*osv.Entry{stdlib, e},
},
} {
t.Run(strings.Join(tc.query, ","), func(t *testing.T) {
h := test.NewMockHandler()
err := runQuery(ctx, h, &config{patterns: tc.query}, c)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(h.OSVMessages, tc.want); diff != "" {
t.Errorf("runQuery: unexpected diff:\n%s", diff)
}
})
}
}
func TestParseModuleQuery(t *testing.T) {
for _, tc := range []struct {
pattern, wantMod, wantVer string
wantErr string
}{
{
pattern: "stdlib@go1.18",
wantMod: "stdlib",
wantVer: "go1.18",
},
{
pattern: "golang.org/x/tools@v0.0.0-20140414041502-123456789012",
wantMod: "golang.org/x/tools",
wantVer: "v0.0.0-20140414041502-123456789012",
},
{
pattern: "golang.org/x/tools",
wantErr: "invalid query",
},
{
pattern: "golang.org/x/tools@1.0.0.2",
wantErr: "not valid semver",
},
} {
t.Run(tc.pattern, func(t *testing.T) {
gotMod, gotVer, err := parseModuleQuery(tc.pattern)
if tc.wantErr == "" {
if err != nil {
t.Fatal(err)
}
if gotMod != tc.wantMod || gotVer != tc.wantVer {
t.Errorf("parseModuleQuery = (%s, %s), want (%s, %s)", gotMod, gotVer, tc.wantMod, tc.wantVer)
}
} else {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("parseModuleQuery = %v, want err containing %s", err, tc.wantErr)
}
}
})
}
}
@@ -0,0 +1,75 @@
// 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 scan
import (
"strings"
"testing"
"golang.org/x/vuln/internal/govulncheck"
)
func TestFrame(t *testing.T) {
for _, test := range []struct {
name string
frame *govulncheck.Frame
short bool
wantFunc string
wantPos string
}{
{
name: "position and function",
frame: &govulncheck.Frame{
Package: "golang.org/x/vuln/internal/vulncheck",
Function: "Foo",
Position: &govulncheck.Position{Filename: "some/path/file.go", Line: 12},
},
wantFunc: "golang.org/x/vuln/internal/vulncheck.Foo",
wantPos: "some/path/file.go:12",
},
{
name: "receiver",
frame: &govulncheck.Frame{
Package: "golang.org/x/vuln/internal/vulncheck",
Receiver: "Bar",
Function: "Foo",
},
wantFunc: "golang.org/x/vuln/internal/vulncheck.Bar.Foo",
},
{
name: "function and receiver",
frame: &govulncheck.Frame{Receiver: "*ServeMux", Function: "Handle"},
wantFunc: "ServeMux.Handle",
},
{
name: "package and function",
frame: &govulncheck.Frame{Package: "net/http", Function: "Get"},
wantFunc: "net/http.Get",
},
{
name: "package, function and receiver",
frame: &govulncheck.Frame{Package: "net/http", Receiver: "*ServeMux", Function: "Handle"},
wantFunc: "net/http.ServeMux.Handle",
},
{
name: "short",
frame: &govulncheck.Frame{Package: "net/http", Function: "Get"},
short: true,
wantFunc: "http.Get",
},
} {
t.Run(test.name, func(t *testing.T) {
buf := &strings.Builder{}
addSymbolName(buf, test.frame, test.short)
got := buf.String()
if got != test.wantFunc {
t.Errorf("want %v func name; got %v", test.wantFunc, got)
}
if got := posToString(test.frame.Position); got != test.wantPos {
t.Errorf("want %v call position; got %v", test.wantPos, got)
}
})
}
}
@@ -0,0 +1,132 @@
// 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 scan
import (
"context"
"fmt"
"io"
"os/exec"
"path"
"path/filepath"
"runtime/debug"
"strings"
"time"
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/govulncheck"
)
// RunGovulncheck performs main govulncheck functionality and exits the
// program upon success with an appropriate exit status. Otherwise,
// returns an error.
func RunGovulncheck(ctx context.Context, env []string, r io.Reader, stdout io.Writer, stderr io.Writer, args []string) error {
cfg := &config{env: env}
if err := parseFlags(cfg, stderr, args); err != nil {
return err
}
client, err := client.NewClient(cfg.db, nil)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
prepareConfig(ctx, cfg, client)
var handler govulncheck.Handler
switch {
case cfg.json:
handler = govulncheck.NewJSONHandler(stdout)
default:
th := NewTextHandler(stdout)
th.Show(cfg.show)
handler = th
}
// Write the introductory message to the user.
if err := handler.Config(&cfg.Config); err != nil {
return err
}
switch cfg.mode {
case modeSource:
dir := filepath.FromSlash(cfg.dir)
err = runSource(ctx, handler, cfg, client, dir)
case modeBinary:
err = runBinary(ctx, handler, cfg, client)
case modeExtract:
return runExtract(cfg, stdout)
case modeQuery:
err = runQuery(ctx, handler, cfg, client)
case modeConvert:
err = govulncheck.HandleJSON(r, handler)
}
if err != nil {
return err
}
return Flush(handler)
}
func prepareConfig(ctx context.Context, cfg *config, client *client.Client) {
cfg.ProtocolVersion = govulncheck.ProtocolVersion
cfg.DB = cfg.db
if cfg.mode == modeSource && cfg.GoVersion == "" {
const goverPrefix = "GOVERSION="
for _, env := range cfg.env {
if val := strings.TrimPrefix(env, goverPrefix); val != env {
cfg.GoVersion = val
}
}
if cfg.GoVersion == "" {
if out, err := exec.Command("go", "env", "GOVERSION").Output(); err == nil {
cfg.GoVersion = strings.TrimSpace(string(out))
}
}
}
if bi, ok := debug.ReadBuildInfo(); ok {
scannerVersion(cfg, bi)
}
if mod, err := client.LastModifiedTime(ctx); err == nil {
cfg.DBLastModified = &mod
}
}
// scannerVersion reconstructs the current version of
// this binary used from the build info.
func scannerVersion(cfg *config, bi *debug.BuildInfo) {
if bi.Path != "" {
cfg.ScannerName = path.Base(bi.Path)
}
if bi.Main.Version != "" && bi.Main.Version != "(devel)" {
cfg.ScannerVersion = bi.Main.Version
return
}
// TODO(https://go.dev/issue/29228): we need to manually construct the
// version string when it is "(devel)" until #29228 is resolved.
var revision, at string
for _, s := range bi.Settings {
if s.Key == "vcs.revision" {
revision = s.Value
}
if s.Key == "vcs.time" {
at = s.Value
}
}
buf := strings.Builder{}
buf.WriteString("v0.0.0")
if revision != "" {
buf.WriteString("-")
buf.WriteString(revision[:12])
}
if at != "" {
// commit time is of the form 2023-01-25T19:57:54Z
p, err := time.Parse(time.RFC3339, at)
if err == nil {
buf.WriteString("-")
buf.WriteString(p.Format("20060102150405"))
}
}
cfg.ScannerVersion = buf.String()
}
@@ -0,0 +1,26 @@
// 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 scan
import (
"runtime/debug"
"testing"
)
func TestGovulncheckVersion(t *testing.T) {
bi := &debug.BuildInfo{
Settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "1234567890001234"},
{Key: "vcs.time", Value: "2023-01-25T19:57:54Z"},
},
}
want := "v0.0.0-123456789000-20230125195754"
got := &config{}
scannerVersion(got, bi)
if got.ScannerVersion != want {
t.Errorf("got %s; want %s", got.ScannerVersion, want)
}
}
@@ -0,0 +1,124 @@
// 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 scan
import (
"context"
"fmt"
"golang.org/x/tools/go/packages"
"golang.org/x/vuln/internal/client"
"golang.org/x/vuln/internal/derrors"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/vulncheck"
)
// runSource reports vulnerabilities that affect the analyzed packages.
//
// Vulnerabilities can be called (affecting the package, because a vulnerable
// symbol is actually exercised) or just imported by the package
// (likely having a non-affecting outcome).
func runSource(ctx context.Context, handler govulncheck.Handler, cfg *config, client *client.Client, dir string) (err error) {
defer derrors.Wrap(&err, "govulncheck")
if cfg.ScanLevel.WantPackages() && len(cfg.patterns) == 0 {
return nil // don't throw an error here
}
if !gomodExists(dir) {
return errNoGoMod
}
var pkgs []*packages.Package
var mods []*packages.Module
graph := vulncheck.NewPackageGraph(cfg.GoVersion)
pkgConfig := &packages.Config{
Dir: dir,
Tests: cfg.test,
Env: cfg.env,
}
pkgs, mods, err = graph.LoadPackagesAndMods(pkgConfig, cfg.tags, cfg.patterns)
if err != nil {
if isGoVersionMismatchError(err) {
return fmt.Errorf("%v\n\n%v", errGoVersionMismatch, err)
}
return fmt.Errorf("loading packages: %w", err)
}
if err := handler.Progress(sourceProgressMessage(pkgs, len(mods)-1, cfg.ScanLevel)); err != nil {
return err
}
if cfg.ScanLevel.WantPackages() && len(pkgs) == 0 {
return nil // early exit
}
return vulncheck.Source(ctx, handler, pkgs, mods, &cfg.Config, client, graph)
}
// sourceProgressMessage returns a string of the form
//
// "Scanning your code and P packages across M dependent modules for known vulnerabilities..."
//
// P is the number of strictly dependent packages of
// topPkgs and Y is the number of their modules. If P
// is 0, then the following message is returned
//
// "No packages matching the provided pattern."
func sourceProgressMessage(topPkgs []*packages.Package, mods int, mode govulncheck.ScanLevel) *govulncheck.Progress {
var pkgsPhrase, modsPhrase string
if mode.WantPackages() {
if len(topPkgs) == 0 {
// The package pattern is valid, but no packages are matching.
// Example is pkg/strace/... (see #59623).
return &govulncheck.Progress{Message: "No packages matching the provided pattern."}
}
pkgs := depPkgs(topPkgs)
pkgsPhrase = fmt.Sprintf(" and %d package%s", pkgs, choose(pkgs != 1, "s", ""))
}
modsPhrase = fmt.Sprintf(" %d dependent module%s", mods, choose(mods != 1, "s", ""))
msg := fmt.Sprintf("Scanning your code%s across%s for known vulnerabilities...", pkgsPhrase, modsPhrase)
return &govulncheck.Progress{Message: msg}
}
// depPkgs returns the number of packages that topPkgs depend on
func depPkgs(topPkgs []*packages.Package) int {
tops := make(map[string]bool)
depPkgs := make(map[string]bool)
for _, t := range topPkgs {
tops[t.PkgPath] = true
}
var visit func(*packages.Package, bool)
visit = func(p *packages.Package, top bool) {
path := p.PkgPath
if depPkgs[path] {
return
}
if tops[path] && !top {
// A top package that is a dependency
// will not be in depPkgs, so we skip
// reiterating on it here.
return
}
// We don't count a top-level package as
// a dependency even when they are used
// as a dependent package.
if !tops[path] {
depPkgs[path] = true
}
for _, d := range p.Imports {
visit(d, false)
}
}
for _, t := range topPkgs {
visit(t, true)
}
return len(depPkgs)
}
@@ -0,0 +1,76 @@
// 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 scan
import (
"strings"
"testing"
"golang.org/x/vuln/internal/govulncheck"
)
func TestSummarizeCallStack(t *testing.T) {
for _, test := range []struct {
in, want string
}{
{"ma.a.F", "a.F"},
{"m1.p1.F", "p1.F"},
{"mv.v.V", "v.V"},
{
"m1.p1.F mv.v.V",
"p1.F calls v.V",
},
{
"m1.p1.F m1.p2.G mv.v.V1 mv.v.v2",
"p2.G calls v.V1, which calls v.v2",
},
{
"m1.p1.F m1.p2.G mv.v.V$1 mv.v.V1",
"p2.G calls v.V, which calls v.V1",
},
{
"m1.p1.F m1.p2.G$1 mv.v.V1",
"p2.G calls v.V1",
},
{
"m1.p1.F m1.p2.G$1 mv.v.V$1 mv.v.V1",
"p2.G calls v.V, which calls v.V1",
},
{
"m1.p1.F w.x.Y m1.p2.G ma.a.H mb.b.I mc.c.J mv.v.V",
"p2.G calls a.H, which eventually calls v.V",
},
{
"m1.p1.F w.x.Y m1.p2.G ma.a.H mb.b.I mc.c.J mv.v.V$1 mv.v.V1",
"p2.G calls a.H, which eventually calls v.V1",
},
{
"m1.p1.F m1.p1.F$1 ma.a.H mb.b.I mv.v.V1",
"p1.F calls a.H, which eventually calls v.V1",
},
} {
in := stringToFinding(test.in)
got := compactTrace(in)
if got != test.want {
t.Errorf("%s:\ngot %s\nwant %s", test.in, got, test.want)
}
}
}
func stringToFinding(s string) *govulncheck.Finding {
f := &govulncheck.Finding{}
entries := strings.Fields(s)
for i := len(entries) - 1; i >= 0; i-- {
e := entries[i]
firstDot := strings.Index(e, ".")
lastDot := strings.LastIndex(e, ".")
f.Trace = append(f.Trace, &govulncheck.Frame{
Module: e[:firstDot],
Package: e[:firstDot] + "/" + e[firstDot+1:lastDot],
Function: e[lastDot+1:],
})
}
return f
}
@@ -0,0 +1,71 @@
// 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 scan
import (
"fmt"
"strings"
"golang.org/x/mod/semver"
)
// Support functions for standard library packages.
// These are copied from the internal/stdlib package in the pkgsite repo.
// semverToGoTag returns the Go standard library repository tag corresponding
// to semver, a version string without the initial "v".
// Go tags differ from standard semantic versions in a few ways,
// such as beginning with "go" instead of "v".
func semverToGoTag(v string) string {
if strings.HasPrefix(v, "v0.0.0") {
return "master"
}
// Special case: v1.0.0 => go1.
if v == "v1.0.0" {
return "go1"
}
if !semver.IsValid(v) {
return fmt.Sprintf("<!%s:invalid semver>", v)
}
goVersion := semver.Canonical(v)
prerelease := semver.Prerelease(goVersion)
versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease)
patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".")
if patch == "0" {
versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0")
}
goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v"))
if prerelease != "" {
// Go prereleases look like "beta1" instead of "beta.1".
// "beta1" is bad for sorting (since beta10 comes before beta9), so
// require the dot form.
i := finalDigitsIndex(prerelease)
if i >= 1 {
if prerelease[i-1] != '.' {
return fmt.Sprintf("<!%s:final digits in a prerelease must follow a period>", v)
}
// Remove the dot.
prerelease = prerelease[:i-1] + prerelease[i:]
}
goVersion += strings.TrimPrefix(prerelease, "-")
}
return goVersion
}
// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s.
// If s doesn't end in digits, it returns -1.
func finalDigitsIndex(s string) int {
// Assume ASCII (since the semver package does anyway).
var i int
for i = len(s) - 1; i >= 0; i-- {
if s[i] < '0' || s[i] > '9' {
break
}
}
if i == len(s)-1 {
return -1
}
return i + 1
}
@@ -0,0 +1,287 @@
// 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 scan
import (
"go/token"
"io"
"path"
"sort"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/osv"
)
type findingSummary struct {
*govulncheck.Finding
Compact string
OSV *osv.Entry
}
type summaryCounters struct {
VulnerabilitiesCalled int
ModulesCalled int
VulnerabilitiesImported int
VulnerabilitiesRequired int
StdlibCalled bool
}
func fixupFindings(osvs []*osv.Entry, findings []*findingSummary) {
for _, f := range findings {
f.OSV = getOSV(osvs, f.Finding.OSV)
}
}
func groupByVuln(findings []*findingSummary) [][]*findingSummary {
return groupBy(findings, func(left, right *findingSummary) int {
return -strings.Compare(left.OSV.ID, right.OSV.ID)
})
}
func groupByModule(findings []*findingSummary) [][]*findingSummary {
return groupBy(findings, func(left, right *findingSummary) int {
return strings.Compare(left.Trace[0].Module, right.Trace[0].Module)
})
}
func groupBy(findings []*findingSummary, compare func(left, right *findingSummary) int) [][]*findingSummary {
switch len(findings) {
case 0:
return nil
case 1:
return [][]*findingSummary{findings}
}
sort.SliceStable(findings, func(i, j int) bool {
return compare(findings[i], findings[j]) < 0
})
result := [][]*findingSummary{}
first := 0
for i, next := range findings {
if i == first {
continue
}
if compare(findings[first], next) != 0 {
result = append(result, findings[first:i])
first = i
}
}
result = append(result, findings[first:])
return result
}
func isRequired(findings []*findingSummary) bool {
for _, f := range findings {
if f.Trace[0].Module != "" {
return true
}
}
return false
}
func isImported(findings []*findingSummary) bool {
for _, f := range findings {
if f.Trace[0].Package != "" {
return true
}
}
return false
}
func isCalled(findings []*findingSummary) bool {
for _, f := range findings {
if f.Trace[0].Function != "" {
return true
}
}
return false
}
func getOSV(osvs []*osv.Entry, id string) *osv.Entry {
for _, entry := range osvs {
if entry.ID == id {
return entry
}
}
return &osv.Entry{
ID: id,
DatabaseSpecific: &osv.DatabaseSpecific{},
}
}
func newFindingSummary(f *govulncheck.Finding) *findingSummary {
return &findingSummary{
Finding: f,
Compact: compactTrace(f),
}
}
// platforms returns a string describing the GOOS, GOARCH,
// or GOOS/GOARCH pairs that the vuln affects for a particular
// module mod. If it affects all of them, it returns the empty
// string.
//
// When mod is an empty string, returns platform information for
// all modules of e.
func platforms(mod string, e *osv.Entry) []string {
if e == nil {
return nil
}
platforms := map[string]bool{}
for _, a := range e.Affected {
if mod != "" && a.Module.Path != mod {
continue
}
for _, p := range a.EcosystemSpecific.Packages {
for _, os := range p.GOOS {
// In case there are no specific architectures,
// just list the os entries.
if len(p.GOARCH) == 0 {
platforms[os] = true
continue
}
// Otherwise, list all the os+arch combinations.
for _, arch := range p.GOARCH {
platforms[os+"/"+arch] = true
}
}
// Cover the case where there are no specific
// operating systems listed.
if len(p.GOOS) == 0 {
for _, arch := range p.GOARCH {
platforms[arch] = true
}
}
}
}
var keys []string
for k := range platforms {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func posToString(p *govulncheck.Position) string {
if p == nil || p.Line <= 0 {
return ""
}
return token.Position{
Filename: AbsRelShorter(p.Filename),
Offset: p.Offset,
Line: p.Line,
Column: p.Column,
}.String()
}
func symbol(frame *govulncheck.Frame, short bool) string {
buf := &strings.Builder{}
addSymbolName(buf, frame, short)
return buf.String()
}
// compactTrace returns a short description of the call stack.
// It prefers to show you the edge from the top module to other code, along with
// the vulnerable symbol.
// Where the vulnerable symbol directly called by the users code, it will only
// show those two points.
// If the vulnerable symbol is in the users code, it will show the entry point
// and the vulnerable symbol.
func compactTrace(finding *govulncheck.Finding) string {
if len(finding.Trace) < 1 {
return ""
}
iTop := len(finding.Trace) - 1
topModule := finding.Trace[iTop].Module
// search for the exit point of the top module
for i, frame := range finding.Trace {
if frame.Module == topModule {
iTop = i
break
}
}
if iTop == 0 {
// all in one module, reset to the end
iTop = len(finding.Trace) - 1
}
buf := &strings.Builder{}
topPos := posToString(finding.Trace[iTop].Position)
if topPos != "" {
buf.WriteString(topPos)
buf.WriteString(": ")
}
if iTop > 0 {
addSymbolName(buf, finding.Trace[iTop], true)
buf.WriteString(" calls ")
}
if iTop > 1 {
addSymbolName(buf, finding.Trace[iTop-1], true)
buf.WriteString(", which")
if iTop > 2 {
buf.WriteString(" eventually")
}
buf.WriteString(" calls ")
}
addSymbolName(buf, finding.Trace[0], true)
return buf.String()
}
// notIdentifier reports whether ch is an invalid identifier character.
func notIdentifier(ch rune) bool {
return !('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' ||
'0' <= ch && ch <= '9' ||
ch == '_' ||
ch >= utf8.RuneSelf && (unicode.IsLetter(ch) || unicode.IsDigit(ch)))
}
// importPathToAssumedName is taken from goimports, it works out the natural imported name
// for a package.
// This is used to get a shorter identifier in the compact stack trace
func importPathToAssumedName(importPath string) string {
base := path.Base(importPath)
if strings.HasPrefix(base, "v") {
if _, err := strconv.Atoi(base[1:]); err == nil {
dir := path.Dir(importPath)
if dir != "." {
base = path.Base(dir)
}
}
}
base = strings.TrimPrefix(base, "go-")
if i := strings.IndexFunc(base, notIdentifier); i >= 0 {
base = base[:i]
}
return base
}
func addSymbolName(w io.Writer, frame *govulncheck.Frame, short bool) {
if frame.Function == "" {
return
}
if frame.Package != "" {
pkg := frame.Package
if short {
pkg = importPathToAssumedName(frame.Package)
}
io.WriteString(w, pkg)
io.WriteString(w, ".")
}
if frame.Receiver != "" {
if frame.Receiver[0] == '*' {
io.WriteString(w, frame.Receiver[1:])
} else {
io.WriteString(w, frame.Receiver)
}
io.WriteString(w, ".")
}
funcname := strings.Split(frame.Function, "$")[0]
io.WriteString(w, funcname)
}
@@ -0,0 +1,82 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod",
"function": "Vuln"
}
]
}
}
{
"osv": {
"id": "GO-0000-0002",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Stdlib vulnerability",
"affected": [
{
"package": {
"name": "stdlib",
"ecosystem": ""
},
"ecosystem_specific": {}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0002"
}
}
}
{
"finding": {
"osv": "GO-0000-0002",
"trace": [
{
"module": "stdlib",
"version": "v0.0.1",
"package": "net/http",
"function": "Vuln2"
}
]
}
}
@@ -0,0 +1,25 @@
=== Symbol Results ===
Vulnerability #1: GO-0000-0002
Stdlib vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0002
Standard library
Found in: net/http@go0.0.1
Fixed in: N/A
Example traces found:
#1: http.Vuln2
Vulnerability #2: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Example traces found:
#1: vmod.Vuln
Your code is affected by 2 vulnerabilities from 1 module and the Go standard library.
This scan found no other vulnerabilities in packages you import or modules you
require.
Use '-show verbose' for more details.
@@ -0,0 +1,67 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1"
}
]
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod",
"function": "VulnFoo"
},
{
"module": "golang.org/main",
"version": "v0.0.1",
"package": "golang.org/main",
"function": "main"
}
]
}
}
@@ -0,0 +1,16 @@
=== Symbol Results ===
Vulnerability #1: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Example traces found:
#1: main.main calls vmod.VulnFoo
Your code is affected by 1 vulnerability from 1 module.
This scan found no other vulnerabilities in packages you import or modules you
require.
Use '-show verbose' for more details.
@@ -0,0 +1,47 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "module"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1"
}
]
}
}
@@ -0,0 +1,12 @@
=== Module Results ===
Vulnerability #1: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Your code may be affected by 1 vulnerability.
Use '-scan symbol' for more fine grained vulnerability detection.
@@ -0,0 +1,79 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "module"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1"
}
]
}
}
{
"osv": {
"id": "GO-0000-0002",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0002"
}
}
}
{
"finding": {
"osv": "GO-0000-0002",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1"
}
]
}
}
@@ -0,0 +1,19 @@
=== Module Results ===
Vulnerability #1: GO-0000-0002
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0002
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Vulnerability #2: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Your code may be affected by 2 vulnerabilities.
Use '-scan symbol' for more fine grained vulnerability detection.
@@ -0,0 +1,115 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "vmod",
"function": "Vuln"
},
{
"module": "golang.org/main",
"version": "v0.0.1",
"package": "main",
"function": "main"
}
]
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "vmod",
"function": "VulnFoo"
},
{
"module": "golang.org/main",
"version": "v0.0.1",
"package": "main",
"function": "main"
}
]
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.0.4",
"trace": [
{
"module": "golang.org/vmod1",
"version": "v0.0.3",
"package": "vmod1",
"function": "Vuln"
},
{
"module": "golang.org/other",
"version": "v2.0.3",
"package": "other",
"function": "Foo"
}
]
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.0.4",
"trace": [
{
"module": "golang.org/vmod1",
"version": "v0.0.3",
"package": "vmod1",
"function": "VulnFoo"
},
{
"module": "golang.org/other",
"version": "v2.0.3",
"package": "other",
"function": "Bar"
}
]
}
}
@@ -0,0 +1,24 @@
=== Symbol Results ===
Vulnerability #1: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Example traces found:
#1: main.main calls vmod.Vuln
#2: main.main calls vmod.VulnFoo
Module: golang.org/vmod1
Found in: golang.org/vmod1@v0.0.3
Fixed in: golang.org/vmod1@v0.0.4
Example traces found:
#1: other.Foo calls vmod1.Vuln
#2: other.Bar calls vmod1.VulnFoo
Your code is affected by 1 vulnerability from the Go standard library.
This scan found no other vulnerabilities in packages you import or modules you
require.
Use '-show verbose' for more details.
@@ -0,0 +1,60 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "package"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1"
}
]
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod"
}
]
}
}
@@ -0,0 +1,14 @@
=== Package Results ===
Vulnerability #1: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Your code may be affected by 1 vulnerability.
This scan also found 0 vulnerabilities in modules you require.
Use '-scan symbol' for more fine grained vulnerability detection and '-show
verbose' for more details.
@@ -0,0 +1,32 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "All",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "",
"affected": null,
"database_specific": {
"url": "https://pkg.go.dev/vuln/All"
}
}
}
{
"finding": {
"osv": "All",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod"
}
]
}
}
@@ -0,0 +1,9 @@
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 0
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,58 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "one-arch-only",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "1.2.0"
}
]
}
],
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd64"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/one-arch-only"
}
}
}
{
"finding": {
"osv": "one-arch-only",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod"
}
]
}
}
@@ -0,0 +1,9 @@
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 0
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,63 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "one-import",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "1.2.0"
}
]
}
],
"ecosystem_specific": {
"imports": [
{
"goos": [
"windows",
"linux"
],
"goarch": [
"amd64",
"wasm"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/one-import"
}
}
}
{
"finding": {
"osv": "one-import",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod"
}
]
}
}
@@ -0,0 +1,9 @@
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 0
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,69 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "two-imports",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "1.2.0"
}
]
}
],
"ecosystem_specific": {
"imports": [
{
"goos": [
"windows"
],
"goarch": [
"amd64"
]
},
{
"goos": [
"linux"
],
"goarch": [
"amd64"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/two-imports"
}
}
}
{
"finding": {
"osv": "two-imports",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod"
}
]
}
}
@@ -0,0 +1,9 @@
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 0
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,58 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "two-os-only",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "1.2.0"
}
]
}
],
"ecosystem_specific": {
"imports": [
{
"goos": [
"windows, linux"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/two-os-only"
}
}
}
{
"finding": {
"osv": "two-os-only",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "golang.org/vmod"
}
]
}
}
@@ -0,0 +1,9 @@
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 0
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,87 @@
{
"config": {
"protocol_version": "v0.1.0",
"scanner_name": "govulncheck",
"scan_level": "symbol"
}
}
{
"osv": {
"id": "GO-0000-0001",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Third-party vulnerability",
"affected": [
{
"package": {
"name": "golang.org/vmod",
"ecosystem": ""
},
"ecosystem_specific": {
"imports": [
{
"goos": [
"amd"
]
}
]
}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0001"
}
}
}
{
"finding": {
"osv": "GO-0000-0001",
"fixed_version": "v0.1.3",
"trace": [
{
"module": "golang.org/vmod",
"version": "v0.0.1",
"package": "vmod",
"function": "Vuln"
},
{
"module": "golang.org/app",
"version": "v0.0.1",
"package": "main",
"function": "main"
}
]
}
}
{
"osv": {
"id": "GO-0000-0002",
"modified": "0001-01-01T00:00:00Z",
"published": "0001-01-01T00:00:00Z",
"details": "Stdlib vulnerability",
"affected": [
{
"package": {
"name": "stdlib",
"ecosystem": ""
},
"ecosystem_specific": {}
}
],
"database_specific": {
"url": "https://pkg.go.dev/vuln/GO-0000-0002"
}
}
}
{
"finding": {
"osv": "GO-0000-0002",
"trace": [
{
"module": "stdlib",
"version": "v0.0.1",
"package": "net/http"
}
]
}
}
@@ -0,0 +1,17 @@
=== Symbol Results ===
Vulnerability #1: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Example traces found:
#1: main.main calls vmod.Vuln
Your code is affected by 1 vulnerability from the Go standard library.
This scan also found 0 vulnerabilities in packages you import and 1
vulnerability in modules you require, but your code doesn't appear to call these
vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,19 @@
=== Symbol Results ===
Vulnerability #1: GO-0000-0001
Third-party vulnerability
More info: https://pkg.go.dev/vuln/GO-0000-0001
Module: golang.org/vmod
Found in: golang.org/vmod@v0.0.1
Fixed in: golang.org/vmod@v0.1.3
Platforms: amd
Example traces found:
#1: for function vmod.Vuln
main.main
vmod.Vuln
Your code is affected by 1 vulnerability from the Go standard library.
This scan also found 0 vulnerabilities in packages you import and 1
vulnerability in modules you require, but your code doesn't appear to call these
vulnerabilities.
Use '-show verbose' for more details.
@@ -0,0 +1,481 @@
// 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 scan
import (
"fmt"
"io"
"strings"
"golang.org/x/vuln/internal"
"golang.org/x/vuln/internal/govulncheck"
"golang.org/x/vuln/internal/osv"
"golang.org/x/vuln/internal/vulncheck"
)
type style int
const (
defaultStyle = style(iota)
osvCalledStyle
osvImportedStyle
detailsStyle
sectionStyle
keyStyle
valueStyle
)
// NewtextHandler returns a handler that writes govulncheck output as text.
func NewTextHandler(w io.Writer) *TextHandler {
return &TextHandler{w: w}
}
type TextHandler struct {
w io.Writer
osvs []*osv.Entry
findings []*findingSummary
scanLevel govulncheck.ScanLevel
err error
showColor bool
showTraces bool
showVersion bool
showAllVulns bool
}
const (
detailsMessage = `For details, see https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck.`
binaryProgressMessage = `Scanning your binary for known vulnerabilities...`
noVulnsMessage = `No vulnerabilities found.`
noOtherVulnsMessage = `No other vulnerabilities found.`
verboseMessage = `'-show verbose' for more details`
symbolMessage = `'-scan symbol' for more fine grained vulnerability detection`
)
func (h *TextHandler) Show(show []string) {
for _, show := range show {
switch show {
case "traces":
h.showTraces = true
case "color":
h.showColor = true
case "version":
h.showVersion = true
case "verbose":
h.showAllVulns = true
}
}
}
func Flush(h govulncheck.Handler) error {
if th, ok := h.(interface{ Flush() error }); ok {
return th.Flush()
}
return nil
}
func (h *TextHandler) Flush() error {
if len(h.findings) == 0 {
h.print(noVulnsMessage + "\n")
} else {
fixupFindings(h.osvs, h.findings)
counters := h.allVulns(h.findings)
h.summary(counters)
}
if h.err != nil {
return h.err
}
// We found vulnerabilities when the findings' level matches the scan level.
if (isCalled(h.findings) && h.scanLevel == govulncheck.ScanLevelSymbol) ||
(isImported(h.findings) && h.scanLevel == govulncheck.ScanLevelPackage) ||
(isRequired(h.findings) && h.scanLevel == govulncheck.ScanLevelModule) {
return errVulnerabilitiesFound
}
return nil
}
// Config writes version information only if --version was set.
func (h *TextHandler) Config(config *govulncheck.Config) error {
if config.ScanLevel != "" {
h.scanLevel = config.ScanLevel
}
if !h.showVersion {
return nil
}
if config.GoVersion != "" {
h.style(keyStyle, "Go: ")
h.print(config.GoVersion, "\n")
}
if config.ScannerName != "" {
h.style(keyStyle, "Scanner: ")
h.print(config.ScannerName)
if config.ScannerVersion != "" {
h.print(`@`, config.ScannerVersion)
}
h.print("\n")
}
if config.DB != "" {
h.style(keyStyle, "DB: ")
h.print(config.DB, "\n")
if config.DBLastModified != nil {
h.style(keyStyle, "DB updated: ")
h.print(*config.DBLastModified, "\n")
}
}
h.print("\n")
return h.err
}
// Progress writes progress updates during govulncheck execution.
func (h *TextHandler) Progress(progress *govulncheck.Progress) error {
h.print(progress.Message, "\n\n")
return h.err
}
// OSV gathers osv entries to be written.
func (h *TextHandler) OSV(entry *osv.Entry) error {
h.osvs = append(h.osvs, entry)
return nil
}
// Finding gathers vulnerability findings to be written.
func (h *TextHandler) Finding(finding *govulncheck.Finding) error {
if err := validateFindings(finding); err != nil {
return err
}
h.findings = append(h.findings, newFindingSummary(finding))
return nil
}
func (h *TextHandler) allVulns(findings []*findingSummary) summaryCounters {
byVuln := groupByVuln(findings)
var called, imported, required [][]*findingSummary
mods := map[string]struct{}{}
stdlibCalled := false
for _, findings := range byVuln {
switch {
case isStdFindings(findings):
if isCalled(findings) {
called = append(called, findings)
stdlibCalled = true
} else {
required = append(required, findings)
}
case isCalled(findings):
called = append(called, findings)
mods[findings[0].Trace[0].Module] = struct{}{}
case isImported(findings):
imported = append(imported, findings)
default:
required = append(required, findings)
}
}
if h.scanLevel.WantSymbols() {
h.style(sectionStyle, "=== Symbol Results ===\n\n")
if len(called) == 0 {
h.print(noVulnsMessage, "\n\n")
}
for index, findings := range called {
h.vulnerability(index, findings)
}
}
if h.scanLevel == govulncheck.ScanLevelPackage || (h.scanLevel.WantPackages() && h.showAllVulns) {
h.style(sectionStyle, "=== Package Results ===\n\n")
if len(imported) == 0 {
h.print(choose(!h.scanLevel.WantSymbols(), noVulnsMessage, noOtherVulnsMessage), "\n\n")
}
for index, findings := range imported {
h.vulnerability(index, findings)
}
}
if h.showAllVulns || h.scanLevel == govulncheck.ScanLevelModule {
h.style(sectionStyle, "=== Module Results ===\n\n")
if len(required) == 0 {
h.print(choose(!h.scanLevel.WantPackages(), noVulnsMessage, noOtherVulnsMessage), "\n\n")
}
for index, findings := range required {
h.vulnerability(index, findings)
}
}
return summaryCounters{
VulnerabilitiesCalled: len(called),
VulnerabilitiesImported: len(imported),
VulnerabilitiesRequired: len(required),
ModulesCalled: len(mods),
StdlibCalled: stdlibCalled,
}
}
func (h *TextHandler) vulnerability(index int, findings []*findingSummary) {
h.style(keyStyle, "Vulnerability")
h.print(" #", index+1, ": ")
if isCalled(findings) {
h.style(osvCalledStyle, findings[0].OSV.ID)
} else {
h.style(osvImportedStyle, findings[0].OSV.ID)
}
h.print("\n")
h.style(detailsStyle)
description := findings[0].OSV.Summary
if description == "" {
description = findings[0].OSV.Details
}
h.wrap(" ", description, 80)
h.style(defaultStyle)
h.print("\n")
h.style(keyStyle, " More info:")
h.print(" ", findings[0].OSV.DatabaseSpecific.URL, "\n")
byModule := groupByModule(findings)
first := true
for _, module := range byModule {
//TODO: this assumes all traces on a module are found and fixed at the same versions
lastFrame := module[0].Trace[0]
mod := lastFrame.Module
path := lastFrame.Module
if path == internal.GoStdModulePath {
path = lastFrame.Package
}
foundVersion := moduleVersionString(lastFrame.Module, lastFrame.Version)
fixedVersion := moduleVersionString(lastFrame.Module, module[0].FixedVersion)
if !first {
h.print("\n")
}
first = false
h.print(" ")
if mod == internal.GoStdModulePath {
h.print("Standard library")
} else {
h.style(keyStyle, "Module: ")
h.print(mod)
}
h.print("\n ")
h.style(keyStyle, "Found in: ")
h.print(path, "@", foundVersion, "\n ")
h.style(keyStyle, "Fixed in: ")
if fixedVersion != "" {
h.print(path, "@", fixedVersion)
} else {
h.print("N/A")
}
h.print("\n")
platforms := platforms(mod, module[0].OSV)
if len(platforms) > 0 {
h.style(keyStyle, " Platforms: ")
for ip, p := range platforms {
if ip > 0 {
h.print(", ")
}
h.print(p)
}
h.print("\n")
}
h.traces(module)
}
h.print("\n")
}
func (h *TextHandler) traces(traces []*findingSummary) {
first := true
count := 1
for _, entry := range traces {
if entry.Compact == "" {
continue
}
if first {
h.style(keyStyle, " Example traces found:\n")
}
first = false
h.print(" #", count, ": ")
count++
if !h.showTraces {
h.print(entry.Compact, "\n")
} else {
h.print("for function ", symbol(entry.Trace[0], false), "\n")
for i := len(entry.Trace) - 1; i >= 0; i-- {
t := entry.Trace[i]
h.print(" ")
if t.Position != nil {
h.print(posToString(t.Position), ": ")
}
h.print(symbol(t, false), "\n")
}
}
}
}
func (h *TextHandler) summary(c summaryCounters) {
// print short summary of findings identified at the desired level of scan precision
var vulnCount int
h.print("Your code ", choose(h.scanLevel.WantSymbols(), "is", "may be"), " affected by ")
switch h.scanLevel {
case govulncheck.ScanLevelSymbol:
vulnCount = c.VulnerabilitiesCalled
case govulncheck.ScanLevelPackage:
vulnCount = c.VulnerabilitiesImported
case govulncheck.ScanLevelModule:
vulnCount = c.VulnerabilitiesRequired
}
h.style(valueStyle, vulnCount)
h.print(choose(vulnCount == 1, ` vulnerability`, ` vulnerabilities`))
if h.scanLevel.WantSymbols() {
h.print(choose(c.ModulesCalled > 0 || c.StdlibCalled, ` from `, ``))
if c.ModulesCalled > 0 {
h.style(valueStyle, c.ModulesCalled)
h.print(choose(c.ModulesCalled == 1, ` module`, ` modules`))
}
if c.StdlibCalled {
if c.ModulesCalled != 0 {
h.print(` and `)
}
h.print(`the Go standard library`)
}
}
h.print(".\n")
// print summary for vulnerabilities found at other levels of scan precision
if other := h.summaryOtherVulns(c); other != "" {
h.wrap("", other, 80)
h.print("\n")
}
// print suggested flags for more/better info depending on scan level and if in verbose mode
if sugg := h.summarySuggestion(); sugg != "" {
h.wrap("", sugg, 80)
h.print("\n")
}
}
func (h *TextHandler) summaryOtherVulns(c summaryCounters) string {
var summary strings.Builder
if c.VulnerabilitiesRequired+c.VulnerabilitiesImported == 0 {
summary.WriteString("This scan found no other vulnerabilities in ")
if h.scanLevel.WantSymbols() {
summary.WriteString("packages you import or ")
}
summary.WriteString("modules you require.")
} else {
summary.WriteString(choose(h.scanLevel.WantPackages(), "This scan also found ", ""))
if h.scanLevel.WantSymbols() {
summary.WriteString(fmt.Sprint(c.VulnerabilitiesImported))
summary.WriteString(choose(c.VulnerabilitiesImported == 1, ` vulnerability `, ` vulnerabilities `))
summary.WriteString("in packages you import and ")
}
if h.scanLevel.WantPackages() {
summary.WriteString(fmt.Sprint(c.VulnerabilitiesRequired))
summary.WriteString(choose(c.VulnerabilitiesRequired == 1, ` vulnerability `, ` vulnerabilities `))
summary.WriteString("in modules you require")
summary.WriteString(choose(h.scanLevel.WantSymbols(), ", but your code doesn't appear to call these vulnerabilities.", "."))
}
}
return summary.String()
}
func (h *TextHandler) summarySuggestion() string {
var sugg strings.Builder
switch h.scanLevel {
case govulncheck.ScanLevelSymbol:
if !h.showAllVulns {
sugg.WriteString("Use " + verboseMessage + ".")
}
case govulncheck.ScanLevelPackage:
sugg.WriteString("Use " + symbolMessage)
if !h.showAllVulns {
sugg.WriteString(" and " + verboseMessage)
}
sugg.WriteString(".")
case govulncheck.ScanLevelModule:
sugg.WriteString("Use " + symbolMessage + ".")
}
return sugg.String()
}
func (h *TextHandler) style(style style, values ...any) {
if h.showColor {
switch style {
default:
h.print(colorReset)
case osvCalledStyle:
h.print(colorBold, fgRed)
case osvImportedStyle:
h.print(colorBold, fgGreen)
case detailsStyle:
h.print(colorFaint)
case sectionStyle:
h.print(fgBlue)
case keyStyle:
h.print(colorFaint, fgYellow)
case valueStyle:
h.print(colorBold, fgCyan)
}
}
h.print(values...)
if h.showColor && len(values) > 0 {
h.print(colorReset)
}
}
func (h *TextHandler) print(values ...any) int {
total, w := 0, 0
for _, v := range values {
if h.err != nil {
return total
}
// do we need to specialize for some types, like time?
w, h.err = fmt.Fprint(h.w, v)
total += w
}
return total
}
// wrap wraps s to fit in maxWidth by breaking it into lines at whitespace. If a
// single word is longer than maxWidth, it is retained as its own line.
func (h *TextHandler) wrap(indent string, s string, maxWidth int) {
w := 0
for _, f := range strings.Fields(s) {
if w > 0 && w+len(f)+1 > maxWidth {
// line would be too long with this word
h.print("\n")
w = 0
}
if w == 0 {
// first field on line, indent
w = h.print(indent)
} else {
// not first word, space separate
w += h.print(" ")
}
// now write the word
w += h.print(f)
}
}
func choose[t any](b bool, yes, no t) t {
if b {
return yes
}
return no
}
func isStdFindings(findings []*findingSummary) bool {
for _, f := range findings {
if vulncheck.IsStdPackage(f.Trace[0].Package) || f.Trace[0].Module == internal.GoStdModulePath {
return true
}
}
return false
}
@@ -0,0 +1,59 @@
// 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 scan
import (
"fmt"
"os"
"os/exec"
"golang.org/x/vuln/internal"
"golang.org/x/vuln/internal/govulncheck"
)
// validateFindings checks that the supplied findings all obey the protocol
// rules.
func validateFindings(findings ...*govulncheck.Finding) error {
for _, f := range findings {
if f.OSV == "" {
return fmt.Errorf("invalid finding: all findings must have an associated OSV")
}
if len(f.Trace) < 1 {
return fmt.Errorf("invalid finding: all callstacks must have at least one frame")
}
for _, frame := range f.Trace {
if frame.Version != "" && frame.Module == "" {
return fmt.Errorf("invalid finding: if Frame.Version is set, Frame.Module must also be")
}
if frame.Package != "" && frame.Module == "" {
return fmt.Errorf("invalid finding: if Frame.Package is set, Frame.Module must also be")
}
if frame.Function != "" && frame.Package == "" {
return fmt.Errorf("invalid finding: if Frame.Function is set, Frame.Package must also be")
}
}
}
return nil
}
func moduleVersionString(modulePath, version string) string {
if version == "" {
return ""
}
if modulePath == internal.GoStdModulePath || modulePath == internal.GoCmdModulePath {
version = semverToGoTag(version)
}
return version
}
func gomodExists(dir string) bool {
cmd := exec.Command("go", "env", "GOMOD")
cmd.Dir = dir
out, err := cmd.Output()
output := string(out)
// If module-aware mode is enabled, but there is no go.mod, GOMOD will be os.DevNull
// If module-aware mode is disabled, GOMOD will be the empty string.
return err == nil && !(output == os.DevNull || output == "")
}