cmd/govulncheck: add hermetic tests for default mode

Add a test that runs the govulncheck binary on a module and vuln DB
that are completely controlled by this module.

- Create a tiny local vuln DB with a couple of vulns.

- In our tests, run govulncheck with the GOVULNDB env var set to
  that DB.

- Define two trivial modules that differ only in the version of
  a dependent module: one version matches a vulnerability in our DB,
  and one does not.

- Create a test that runs govulncheck on each module, and verify
  that the vulnerability is found and the output is what we expect.

Change-Id: Idc053ab3a451375f7a211b4bb24d0ec282d2aaa0
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/399234
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck/main_test.go b/cmd/govulncheck/main_test.go
index f839d1d..26ba4fc 100644
--- a/cmd/govulncheck/main_test.go
+++ b/cmd/govulncheck/main_test.go
@@ -8,7 +8,11 @@
 package main
 
 import (
+	"errors"
 	"flag"
+	"os"
+	"os/exec"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -21,14 +25,36 @@
 var update = flag.Bool("update", false, "update test files with results")
 
 func TestCommand(t *testing.T) {
-	binary, cleanup := buildtest.GoBuild(t, ".")
-	defer cleanup()
-
+	testDir, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
 	ts, err := cmdtest.Read("testdata")
 	if err != nil {
 		t.Fatal(err)
 	}
-	ts.Commands["govulncheck"] = cmdtest.Program(binary)
+	// 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")
+		return cmd.CombinedOutput()
+	}
 	ts.Run(t, *update)
 }
 
