blob: 8a6d823841dafe5a3a982c8e87f0dceaa48451ca [file] [log] [blame]
// 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 main
import (
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/google/go-cmdtest"
"golang.org/x/vuln/internal/buildtest"
"golang.org/x/vuln/osv"
"golang.org/x/vuln/vulncheck"
)
var update = flag.Bool("update", false, "update test files with results")
func TestCommand(t *testing.T) {
testDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
ts, err := cmdtest.Read("testdata")
if err != nil {
t.Fatal(err)
}
ts.DisableLogging = true
// Define a command that lets us cd into a module directory.
// The modules for these tests live under testdata/modules.
ts.Commands["cdmodule"] = func(args []string, inputFile string) ([]byte, error) {
if len(args) != 1 {
return nil, errors.New("need exactly 1 argument")
}
return nil, os.Chdir(filepath.Join(testDir, "testdata", "modules", args[0]))
}
// Define a command that runs govulncheck with our local DB. We can't use
// cmdtest.Program for this because it doesn't let us set the environment,
// and that is the only way to tell govulncheck about an alternative vuln
// database.
binary, cleanup := buildtest.GoBuild(t, ".") // build govulncheck
defer cleanup()
ts.Commands["govulncheck"] = func(args []string, inputFile string) ([]byte, error) {
cmd := exec.Command(binary, args...)
if inputFile != "" {
return nil, errors.New("input redirection makes no sense")
}
cmd.Env = append(os.Environ(), "GOVULNDB=file://"+testDir+"/testdata/vulndb")
out, err := cmd.CombinedOutput()
for _, arg := range args {
if arg == "-json" {
out = filterJSON(out)
break
}
}
return out, err
}
// Build test module binaries.
moduleDirs, err := filepath.Glob("testdata/modules/*")
if err != nil {
t.Fatal(err)
}
for _, md := range moduleDirs {
binary, cleanup := buildtest.GoBuild(t, md)
defer cleanup()
// Set an environment variable to the path to the binary, so tests
// can refer to it.
varName := filepath.Base(md) + "_binary"
os.Setenv(varName, binary)
}
ts.Run(t, *update)
}
var goFileRegexp = regexp.MustCompile(`"[^"]*\.go"`)
// filterJSON modifies paths to Go files by replacing their directory with "...".
// For example, "/a/b/c.go" becomes ".../c.go".
// This makes it possible to compare govulncheck JSON output across systems, because
// Go filenames in JSON output include setup-specific paths.
func filterJSON(data []byte) []byte {
return goFileRegexp.ReplaceAllFunc(data, func(b []byte) []byte {
return []byte(fmt.Sprintf(`".../%s"`, filepath.Base(string(b)[1:len(b)-1])))
})
}
func TestLatestFixed(t *testing.T) {
for _, test := range []struct {
name string
in []osv.Affected
want string
}{
{"empty", nil, ""},
{
"no semver",
[]osv.Affected{
{
Ranges: osv.Affects{
{
Type: osv.TypeGit,
Events: []osv.RangeEvent{
{Introduced: "v1.0.0", Fixed: "v1.2.3"},
},
}},
},
},
"",
},
{
"one",
[]osv.Affected{
{
Ranges: osv.Affects{
{
Type: osv.TypeSemver,
Events: []osv.RangeEvent{
{Introduced: "v1.0.0", Fixed: "v1.2.3"},
},
}},
},
},
"v1.2.3",
},
{
"several",
[]osv.Affected{
{
Ranges: osv.Affects{
{
Type: osv.TypeSemver,
Events: []osv.RangeEvent{
{Introduced: "v1.0.0", Fixed: "v1.2.3"},
{Introduced: "v1.5.0", Fixed: "v1.5.6"},
},
}},
},
{
Ranges: osv.Affects{
{
Type: osv.TypeSemver,
Events: []osv.RangeEvent{
{Introduced: "v1.3.0", Fixed: "v1.4.1"},
},
}},
},
},
"v1.5.6",
},
} {
t.Run(test.name, func(t *testing.T) {
got := latestFixed(test.in)
if got != test.want {
t.Errorf("got %q, want %q", got, test.want)
}
})
}
}
func TestPkgPath(t *testing.T) {
for _, test := range []struct {
in vulncheck.FuncNode
want string
}{
{
vulncheck.FuncNode{PkgPath: "math", Name: "Floor"},
"math",
},
{
vulncheck.FuncNode{RecvType: "a.com/b.T", Name: "M"},
"a.com/b",
},
{
vulncheck.FuncNode{RecvType: "*a.com/b.T", Name: "M"},
"a.com/b",
},
} {
got := pkgPath(&test.in)
if got != test.want {
t.Errorf("%+v: got %q, want %q", test.in, got, test.want)
}
}
}
func TestSummarizeCallStack(t *testing.T) {
topPkgs := map[string]bool{"t1": true, "t2": true}
vulnPkg := "v"
for _, test := range []struct {
in, want string
}{
{"a.F", ""},
{"t1.F", ""},
{"v.V", ""},
{
"t1.F v.V",
"t1.F calls v.V",
},
{
"t1.F t2.G v.V1 v.v2",
"t2.G calls v.V1",
},
{
"t1.F x.Y t2.G a.H b.I c.J v.V",
"t2.G calls a.H, which eventually calls v.V",
},
} {
in := stringToCallStack(test.in)
got := summarizeCallStack(in, topPkgs, vulnPkg)
if got != test.want {
t.Errorf("%s:\ngot %s\nwant %s", test.in, got, test.want)
}
}
}
func stringToCallStack(s string) vulncheck.CallStack {
var cs vulncheck.CallStack
for _, e := range strings.Fields(s) {
parts := strings.Split(e, ".")
cs = append(cs, vulncheck.StackEntry{
Function: &vulncheck.FuncNode{
PkgPath: parts[0],
Name: parts[1],
},
})
}
return cs
}