From cd31eaad03d53a18fa4b5e25b94d0b65e7380349 Mon Sep 17 00:00:00 2001 From: Hana Date: Thu, 24 Mar 2022 12:24:17 -0400 Subject: [PATCH] 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 Run-TryBot: Hyang-Ah Hana Kim Reviewed-by: Jonathan Amsterdam gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/doc/commands.md | 35 ++++++++++++++++++++++ gopls/internal/hooks/hooks.go | 3 ++ gopls/internal/vulncheck/command.go | 42 ++++++++++++++++++++++++--- gopls/internal/vulncheck/vulncheck.go | 23 +++++++++++++++ internal/lsp/command.go | 33 +++++++++++++++++++++ internal/lsp/command/command_gen.go | 20 +++++++++++++ internal/lsp/command/interface.go | 5 ++++ internal/lsp/source/api_json.go | 7 +++++ internal/lsp/source/options.go | 5 ++++ 9 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 gopls/internal/vulncheck/vulncheck.go diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 37a4ca4108..b4b16ddd8a 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -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` diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index db554f5fc1..023aefeab9 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -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 } diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index efc2ceb8af..a367b0685c 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -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 diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go new file mode 100644 index 0000000000..198f9745b3 --- /dev/null +++ b/gopls/internal/vulncheck/vulncheck.go @@ -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") +} diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 6f491f75b4..de8d88c211 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -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 +} diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go index 55696936ba..22cfeff5ba 100644 --- a/internal/lsp/command/command_gen.go +++ b/internal/lsp/command/command_gen.go @@ -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 { diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go index cd060fdfa3..8985adb8d5 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -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 { diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 5e7b440246..1db1700234 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -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", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index b3b3ef2095..319f77e6d9 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -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,