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,