internal/worker: add logic to run govulncheck in insecure mode

Change-Id: I238bd12b012f99ff0b752db03b30afdbc18109d3
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/470895
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Run-TryBot: Maceo Thompson <maceothompson@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/worker/vulncheck_enqueue_test.go b/internal/worker/vulncheck_enqueue_test.go
index 8707d06..ad17ea3 100644
--- a/internal/worker/vulncheck_enqueue_test.go
+++ b/internal/worker/vulncheck_enqueue_test.go
@@ -80,7 +80,7 @@
 	}
 	wantTasks = nil
 	// cfg.BinaryBucket is empty, so no binary-mode tasks are created.
-	for _, mode := range []string{ModeImports, ModeVTA, ModeVTAStacks} {
+	for _, mode := range []string{ModeGovulncheck, ModeImports, ModeVTA, ModeVTAStacks} {
 		wantTasks = append(wantTasks,
 			vreq("github.com/pkg/errors", "v0.9.1", mode, 10),
 			vreq("golang.org/x/net", "v0.4.0", mode, 20))
diff --git a/internal/worker/vulncheck_scan.go b/internal/worker/vulncheck_scan.go
index 066446b..2d1cbc1 100644
--- a/internal/worker/vulncheck_scan.go
+++ b/internal/worker/vulncheck_scan.go
@@ -52,14 +52,18 @@
 
 	// ModeBinary runs vulncheck.Binary
 	ModeBinary string = "BINARY"
+
+	// Modegovulncheck runs the govulncheck binary
+	ModeGovulncheck = "GOVULNCHECK"
 )
 
 // modes is a set of supported vulncheck modes
 var modes = map[string]bool{
-	ModeImports:   true,
-	ModeVTA:       true,
-	ModeVTAStacks: true,
-	ModeBinary:    true,
+	ModeImports:     true,
+	ModeVTA:         true,
+	ModeVTAStacks:   true,
+	ModeBinary:      true,
+	ModeGovulncheck: true,
 }
 
 func IsValidVulncheckMode(mode string) bool {
@@ -211,6 +215,12 @@
 	row.PkgsMemory = int64(stats.pkgsMemory)
 	row.Workers = config.GetEnvInt("CLOUD_RUN_CONCURRENCY", "0", -1)
 	if err != nil {
+		// If an error occurred, wrap it accordingly
+		if isVulnDBConnection(err) {
+			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleVulncheckDBConnectionError)
+		} else if !errors.Is(err, derrors.ScanModuleMemoryLimitExceeded) {
+			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleVulncheckError)
+		}
 		row.AddError(err)
 		log.Infof(ctx, "scanner.runScanModule return error for %s (%v)", sreq.Path(), err)
 	} else {
@@ -269,24 +279,33 @@
 		}
 	}()
 
-	var vulns []*vulncheck.Vuln
-	if s.insecure {
-		vulns, err = s.runScanModuleInsecure(ctx, modulePath, version, binaryDir, mode, stats)
-	} else {
-		vulns, err = s.runScanModuleSandbox(ctx, modulePath, version, binaryDir, mode, stats)
-	}
-
-	// If an error occurred, wrap it accordingly.
-	if err != nil {
-		if isVulnDBConnection(err) {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleVulncheckDBConnectionError)
-		} else if !errors.Is(err, derrors.ScanModuleMemoryLimitExceeded) {
-			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleVulncheckError)
+	if mode != ModeGovulncheck {
+		var vulns []*vulncheck.Vuln
+		if s.insecure {
+			vulns, err = s.runScanModuleInsecure(ctx, modulePath, version, binaryDir, mode, stats)
+		} else {
+			vulns, err = s.runScanModuleSandbox(ctx, modulePath, version, binaryDir, mode, stats)
 		}
-		return nil, err
-	}
-	for _, v := range vulns {
-		bvulns = append(bvulns, convertVuln(v))
+
+		if err != nil {
+			return nil, err
+		}
+		for _, v := range vulns {
+			bvulns = append(bvulns, convertVuln(v))
+		}
+	} else { // Govulncheck mode
+		var vulns []*govulncheck.Vuln
+		if s.insecure {
+			vulns, err = s.runGoVulncheckScanInsecure(ctx, modulePath, version, stats)
+		} else {
+			return nil, errors.New("Govulncheck scan is currently unsupported in sandbox mode")
+		}
+		if err != nil {
+			return nil, err
+		}
+		for _, v := range vulns {
+			bvulns = append(bvulns, convertGovulncheckOutput(v)...)
+		}
 	}
 	return bvulns, nil
 }
