cmd/govulncheck_compare: add govulncheck_compare binary
Adds the govulncheck_compare binary for comparing binary and source mode
scanning with govulncheck in the sandbox for ecosystem metrics.
Change-Id: I111810879f010d48a6fd8957c672a18c7bc25ee9
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/509357
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Maceo Thompson <maceothompson@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/govulncheck_compare/govulncheck_compare.go b/cmd/govulncheck_compare/govulncheck_compare.go
new file mode 100644
index 0000000..5516e6b
--- /dev/null
+++ b/cmd/govulncheck_compare/govulncheck_compare.go
@@ -0,0 +1,104 @@
+// 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.
+
+// This program finds all binaries that can be compiled in a module, then runs
+// govulncheck on the module and the subpackages that are used for the binaries,
+// as well as the binaries themselves (for comparison). It then writes the results
+// as JSON. It is intended to be run in a sandbox.
+// Unless it panics, this program always terminates with exit code 0.
+// If there is an error, it writes a JSON object with field "Error".
+// Otherwise, it writes a internal/govulncheck.CompareResponse as JSON.
+
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+
+ "golang.org/x/exp/slices"
+ "golang.org/x/pkgsite-metrics/internal/buildbinary"
+ "golang.org/x/pkgsite-metrics/internal/govulncheck"
+)
+
+// govulncheck compare accepts three inputs in the following order
+// - path to govulncheck
+// - input module to scan
+// - full path to the vulnerability database
+func main() {
+ flag.Parse()
+ run(os.Stdout, flag.Args())
+}
+
+func run(w io.Writer, args []string) {
+ fail := func(err error) {
+ fmt.Fprintf(w, `{"Error": %q}`, err)
+ fmt.Fprintln(w)
+ }
+ if len(args) != 3 {
+ fail(errors.New("need three args: govulncheck path, input module dir, full path to vuln db"))
+ return
+ }
+ govulncheckPath := args[0]
+ modulePath := args[1]
+ vulndbPath := args[2]
+
+ response := govulncheck.CompareResponse{
+ FindingsForMod: make(map[string]*govulncheck.ComparePair),
+ }
+
+ binaryPaths, err := buildbinary.FindAndBuildBinaries(modulePath)
+ if err != nil {
+ fail(err)
+ return
+ }
+ defer removeBinaries(binaryPaths)
+
+ // Sort binaryPath keys so that range is deterministic
+ keys := make([]string, 0, len(binaryPaths))
+ for k := range binaryPaths {
+ keys = append(keys, k)
+ }
+ slices.Sort(keys)
+
+ for _, binaryPath := range keys {
+ importPath := binaryPaths[binaryPath]
+ pair := govulncheck.ComparePair{
+ BinaryResults: govulncheck.SandboxResponse{Stats: govulncheck.ScanStats{}},
+ SourceResults: govulncheck.SandboxResponse{Stats: govulncheck.ScanStats{}},
+ }
+
+ pair.SourceResults.Findings, err = govulncheck.RunGovulncheckCmd(govulncheckPath, govulncheck.FlagSource, importPath, modulePath, vulndbPath, &pair.SourceResults.Stats)
+ if err != nil {
+ fail(err)
+ return
+ }
+
+ pair.BinaryResults.Findings, err = govulncheck.RunGovulncheckCmd(govulncheckPath, govulncheck.FlagBinary, binaryPath, modulePath, vulndbPath, &pair.BinaryResults.Stats)
+ if err != nil {
+ fail(err)
+ return
+ }
+
+ response.FindingsForMod[importPath] = &pair
+ }
+
+ b, err := json.MarshalIndent(response, "", "\t")
+ if err != nil {
+ fail(err)
+ return
+ }
+
+ w.Write(b)
+ fmt.Println()
+}
+
+func removeBinaries(binaryPaths map[string]string) {
+ for path := range binaryPaths {
+ os.Remove(path)
+ }
+}
diff --git a/cmd/govulncheck_compare/govulncheck_compare_test.go b/cmd/govulncheck_compare/govulncheck_compare_test.go
new file mode 100644
index 0000000..3b001c8
--- /dev/null
+++ b/cmd/govulncheck_compare/govulncheck_compare_test.go
@@ -0,0 +1,62 @@
+// 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.
+
+// Tests for the govulncheck_compare binary
+
+package main
+
+import (
+ "bytes"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "golang.org/x/pkgsite-metrics/internal/buildtest"
+ "golang.org/x/pkgsite-metrics/internal/govulncheck"
+)
+
+func Test(t *testing.T) {
+ // TODO: Modify test to ensure that govulncheck & the built binaries are all
+ // built with the same version of go. Test currently fails on cloudtop machines
+ // because go versions are different.
+ // govulncheck_compare works in integration testing, as binaries are built in
+ // the sandbox which ensures that the go versions are the same
+ t.Skip("Govulncheck fails on binaries built with Go versions 12.1+, which cloudtop is ran on")
+ if runtime.GOOS == "windows" {
+ t.Skip("cannot run on Windows")
+ }
+
+ govulncheckPath, err := buildtest.BuildGovulncheck(t.TempDir())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ testData := "../../internal/testdata"
+ module := filepath.Join(testData, "module")
+
+ // govulncheck binary requires a full path to the vuln db. Otherwise, one
+ // gets "[file://testdata/vulndb], opts): file URL specifies non-local host."
+ vulndb, err := filepath.Abs(filepath.Join(testData, "vulndb"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Run("basicComparison", func(t *testing.T) {
+ resp, err := runTest([]string{govulncheckPath, module, vulndb})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pair := resp.FindingsForMod["golang.org/vuln"]
+ t.Log(pair)
+ // TODO: concretely test that the results are as expected.
+ })
+
+}
+
+func runTest(args []string) (*govulncheck.CompareResponse, error) {
+ var buf bytes.Buffer
+ run(&buf, args)
+ return govulncheck.UnmarshalCompareResponse(buf.Bytes())
+}
diff --git a/cmd/worker/Dockerfile b/cmd/worker/Dockerfile
index 7a70bc1..9dfec2b 100644
--- a/cmd/worker/Dockerfile
+++ b/cmd/worker/Dockerfile
@@ -95,6 +95,9 @@
# Build the program that runs govulncheck inside the sandbox.
RUN go build -mod=readonly -o $BINARY_DIR/govulncheck_sandbox ./cmd/govulncheck_sandbox
+# Build the program that runs govulncheck comparisons inside the sandbox
+RUN go build -mod=readonly -o $BINARY_DIR/govulncheck_compare ./cmd/govulncheck_compare
+
# Build the sandbox runner program and put it in the bundle root.
RUN go build -mod=readonly -o /bundle/rootfs/runner ./internal/sandbox/runner.go
diff --git a/internal/govulncheck/govulncheck.go b/internal/govulncheck/govulncheck.go
index 9397f6f..142a054 100644
--- a/internal/govulncheck/govulncheck.go
+++ b/internal/govulncheck/govulncheck.go
@@ -272,6 +272,31 @@
return &res, nil
}
+type CompareResponse struct {
+ // Map from package import path to pair of binary & source mode findings
+ FindingsForMod map[string]*ComparePair
+}
+
+type ComparePair struct {
+ BinaryResults SandboxResponse
+ SourceResults SandboxResponse
+}
+
+func UnmarshalCompareResponse(output []byte) (*CompareResponse, error) {
+ var e struct{ Error string }
+ if err := json.Unmarshal(output, &e); err != nil {
+ return nil, err
+ }
+ if e.Error != "" {
+ return nil, errors.New(e.Error)
+ }
+ var res CompareResponse
+ if err := json.Unmarshal(output, &res); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
+
func RunGovulncheckCmd(govulncheckPath, modeFlag, pattern, moduleDir, vulndbDir string, stats *ScanStats) ([]*govulncheckapi.Finding, error) {
stdOut := bytes.Buffer{}
stdErr := bytes.Buffer{}