internal/lsp/command: add RunVulncheckExp

This is a command that runs govulncheck-like analysis.
This is highly experimental and can change any time,
so we mark it with the "Exp" suffix. Once the interface
becomes stable, we will rename this command.

It returns VulncheckResult that can be encoded as
a JSON message. The result includes all potentially
affecting vulnerabilities, and sample traces.

This feature is currently available only when gopls
is compiled with go1.18. Otherwise, the command will
return an error.

Updates golang/go#50577
Updates golang/vscode-go#2096

Change-Id: Ia37b0555f7bf98760292c9f68e50fb70dd494522
Reviewed-on: https://go-review.googlesource.com/c/tools/+/395576
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Hana 2022-03-24 12:24:17 -04:00 committed by Hyang-Ah Hana Kim
parent 4737f45953
commit cd31eaad03
9 changed files with 169 additions and 4 deletions

View File

@ -265,6 +265,41 @@ Args:
}
```
### **Run vulncheck (experimental)**
Identifier: `gopls.run_vulncheck_exp`
Run vulnerability check (`govulncheck`).
Args:
```
{
// Dir is the directory from which vulncheck will run from.
"Dir": string,
// Package pattern. E.g. "", ".", "./...".
"Pattern": string,
}
```
Result:
```
{
"Vuln": []{
"id": string,
"details": string,
"aliases": []string,
"symbol": string,
"pkg_path": string,
"mod_path": string,
"url": string,
"current_version": string,
"fixed_version": string,
"call_stacks": [][]golang.org/x/tools/internal/lsp/command.StackEntry,
},
}
```
### **Start the gopls debug server**
Identifier: `gopls.start_debugging`

View File

@ -10,6 +10,7 @@ package hooks // import "golang.org/x/tools/gopls/internal/hooks"
import (
"context"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/internal/lsp/source"
"mvdan.cc/gofumpt/format"
"mvdan.cc/xurls/v2"
@ -28,4 +29,6 @@ func Options(options *source.Options) {
})
}
updateAnalyzers(options)
options.Govulncheck = vulncheck.Govulncheck
}

View File

@ -5,15 +5,13 @@
//go:build go1.18
// +build go1.18
// Package vulncheck provides an analysis command
// that runs vulnerability analysis using data from
// golang.org/x/exp/vulncheck.
// This package requires go1.18 or newer.
package vulncheck
import (
"context"
"fmt"
"os"
"strings"
"golang.org/x/exp/vulncheck"
"golang.org/x/tools/go/packages"
@ -21,6 +19,42 @@ import (
"golang.org/x/vuln/client"
)
func init() {
Govulncheck = govulncheck
}
func govulncheck(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) {
if args.Pattern == "" {
args.Pattern = "."
}
dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: defaultCache()})
if err != nil {
return res, err
}
c := cmd{Client: dbClient}
vulns, err := c.Run(ctx, cfg, args.Pattern)
if err != nil {
return res, err
}
res.Vuln = vulns
return res, err
}
func findGOVULNDB(cfg *packages.Config) []string {
for _, kv := range cfg.Env {
if strings.HasPrefix(kv, "GOVULNDB=") {
return strings.Split(kv[len("GOVULNDB="):], ",")
}
}
if GOVULNDB := os.Getenv("GOVULNDB"); GOVULNDB != "" {
return strings.Split(GOVULNDB, ",")
}
return []string{"https://storage.googleapis.com/go-vulndb"}
}
type Vuln = command.Vuln
type CallStack = command.CallStack
type StackEntry = command.StackEntry

View File

@ -0,0 +1,23 @@
// 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.
// Package vulncheck provides an analysis command
// that runs vulnerability analysis using data from
// golang.org/x/exp/vulncheck.
// This package requires go1.18 or newer.
package vulncheck
import (
"context"
"errors"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/command"
)
// Govulncheck runs the in-process govulncheck implementation.
// With go1.18+, this is swapped with the real implementation.
var Govulncheck = func(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) {
return res, errors.New("not implemented")
}

View File

@ -18,6 +18,7 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/command"
@ -781,3 +782,35 @@ func (c *commandHandler) StartDebugging(ctx context.Context, args command.Debugg
result.URLs = []string{"http://" + listenedAddr}
return result, nil
}
func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) (result command.VulncheckResult, _ error) {
err := c.run(ctx, commandConfig{
progress: "Running vulncheck",
requireSave: true,
forURI: args.Dir, // Will dir work?
}, func(ctx context.Context, deps commandDeps) error {
view := deps.snapshot.View()
opts := view.Options()
if opts == nil || opts.Hooks.Govulncheck == nil {
return errors.New("vulncheck feature is not available")
}
buildFlags := opts.BuildFlags // XXX: is session.Options equivalent to view.Options?
var viewEnv []string
if e := opts.EnvSlice(); e != nil {
viewEnv = append(os.Environ(), e...)
}
cfg := &packages.Config{
Context: ctx,
Tests: true, // TODO(hyangah): add a field in args.
BuildFlags: buildFlags,
Env: viewEnv,
Dir: view.Folder().Filename(),
// TODO(hyangah): configure overlay
}
var err error
result, err = opts.Hooks.Govulncheck(ctx, cfg, args)
return err
})
return result, err
}

View File

@ -33,6 +33,7 @@ const (
RegenerateCgo Command = "regenerate_cgo"
RemoveDependency Command = "remove_dependency"
RunTests Command = "run_tests"
RunVulncheckExp Command = "run_vulncheck_exp"
StartDebugging Command = "start_debugging"
Test Command = "test"
Tidy Command = "tidy"
@ -57,6 +58,7 @@ var Commands = []Command{
RegenerateCgo,
RemoveDependency,
RunTests,
RunVulncheckExp,
StartDebugging,
Test,
Tidy,
@ -152,6 +154,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
return nil, err
}
return nil, s.RunTests(ctx, a0)
case "gopls.run_vulncheck_exp":
var a0 VulncheckArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
return nil, err
}
return s.RunVulncheckExp(ctx, a0)
case "gopls.start_debugging":
var a0 DebuggingArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@ -368,6 +376,18 @@ func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error)
}, nil
}
func NewRunVulncheckExpCommand(title string, a0 VulncheckArgs) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {
return protocol.Command{}, err
}
return protocol.Command{
Title: title,
Command: "gopls.run_vulncheck_exp",
Arguments: args,
}, nil
}
func NewStartDebuggingCommand(title string, a0 DebuggingArgs) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {

View File

@ -143,6 +143,11 @@ type Interface interface {
// Start the gopls debug server if it isn't running, and return the debug
// address.
StartDebugging(context.Context, DebuggingArgs) (DebuggingResult, error)
// RunVulncheckExp: Run vulncheck (experimental)
//
// Run vulnerability check (`govulncheck`).
RunVulncheckExp(context.Context, VulncheckArgs) (VulncheckResult, error)
}
type RunTestsArgs struct {

View File

@ -665,6 +665,13 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "Runs `go test` for a specific set of test or benchmark functions.",
ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}",
},
{
Command: "gopls.run_vulncheck_exp",
Title: "Run vulncheck (experimental)",
Doc: "Run vulnerability check (`govulncheck`).",
ArgDoc: "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}",
ResultDoc: "{\n\t\"Vuln\": []{\n\t\t\"id\": string,\n\t\t\"details\": string,\n\t\t\"aliases\": []string,\n\t\t\"symbol\": string,\n\t\t\"pkg_path\": string,\n\t\t\"mod_path\": string,\n\t\t\"url\": string,\n\t\t\"current_version\": string,\n\t\t\"fixed_version\": string,\n\t\t\"call_stacks\": [][]golang.org/x/tools/internal/lsp/command.StackEntry,\n\t},\n}",
},
{
Command: "gopls.start_debugging",
Title: "Start the gopls debug server",

View File

@ -47,6 +47,7 @@ import (
"golang.org/x/tools/go/analysis/passes/unsafeptr"
"golang.org/x/tools/go/analysis/passes/unusedresult"
"golang.org/x/tools/go/analysis/passes/unusedwrite"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/analysis/fillreturns"
"golang.org/x/tools/internal/lsp/analysis/fillstruct"
"golang.org/x/tools/internal/lsp/analysis/infertypeargs"
@ -476,6 +477,9 @@ type Hooks struct {
TypeErrorAnalyzers map[string]*Analyzer
ConvenienceAnalyzers map[string]*Analyzer
StaticcheckAnalyzers map[string]*Analyzer
// Govulncheck is the implementation of the Govulncheck gopls command.
Govulncheck func(context.Context, *packages.Config, command.VulncheckArgs) (command.VulncheckResult, error)
}
// InternalOptions contains settings that are not intended for use by the
@ -703,6 +707,7 @@ func (o *Options) Clone() *Options {
ComputeEdits: o.ComputeEdits,
GofumptFormat: o.GofumptFormat,
URLRegexp: o.URLRegexp,
Govulncheck: o.Govulncheck,
},
ServerOptions: o.ServerOptions,
UserOptions: o.UserOptions,