diff --git a/cmd/govulncheck/testdata/default.ct b/cmd/govulncheck/testdata/default.ct
new file mode 100644
index 0000000..4f5d37e
--- /dev/null
+++ b/cmd/govulncheck/testdata/default.ct
@@ -0,0 +1,18 @@
+# Test of default mode.
+
+# No vulnerabilities, no output.
+$ cdmodule novuln
+$ govulncheck .
+
+$ cdmodule vuln
+$ govulncheck . --> FAIL
+package:        golang.org/x/text/language
+your version:   v0.3.0
+fixed version:  v0.3.7
+sample call stacks:
+                vuln.main calls golang.org/x/text/language.Parse
+reference:      https://pkg.go.dev/vuln/GO-2021-0113
+description:    Due to improper index calculation, an incorrectly formatted
+                language tag can cause Parse to panic via an out of bounds read.
+                If Parse is used to process untrusted user inputs, this may be
+                used as a vector for a denial of service attack.
diff --git a/cmd/govulncheck/testdata/modules/novuln/go.mod b/cmd/govulncheck/testdata/modules/novuln/go.mod
new file mode 100644
index 0000000..aba8c70
--- /dev/null
+++ b/cmd/govulncheck/testdata/modules/novuln/go.mod
@@ -0,0 +1,6 @@
+module novuln
+
+go 1.18
+
+// This version does not have a vulnerability.
+require golang.org/x/text v0.3.7 // indirect
diff --git a/cmd/govulncheck/testdata/modules/novuln/go.sum b/cmd/govulncheck/testdata/modules/novuln/go.sum
new file mode 100644
index 0000000..1f78e03
--- /dev/null
+++ b/cmd/govulncheck/testdata/modules/novuln/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
diff --git a/cmd/govulncheck/testdata/modules/novuln/novuln.go b/cmd/govulncheck/testdata/modules/novuln/novuln.go
new file mode 100644
index 0000000..e479a37
--- /dev/null
+++ b/cmd/govulncheck/testdata/modules/novuln/novuln.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+	"fmt"
+
+	"golang.org/x/text/language"
+)
+
+func main() {
+	fmt.Println("hello")
+	language.Parse("")
+}
diff --git a/cmd/govulncheck/testdata/modules/vuln/go.mod b/cmd/govulncheck/testdata/modules/vuln/go.mod
new file mode 100644
index 0000000..be80f11
--- /dev/null
+++ b/cmd/govulncheck/testdata/modules/vuln/go.mod
@@ -0,0 +1,7 @@
+module vuln
+
+go 1.18
+
+// This version has a vulnerability.
+require golang.org/x/text v0.3.0
+
diff --git a/cmd/govulncheck/testdata/modules/vuln/go.sum b/cmd/govulncheck/testdata/modules/vuln/go.sum
new file mode 100644
index 0000000..62d6513
--- /dev/null
+++ b/cmd/govulncheck/testdata/modules/vuln/go.sum
@@ -0,0 +1,4 @@
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
diff --git a/cmd/govulncheck/testdata/modules/vuln/vuln.go b/cmd/govulncheck/testdata/modules/vuln/vuln.go
new file mode 100644
index 0000000..e479a37
--- /dev/null
+++ b/cmd/govulncheck/testdata/modules/vuln/vuln.go
@@ -0,0 +1,12 @@
+package main
+
+import (
+	"fmt"
+
+	"golang.org/x/text/language"
+)
+
+func main() {
+	fmt.Println("hello")
+	language.Parse("")
+}
diff --git a/cmd/govulncheck/testdata/vulndb/golang.org/x/crypto.json b/cmd/govulncheck/testdata/vulndb/golang.org/x/crypto.json
new file mode 100644
index 0000000..e0a2791
--- /dev/null
+++ b/cmd/govulncheck/testdata/vulndb/golang.org/x/crypto.json
@@ -0,0 +1 @@
+[{"id":"GO-2020-0012","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2020-9283"],"details":"An attacker can craft an ssh-ed25519 or sk-ssh-ed25519@openssh.com public\nkey, such that the library will panic when trying to verify a signature\nwith it. If verifying signatures using user supplied public keys, this\nmay be used as a denial of service vector.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20200220183623-bac4c82f6975"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0012"},"ecosystem_specific":{"symbols":["parseED25519","ed25519PublicKey.Verify","parseSKEd25519","skEd25519PublicKey.Verify","NewPublicKey"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/crypto/+/220357"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/bac4c82f69751a6dd76e702d54b3ceb88adab236"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/3L45YRc91SY"}]},{"id":"GO-2020-0013","published":"2021-04-14T20:04:52Z","modified":"2021-04-14T20:04:52Z","aliases":["CVE-2017-3204"],"details":"By default host key verification is disabled which allows for\nman-in-the-middle attacks against SSH clients if\nClientConfig.HostKeyCallback is not set.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20170330155735-e4e2799dd7aa"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0013"},"ecosystem_specific":{"symbols":["NewClientConn"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/38701"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/e4e2799dd7aab89f583e1d898300d96367750991"},{"type":"WEB","url":"https://go.dev/issue/19767"},{"type":"WEB","url":"https://bridge.grumpy-troll.org/2017/04/golang-ssh-security/"}]},{"id":"GO-2021-0227","published":"2022-02-17T17:35:32Z","modified":"2022-02-17T17:35:32Z","aliases":["CVE-2020-29652"],"details":"Clients can cause a panic in SSH servers. An attacker can craft\nan authentication request message for the “gssapi-with-mic” method\nwhich will cause NewServerConn to panic via a nil pointer dereference\nif ServerConfig.GSSAPIWithMICConfig is nil.\n","affected":[{"package":{"name":"golang.org/x/crypto/ssh","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.0.0-20201216223049-8b5274cf687f"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0227"},"ecosystem_specific":{"symbols":["connection.serverAuthenticate"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/crypto/+/278852"},{"type":"FIX","url":"https://go.googlesource.com/crypto/+/8b5274cf687fd9316b4108863654cc57385531e8"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/ouZIlBimOsE?pli=1"}]}]
\ No newline at end of file
diff --git a/cmd/govulncheck/testdata/vulndb/golang.org/x/text.json b/cmd/govulncheck/testdata/vulndb/golang.org/x/text.json
new file mode 100644
index 0000000..eee052f
--- /dev/null
+++ b/cmd/govulncheck/testdata/vulndb/golang.org/x/text.json
@@ -0,0 +1 @@
+[{"id":"GO-2020-0015","published":"2021-04-14T20:04:52Z","modified":"2021-06-07T12:00:00Z","aliases":["CVE-2020-14040"],"details":"An attacker could provide a single byte to a UTF16 decoder instantiated with\nUseBOM or ExpectBOM to trigger an infinite loop if the String function on\nthe Decoder is called, or the Decoder is passed to transform.String.\nIf used to parse user supplied input, this may be used as a denial of service\nvector.\n","affected":[{"package":{"name":"golang.org/x/text/encoding/unicode","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"symbols":["utf16Decoder.Transform","bomOverride.Transform"]}},{"package":{"name":"golang.org/x/text/transform","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.3"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2020-0015"},"ecosystem_specific":{"symbols":["Transform"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/text/+/238238"},{"type":"FIX","url":"https://go.googlesource.com/text/+/23ae387dee1f90d29a23c0e87ee0b46038fbed0e"},{"type":"WEB","url":"https://go.dev/issue/39491"},{"type":"WEB","url":"https://groups.google.com/g/golang-announce/c/bXVeAmGOqz0"}]},{"id":"GO-2021-0113","published":"2021-10-06T17:51:21Z","modified":"2021-10-06T17:51:21Z","aliases":["CVE-2021-38561"],"details":"Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n","affected":[{"package":{"name":"golang.org/x/text/language","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.7"}]}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2021-0113"},"ecosystem_specific":{"symbols":["Parse","MatchStrings","MustParse","ParseAcceptLanguage"]}}],"references":[{"type":"FIX","url":"https://go-review.googlesource.com/c/text/+/340830"},{"type":"FIX","url":"https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f"}]}]
\ No newline at end of file