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)
}
})