@@ -398,6 +417,64 @@
 	return &res, nil
 }
 
+func unmarshalGovulncheckOutput(output []byte) (*govulncheck.Result, 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 govulncheck.Result
+	if err := json.Unmarshal(output, &res); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
+func (s *scanner) runGoVulncheckScanInsecure(ctx context.Context, modulePath, version string, stats *vulncheckStats) (_ []*govulncheck.Vuln, err error) {
+	tempDir, err := os.MkdirTemp("", "runGoVulncheckScan")
+	if err != nil {
+		return nil, err
+	}
+
+	defer func() {
+		err1 := os.RemoveAll(tempDir)
+		if err == nil {
+			err = err1
+		}
+	}()
+
+	log.Debugf(ctx, "fetching module zip: %s@%s", modulePath, version)
+	if err := modules.Download(ctx, modulePath, version, tempDir, s.proxyClient, true); err != nil {
+		return nil, err
+	}
+	start := time.Now()
+	vulns, err := runGovulncheckCmd(ctx, modulePath, tempDir, stats)
+	if err != nil {
+		return nil, err
+	}
+	stats.scanSeconds = time.Since(start).Seconds()
+
+	return vulns, nil
+}
+
+func runGovulncheckCmd(ctx context.Context, modulePath, tempDir string, stats *vulncheckStats) ([]*govulncheck.Vuln, error) {
+	govulncheckCmd := exec.Command("govulncheck", "-json", "./...")
+	govulncheckCmd.Dir = tempDir
+	output, err := govulncheckCmd.Output()
+	if err != nil {
+		return nil, err
+	}
+	res, err := unmarshalGovulncheckOutput(output)
+	if err != nil {
+		return nil, err
+	}
+	return res.Vulns, nil
+}
+
 func (s *scanner) runScanModuleInsecure(ctx context.Context, modulePath, version, binaryDir, mode string, stats *vulncheckStats) (_ []*vulncheck.Vuln, err error) {
 	tempDir, err := os.MkdirTemp("", "runScanModule")
 	if err != nil {
@@ -623,7 +700,7 @@
 	}
 }
 
-func convertGoVulncheckOutput(v *govulncheck.Vuln) (vulns []*bigquery.Vuln) {
+func convertGovulncheckOutput(v *govulncheck.Vuln) (vulns []*bigquery.Vuln) {
 	for _, module := range v.Modules {
 		for pkgNum, pkg := range module.Packages {
 			addedSymbols := make(map[string]bool)
diff --git a/internal/worker/vulncheck_scan_test.go b/internal/worker/vulncheck_scan_test.go
index 794fcc7..90603a5 100644
--- a/internal/worker/vulncheck_scan_test.go
+++ b/internal/worker/vulncheck_scan_test.go
@@ -107,6 +107,30 @@
 			t.Errorf("got %d vulns, want %d", g, w)
 		}
 	})
+	t.Run("govulncheck", func(t *testing.T) {
+		s := &scanner{proxyClient: proxyClient, dbClient: dbClient, insecure: true}
+		stats := &vulncheckStats{}
+		vulns, err := s.runScanModule(ctx,
+			"golang.org/x/exp/event", "v0.0.0-20220929112958-4a82f8963a65",
+			"", ModeGovulncheck, stats)
+		if err != nil {
+			t.Fatal(err)
+		}
+		wantID := "GO-2022-0493"
+		found := false
+		for _, v := range vulns {
+			if v.ID == wantID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			t.Errorf("want %s, did not find it in %d vulns", wantID, len(vulns))
+		}
+		if got := stats.scanSeconds; got <= 0 {
+			t.Errorf("scan time not collected or negative: %v", got)
+		}
+	})
 }
 
 func TestParseGoMemLimit(t *testing.T) {
@@ -149,7 +173,7 @@
 	}
 }
 
-func TestConvertGoVulncheckOutput(t *testing.T) {
+func TestConvertGovulncheckOutput(t *testing.T) {
 	var (
 		osvEntry = &osv.Entry{
 			ID: "GO-YYYY-1234",
@@ -265,7 +289,7 @@
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if diff := cmp.Diff(convertGoVulncheckOutput(tt.vuln), tt.wantVulns, cmpopts.EquateEmpty()); diff != "" {
+			if diff := cmp.Diff(convertGovulncheckOutput(tt.vuln), tt.wantVulns, cmpopts.EquateEmpty()); diff != "" {
 				t.Errorf("mismatch (-got, +want): %s", diff)
 			}
 		})