golang.org/x/tools/gopls@v0.15.3/internal/test/integration/misc/vuln_test.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.18
     6  // +build go1.18
     7  
     8  package misc
     9  
    10  import (
    11  	"context"
    12  	"encoding/json"
    13  	"sort"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/google/go-cmp/cmp"
    19  
    20  	"golang.org/x/tools/gopls/internal/cache"
    21  	"golang.org/x/tools/gopls/internal/protocol"
    22  	"golang.org/x/tools/gopls/internal/protocol/command"
    23  	"golang.org/x/tools/gopls/internal/test/compare"
    24  	. "golang.org/x/tools/gopls/internal/test/integration"
    25  	"golang.org/x/tools/gopls/internal/vulncheck"
    26  	"golang.org/x/tools/gopls/internal/vulncheck/vulntest"
    27  )
    28  
    29  func TestRunGovulncheckError(t *testing.T) {
    30  	const files = `
    31  -- go.mod --
    32  module mod.com
    33  
    34  go 1.12
    35  -- foo.go --
    36  package foo
    37  `
    38  	Run(t, files, func(t *testing.T, env *Env) {
    39  		cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{
    40  			URI: "/invalid/file/url", // invalid arg
    41  		})
    42  		if err != nil {
    43  			t.Fatal(err)
    44  		}
    45  
    46  		params := &protocol.ExecuteCommandParams{
    47  			Command:   command.RunGovulncheck.ID(),
    48  			Arguments: cmd.Arguments,
    49  		}
    50  
    51  		response, err := env.Editor.ExecuteCommand(env.Ctx, params)
    52  		// We want an error!
    53  		if err == nil {
    54  			t.Errorf("got success, want invalid file URL error: %v", response)
    55  		}
    56  	})
    57  }
    58  
    59  func TestRunGovulncheckError2(t *testing.T) {
    60  	const files = `
    61  -- go.mod --
    62  module mod.com
    63  
    64  go 1.12
    65  -- foo.go --
    66  package foo
    67  
    68  func F() { // build error incomplete
    69  `
    70  	WithOptions(
    71  		EnvVars{
    72  			"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
    73  		},
    74  		Settings{
    75  			"codelenses": map[string]bool{
    76  				"run_govulncheck": true,
    77  			},
    78  		},
    79  	).Run(t, files, func(t *testing.T, env *Env) {
    80  		env.OpenFile("go.mod")
    81  		var result command.RunVulncheckResult
    82  		env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
    83  		var ws WorkStatus
    84  		env.Await(
    85  			CompletedProgress(result.Token, &ws),
    86  		)
    87  		wantEndMsg, wantMsgPart := "failed", "There are errors with the provided package patterns:"
    88  		if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) {
    89  			t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart)
    90  		}
    91  	})
    92  }
    93  
    94  const vulnsData = `
    95  -- GO-2022-01.yaml --
    96  modules:
    97    - module: golang.org/amod
    98      versions:
    99        - introduced: 1.0.0
   100        - fixed: 1.0.4
   101      packages:
   102        - package: golang.org/amod/avuln
   103          symbols:
   104            - VulnData.Vuln1
   105            - VulnData.Vuln2
   106  description: >
   107      vuln in amod is found
   108  summary: vuln in amod
   109  references:
   110    - href: pkg.go.dev/vuln/GO-2022-01
   111  -- GO-2022-03.yaml --
   112  modules:
   113    - module: golang.org/amod
   114      versions:
   115        - introduced: 1.0.0
   116        - fixed: 1.0.6
   117      packages:
   118        - package: golang.org/amod/avuln
   119          symbols:
   120            - nonExisting
   121  description: >
   122    unaffecting vulnerability is found
   123  summary: unaffecting vulnerability
   124  -- GO-2022-02.yaml --
   125  modules:
   126    - module: golang.org/bmod
   127      packages:
   128        - package: golang.org/bmod/bvuln
   129          symbols:
   130            - Vuln
   131  description: |
   132      vuln in bmod is found.
   133      
   134      This is a long description
   135      of this vulnerability.
   136  summary: vuln in bmod (no fix)
   137  references:
   138    - href: pkg.go.dev/vuln/GO-2022-03
   139  -- GO-2022-04.yaml --
   140  modules:
   141    - module: golang.org/bmod
   142      packages:
   143        - package: golang.org/bmod/unused
   144          symbols:
   145            - Vuln
   146  description: |
   147      vuln in bmod/somethingelse is found
   148  summary: vuln in bmod/somethingelse
   149  references:
   150    - href: pkg.go.dev/vuln/GO-2022-04
   151  -- GOSTDLIB.yaml --
   152  modules:
   153    - module: stdlib
   154      versions:
   155        - introduced: 1.18.0
   156      packages:
   157        - package: archive/zip
   158          symbols:
   159            - OpenReader
   160  summary: vuln in GOSTDLIB
   161  references:
   162    - href: pkg.go.dev/vuln/GOSTDLIB
   163  `
   164  
   165  func TestRunGovulncheckStd(t *testing.T) {
   166  	const files = `
   167  -- go.mod --
   168  module mod.com
   169  
   170  go 1.18
   171  -- main.go --
   172  package main
   173  
   174  import (
   175          "archive/zip"
   176          "fmt"
   177  )
   178  
   179  func main() {
   180          _, err := zip.OpenReader("file.zip")  // vulnerability id: GOSTDLIB
   181          fmt.Println(err)
   182  }
   183  `
   184  
   185  	db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  	defer db.Clean()
   190  	WithOptions(
   191  		EnvVars{
   192  			// Let the analyzer read vulnerabilities data from the testdata/vulndb.
   193  			"GOVULNDB": db.URI(),
   194  			// When fetchinging stdlib package vulnerability info,
   195  			// behave as if our go version is go1.18 for this testing.
   196  			// The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
   197  			cache.GoVersionForVulnTest:        "go1.18",
   198  			"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
   199  		},
   200  		Settings{
   201  			"codelenses": map[string]bool{
   202  				"run_govulncheck": true,
   203  			},
   204  		},
   205  	).Run(t, files, func(t *testing.T, env *Env) {
   206  		env.OpenFile("go.mod")
   207  
   208  		// Run Command included in the codelens.
   209  		var result command.RunVulncheckResult
   210  		env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
   211  
   212  		env.OnceMet(
   213  			CompletedProgress(result.Token, nil),
   214  			ShownMessage("Found GOSTDLIB"),
   215  			NoDiagnostics(ForFile("go.mod")),
   216  		)
   217  		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
   218  			"go.mod": {IDs: []string{"GOSTDLIB"}, Mode: vulncheck.ModeGovulncheck}})
   219  	})
   220  }
   221  func TestFetchVulncheckResultStd(t *testing.T) {
   222  	const files = `
   223  -- go.mod --
   224  module mod.com
   225  
   226  go 1.18
   227  -- main.go --
   228  package main
   229  
   230  import (
   231          "archive/zip"
   232          "fmt"
   233  )
   234  
   235  func main() {
   236          _, err := zip.OpenReader("file.zip")  // vulnerability id: GOSTDLIB
   237          fmt.Println(err)
   238  }
   239  `
   240  
   241  	db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
   242  	if err != nil {
   243  		t.Fatal(err)
   244  	}
   245  	defer db.Clean()
   246  	WithOptions(
   247  		EnvVars{
   248  			// Let the analyzer read vulnerabilities data from the testdata/vulndb.
   249  			"GOVULNDB": db.URI(),
   250  			// When fetchinging stdlib package vulnerability info,
   251  			// behave as if our go version is go1.18 for this testing.
   252  			cache.GoVersionForVulnTest:        "go1.18",
   253  			"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
   254  		},
   255  		Settings{"ui.diagnostic.vulncheck": "Imports"},
   256  	).Run(t, files, func(t *testing.T, env *Env) {
   257  		env.OpenFile("go.mod")
   258  		env.AfterChange(
   259  			NoDiagnostics(ForFile("go.mod")),
   260  			// we don't publish diagnostics for standard library vulnerability yet.
   261  		)
   262  		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
   263  			"go.mod": {
   264  				IDs:  []string{"GOSTDLIB"},
   265  				Mode: vulncheck.ModeImports,
   266  			},
   267  		})
   268  	})
   269  }
   270  
   271  type fetchVulncheckResult struct {
   272  	IDs  []string
   273  	Mode vulncheck.AnalysisMode
   274  }
   275  
   276  func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) {
   277  	t.Helper()
   278  
   279  	var result map[protocol.DocumentURI]*vulncheck.Result
   280  	fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{
   281  		URI: env.Sandbox.Workdir.URI("go.mod"),
   282  	})
   283  	if err != nil {
   284  		t.Fatal(err)
   285  	}
   286  	env.ExecuteCommand(&protocol.ExecuteCommandParams{
   287  		Command:   fetchCmd.Command,
   288  		Arguments: fetchCmd.Arguments,
   289  	}, &result)
   290  
   291  	for _, v := range want {
   292  		sort.Strings(v.IDs)
   293  	}
   294  	got := map[string]fetchVulncheckResult{}
   295  	for k, r := range result {
   296  		osv := map[string]bool{}
   297  		for _, v := range r.Findings {
   298  			osv[v.OSV] = true
   299  		}
   300  		ids := make([]string, 0, len(osv))
   301  		for id := range osv {
   302  			ids = append(ids, id)
   303  		}
   304  		sort.Strings(ids)
   305  		modfile := env.Sandbox.Workdir.RelPath(k.Path())
   306  		got[modfile] = fetchVulncheckResult{
   307  			IDs:  ids,
   308  			Mode: r.Mode,
   309  		}
   310  	}
   311  	if diff := cmp.Diff(want, got); diff != "" {
   312  		t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff)
   313  	}
   314  }
   315  
   316  const workspace1 = `
   317  -- go.mod --
   318  module golang.org/entry
   319  
   320  go 1.18
   321  
   322  require golang.org/cmod v1.1.3
   323  
   324  require (
   325  	golang.org/amod v1.0.0 // indirect
   326  	golang.org/bmod v0.5.0 // indirect
   327  )
   328  -- go.sum --
   329  golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k=
   330  golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E=
   331  golang.org/bmod v0.5.0 h1:KgvUulMyMiYRB7suKA0x+DfWRVdeyPgVJvcishTH+ng=
   332  golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c=
   333  golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ=
   334  golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA=
   335  -- x/x.go --
   336  package x
   337  
   338  import 	(
   339     "golang.org/cmod/c"
   340     "golang.org/entry/y"
   341  )
   342  
   343  func X() {
   344  	c.C1().Vuln1() // vuln use: X -> Vuln1
   345  }
   346  
   347  func CallY() {
   348  	y.Y()  // vuln use: CallY -> y.Y -> bvuln.Vuln 
   349  }
   350  
   351  -- y/y.go --
   352  package y
   353  
   354  import "golang.org/cmod/c"
   355  
   356  func Y() {
   357  	c.C2()() // vuln use: Y -> bvuln.Vuln
   358  }
   359  `
   360  
   361  // cmod/c imports amod/avuln and bmod/bvuln.
   362  const proxy1 = `
   363  -- golang.org/cmod@v1.1.3/go.mod --
   364  module golang.org/cmod
   365  
   366  go 1.12
   367  -- golang.org/cmod@v1.1.3/c/c.go --
   368  package c
   369  
   370  import (
   371  	"golang.org/amod/avuln"
   372  	"golang.org/bmod/bvuln"
   373  )
   374  
   375  type I interface {
   376  	Vuln1()
   377  }
   378  
   379  func C1() I {
   380  	v := avuln.VulnData{}
   381  	v.Vuln2() // vuln use
   382  	return v
   383  }
   384  
   385  func C2() func() {
   386  	return bvuln.Vuln
   387  }
   388  -- golang.org/amod@v1.0.0/go.mod --
   389  module golang.org/amod
   390  
   391  go 1.14
   392  -- golang.org/amod@v1.0.0/avuln/avuln.go --
   393  package avuln
   394  
   395  type VulnData struct {}
   396  func (v VulnData) Vuln1() {}
   397  func (v VulnData) Vuln2() {}
   398  -- golang.org/amod@v1.0.4/go.mod --
   399  module golang.org/amod
   400  
   401  go 1.14
   402  -- golang.org/amod@v1.0.4/avuln/avuln.go --
   403  package avuln
   404  
   405  type VulnData struct {}
   406  func (v VulnData) Vuln1() {}
   407  func (v VulnData) Vuln2() {}
   408  
   409  -- golang.org/bmod@v0.5.0/go.mod --
   410  module golang.org/bmod
   411  
   412  go 1.14
   413  -- golang.org/bmod@v0.5.0/bvuln/bvuln.go --
   414  package bvuln
   415  
   416  func Vuln() {
   417  	// something evil
   418  }
   419  -- golang.org/bmod@v0.5.0/unused/unused.go --
   420  package unused
   421  
   422  func Vuln() {
   423  	// something evil
   424  }
   425  -- golang.org/amod@v1.0.6/go.mod --
   426  module golang.org/amod
   427  
   428  go 1.14
   429  -- golang.org/amod@v1.0.6/avuln/avuln.go --
   430  package avuln
   431  
   432  type VulnData struct {}
   433  func (v VulnData) Vuln1() {}
   434  func (v VulnData) Vuln2() {}
   435  `
   436  
   437  func vulnTestEnv(proxyData string) (*vulntest.DB, []RunOption, error) {
   438  	db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
   439  	if err != nil {
   440  		return nil, nil, nil
   441  	}
   442  	settings := Settings{
   443  		"codelenses": map[string]bool{
   444  			"run_govulncheck": true,
   445  		},
   446  	}
   447  	ev := EnvVars{
   448  		// Let the analyzer read vulnerabilities data from the testdata/vulndb.
   449  		"GOVULNDB": db.URI(),
   450  		// When fetching stdlib package vulnerability info,
   451  		// behave as if our go version is go1.18 for this testing.
   452  		// The default behavior is to run `go env GOVERSION` (which isn't mutable env var).
   453  		cache.GoVersionForVulnTest:        "go1.18",
   454  		"_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`.
   455  		"GOSUMDB":                         "off",
   456  	}
   457  	return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil
   458  }
   459  
   460  func TestRunVulncheckPackageDiagnostics(t *testing.T) {
   461  	db, opts0, err := vulnTestEnv(proxy1)
   462  	if err != nil {
   463  		t.Fatal(err)
   464  	}
   465  	defer db.Clean()
   466  
   467  	checkVulncheckDiagnostics := func(env *Env, t *testing.T) {
   468  		env.OpenFile("go.mod")
   469  
   470  		gotDiagnostics := &protocol.PublishDiagnosticsParams{}
   471  		env.AfterChange(
   472  			Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
   473  			ReadDiagnostics("go.mod", gotDiagnostics),
   474  		)
   475  
   476  		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
   477  			"go.mod": {
   478  				IDs:  []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"},
   479  				Mode: vulncheck.ModeImports,
   480  			},
   481  		})
   482  
   483  		wantVulncheckDiagnostics := map[string]vulnDiagExpectation{
   484  			"golang.org/amod": {
   485  				diagnostics: []vulnDiag{
   486  					{
   487  						msg:      "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.",
   488  						severity: protocol.SeverityInformation,
   489  						source:   string(cache.Vulncheck),
   490  						codeActions: []string{
   491  							"Run govulncheck to verify",
   492  							"Upgrade to v1.0.6",
   493  							"Upgrade to latest",
   494  						},
   495  					},
   496  				},
   497  				codeActions: []string{
   498  					"Run govulncheck to verify",
   499  					"Upgrade to v1.0.6",
   500  					"Upgrade to latest",
   501  				},
   502  				hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"},
   503  			},
   504  			"golang.org/bmod": {
   505  				diagnostics: []vulnDiag{
   506  					{
   507  						msg:      "golang.org/bmod has a vulnerability GO-2022-02.",
   508  						severity: protocol.SeverityInformation,
   509  						source:   string(cache.Vulncheck),
   510  						codeActions: []string{
   511  							"Run govulncheck to verify",
   512  						},
   513  					},
   514  				},
   515  				codeActions: []string{
   516  					"Run govulncheck to verify",
   517  				},
   518  				hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."},
   519  			},
   520  		}
   521  
   522  		for pattern, want := range wantVulncheckDiagnostics {
   523  			modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics)
   524  
   525  			gotActions := env.CodeAction("go.mod", modPathDiagnostics)
   526  			if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
   527  				t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff)
   528  				continue
   529  			}
   530  		}
   531  	}
   532  
   533  	wantNoVulncheckDiagnostics := func(env *Env, t *testing.T) {
   534  		env.OpenFile("go.mod")
   535  
   536  		gotDiagnostics := &protocol.PublishDiagnosticsParams{}
   537  		env.AfterChange(
   538  			ReadDiagnostics("go.mod", gotDiagnostics),
   539  		)
   540  
   541  		if len(gotDiagnostics.Diagnostics) > 0 {
   542  			t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics))
   543  		}
   544  		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{})
   545  	}
   546  
   547  	for _, tc := range []struct {
   548  		name            string
   549  		setting         Settings
   550  		wantDiagnostics bool
   551  	}{
   552  		{"imports", Settings{"ui.diagnostic.vulncheck": "Imports"}, true},
   553  		{"default", Settings{}, false},
   554  		{"invalid", Settings{"ui.diagnostic.vulncheck": "invalid"}, false},
   555  	} {
   556  		t.Run(tc.name, func(t *testing.T) {
   557  			// override the settings options to enable diagnostics
   558  			opts := append(opts0, tc.setting)
   559  			WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) {
   560  				// TODO(hyangah): implement it, so we see GO-2022-01, GO-2022-02, and GO-2022-03.
   561  				// Check that the actions we get when including all diagnostics at a location return the same result
   562  				if tc.wantDiagnostics {
   563  					checkVulncheckDiagnostics(env, t)
   564  				} else {
   565  					wantNoVulncheckDiagnostics(env, t)
   566  				}
   567  
   568  				if tc.name == "imports" && tc.wantDiagnostics {
   569  					// test we get only govulncheck-based diagnostics after "run govulncheck".
   570  					var result command.RunVulncheckResult
   571  					env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
   572  					gotDiagnostics := &protocol.PublishDiagnosticsParams{}
   573  					env.OnceMet(
   574  						CompletedProgress(result.Token, nil),
   575  						ShownMessage("Found"),
   576  					)
   577  					env.OnceMet(
   578  						Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
   579  						ReadDiagnostics("go.mod", gotDiagnostics),
   580  					)
   581  					// We expect only one diagnostic for GO-2022-02.
   582  					count := 0
   583  					for _, diag := range gotDiagnostics.Diagnostics {
   584  						if strings.Contains(diag.Message, "GO-2022-02") {
   585  							count++
   586  							if got, want := diag.Severity, protocol.SeverityWarning; got != want {
   587  								t.Errorf("Diagnostic for GO-2022-02 = %v, want %v", got, want)
   588  							}
   589  						}
   590  					}
   591  					if count != 1 {
   592  						t.Errorf("Unexpected number of diagnostics about GO-2022-02 = %v, want 1:\n%+v", count, stringify(gotDiagnostics))
   593  					}
   594  				}
   595  			})
   596  		})
   597  	}
   598  }
   599  
   600  // TestRunGovulncheck_Expiry checks that govulncheck results expire after a
   601  // certain amount of time.
   602  func TestRunGovulncheck_Expiry(t *testing.T) {
   603  	// For this test, set the max age to a duration smaller than the sleep below.
   604  	defer func(prev time.Duration) {
   605  		cache.MaxGovulncheckResultAge = prev
   606  	}(cache.MaxGovulncheckResultAge)
   607  	cache.MaxGovulncheckResultAge = 99 * time.Millisecond
   608  
   609  	db, opts0, err := vulnTestEnv(proxy1)
   610  	if err != nil {
   611  		t.Fatal(err)
   612  	}
   613  	defer db.Clean()
   614  
   615  	WithOptions(opts0...).Run(t, workspace1, func(t *testing.T, env *Env) {
   616  		env.OpenFile("go.mod")
   617  		env.OpenFile("x/x.go")
   618  
   619  		var result command.RunVulncheckResult
   620  		env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
   621  		env.OnceMet(
   622  			CompletedProgress(result.Token, nil),
   623  			ShownMessage("Found"),
   624  		)
   625  		// Sleep long enough for the results to expire.
   626  		time.Sleep(100 * time.Millisecond)
   627  		// Make an arbitrary edit to force re-diagnosis of the workspace.
   628  		env.RegexpReplace("x/x.go", "package x", "package x ")
   629  		env.AfterChange(
   630  			NoDiagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
   631  		)
   632  	})
   633  }
   634  
   635  func stringify(a interface{}) string {
   636  	data, _ := json.Marshal(a)
   637  	return string(data)
   638  }
   639  
   640  func TestRunVulncheckWarning(t *testing.T) {
   641  	db, opts, err := vulnTestEnv(proxy1)
   642  	if err != nil {
   643  		t.Fatal(err)
   644  	}
   645  	defer db.Clean()
   646  	WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) {
   647  		env.OpenFile("go.mod")
   648  
   649  		var result command.RunVulncheckResult
   650  		env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
   651  		gotDiagnostics := &protocol.PublishDiagnosticsParams{}
   652  		env.OnceMet(
   653  			CompletedProgress(result.Token, nil),
   654  			ShownMessage("Found"),
   655  		)
   656  		// Vulncheck diagnostics asynchronous to the vulncheck command.
   657  		env.OnceMet(
   658  			Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)),
   659  			ReadDiagnostics("go.mod", gotDiagnostics),
   660  		)
   661  
   662  		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{
   663  			"go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: vulncheck.ModeGovulncheck},
   664  		})
   665  		env.OpenFile("x/x.go")
   666  		env.OpenFile("y/y.go")
   667  		wantDiagnostics := map[string]vulnDiagExpectation{
   668  			"golang.org/amod": {
   669  				applyAction: "Upgrade to v1.0.6",
   670  				diagnostics: []vulnDiag{
   671  					{
   672  						msg:      "golang.org/amod has a vulnerability used in the code: GO-2022-01.",
   673  						severity: protocol.SeverityWarning,
   674  						source:   string(cache.Govulncheck),
   675  						codeActions: []string{
   676  							"Upgrade to v1.0.4",
   677  							"Upgrade to latest",
   678  							"Reset govulncheck result",
   679  						},
   680  					},
   681  					{
   682  						msg:      "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.",
   683  						severity: protocol.SeverityInformation,
   684  						source:   string(cache.Govulncheck),
   685  						codeActions: []string{
   686  							"Upgrade to v1.0.6",
   687  							"Upgrade to latest",
   688  							"Reset govulncheck result",
   689  						},
   690  					},
   691  				},
   692  				codeActions: []string{
   693  					"Upgrade to v1.0.6",
   694  					"Upgrade to latest",
   695  					"Reset govulncheck result",
   696  				},
   697  				hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"},
   698  			},
   699  			"golang.org/bmod": {
   700  				diagnostics: []vulnDiag{
   701  					{
   702  						msg:      "golang.org/bmod has a vulnerability used in the code: GO-2022-02.",
   703  						severity: protocol.SeverityWarning,
   704  						source:   string(cache.Govulncheck),
   705  						codeActions: []string{
   706  							"Reset govulncheck result", // no fix, but we should give an option to reset.
   707  						},
   708  					},
   709  				},
   710  				codeActions: []string{
   711  					"Reset govulncheck result", // no fix, but we should give an option to reset.
   712  				},
   713  				hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."},
   714  			},
   715  		}
   716  
   717  		for mod, want := range wantDiagnostics {
   718  			modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics)
   719  
   720  			// Check that the actions we get when including all diagnostics at a location return the same result
   721  			gotActions := env.CodeAction("go.mod", modPathDiagnostics)
   722  			if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
   723  				t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff)
   724  				continue
   725  			}
   726  
   727  			// Apply the code action matching applyAction.
   728  			if want.applyAction == "" {
   729  				continue
   730  			}
   731  			for _, action := range gotActions {
   732  				if action.Title == want.applyAction {
   733  					env.ApplyCodeAction(action)
   734  					break
   735  				}
   736  			}
   737  		}
   738  
   739  		env.Await(env.DoneWithChangeWatchedFiles())
   740  		wantGoMod := `module golang.org/entry
   741  
   742  go 1.18
   743  
   744  require golang.org/cmod v1.1.3
   745  
   746  require (
   747  	golang.org/amod v1.0.6 // indirect
   748  	golang.org/bmod v0.5.0 // indirect
   749  )
   750  `
   751  		if got := env.BufferText("go.mod"); got != wantGoMod {
   752  			t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got))
   753  		}
   754  	})
   755  }
   756  
   757  func diffCodeActions(gotActions []protocol.CodeAction, want []string) string {
   758  	var gotTitles []string
   759  	for _, ca := range gotActions {
   760  		gotTitles = append(gotTitles, ca.Title)
   761  	}
   762  	return cmp.Diff(want, gotTitles)
   763  }
   764  
   765  const workspace2 = `
   766  -- go.mod --
   767  module golang.org/entry
   768  
   769  go 1.18
   770  
   771  require golang.org/bmod v0.5.0
   772  
   773  -- go.sum --
   774  golang.org/bmod v0.5.0 h1:MT/ysNRGbCiURc5qThRFWaZ5+rK3pQRPo9w7dYZfMDk=
   775  golang.org/bmod v0.5.0/go.mod h1:k+zl+Ucu4yLIjndMIuWzD/MnOHy06wqr3rD++y0abVs=
   776  -- x/x.go --
   777  package x
   778  
   779  import "golang.org/bmod/bvuln"
   780  
   781  func F() {
   782  	// Calls a benign func in bvuln.
   783  	bvuln.OK()
   784  }
   785  `
   786  
   787  const proxy2 = `
   788  -- golang.org/bmod@v0.5.0/bvuln/bvuln.go --
   789  package bvuln
   790  
   791  func Vuln() {} // vulnerable.
   792  func OK() {} // ok.
   793  `
   794  
   795  func TestGovulncheckInfo(t *testing.T) {
   796  	db, opts, err := vulnTestEnv(proxy2)
   797  	if err != nil {
   798  		t.Fatal(err)
   799  	}
   800  	defer db.Clean()
   801  	WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) {
   802  		env.OpenFile("go.mod")
   803  		var result command.RunVulncheckResult
   804  		env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result)
   805  		gotDiagnostics := &protocol.PublishDiagnosticsParams{}
   806  		env.OnceMet(
   807  			CompletedProgress(result.Token, nil),
   808  			ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities.
   809  		)
   810  
   811  		// Vulncheck diagnostics asynchronous to the vulncheck command.
   812  		env.OnceMet(
   813  			Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")),
   814  			ReadDiagnostics("go.mod", gotDiagnostics),
   815  		)
   816  
   817  		testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: vulncheck.ModeGovulncheck}})
   818  		// wantDiagnostics maps a module path in the require
   819  		// section of a go.mod to diagnostics that will be returned
   820  		// when running vulncheck.
   821  		wantDiagnostics := map[string]vulnDiagExpectation{
   822  			"golang.org/bmod": {
   823  				diagnostics: []vulnDiag{
   824  					{
   825  						msg:      "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.",
   826  						severity: protocol.SeverityInformation,
   827  						source:   string(cache.Govulncheck),
   828  						codeActions: []string{
   829  							"Reset govulncheck result",
   830  						},
   831  					},
   832  				},
   833  				codeActions: []string{
   834  					"Reset govulncheck result",
   835  				},
   836  				hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."},
   837  			},
   838  		}
   839  
   840  		var allActions []protocol.CodeAction
   841  		for mod, want := range wantDiagnostics {
   842  			modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics)
   843  			// Check that the actions we get when including all diagnostics at a location return the same result
   844  			gotActions := env.CodeAction("go.mod", modPathDiagnostics)
   845  			allActions = append(allActions, gotActions...)
   846  			if diff := diffCodeActions(gotActions, want.codeActions); diff != "" {
   847  				t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff)
   848  				continue
   849  			}
   850  		}
   851  
   852  		// Clear Diagnostics by using one of the reset code actions.
   853  		var reset protocol.CodeAction
   854  		for _, a := range allActions {
   855  			if a.Title == "Reset govulncheck result" {
   856  				reset = a
   857  				break
   858  			}
   859  		}
   860  		if reset.Title != "Reset govulncheck result" {
   861  			t.Errorf("failed to find a 'Reset govulncheck result' code action, got %v", allActions)
   862  		}
   863  		env.ApplyCodeAction(reset)
   864  
   865  		env.Await(NoDiagnostics(ForFile("go.mod")))
   866  	})
   867  }
   868  
   869  // testVulnDiagnostics finds the require or module statement line for the requireMod in go.mod file
   870  // and runs checks if diagnostics and code actions associated with the line match expectation.
   871  func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic {
   872  	t.Helper()
   873  	loc := env.RegexpSearch("go.mod", pattern)
   874  	var modPathDiagnostics []protocol.Diagnostic
   875  	for _, w := range want.diagnostics {
   876  		// Find the diagnostics at loc.start.
   877  		var diag *protocol.Diagnostic
   878  		for _, g := range got.Diagnostics {
   879  			g := g
   880  			if g.Range.Start == loc.Range.Start && w.msg == g.Message {
   881  				modPathDiagnostics = append(modPathDiagnostics, g)
   882  				diag = &g
   883  				break
   884  			}
   885  		}
   886  		if diag == nil {
   887  			t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg)
   888  			continue
   889  		}
   890  		if diag.Severity != w.severity || diag.Source != w.source {
   891  			t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source)
   892  		}
   893  		// Check expected code actions appear.
   894  		gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag})
   895  		if diff := diffCodeActions(gotActions, w.codeActions); diff != "" {
   896  			t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff)
   897  			continue
   898  		}
   899  	}
   900  	// Check that useful info is supplemented as hover.
   901  	if len(want.hover) > 0 {
   902  		hover, _ := env.Hover(loc)
   903  		for _, part := range want.hover {
   904  			if !strings.Contains(hover.Value, part) {
   905  				t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value)
   906  				break
   907  			}
   908  		}
   909  	}
   910  	return modPathDiagnostics
   911  }
   912  
   913  type vulnRelatedInfo struct {
   914  	Filename string
   915  	Line     uint32
   916  	Message  string
   917  }
   918  
   919  type vulnDiag struct {
   920  	msg      string
   921  	severity protocol.DiagnosticSeverity
   922  	// codeActions is a list titles of code actions that we get with this
   923  	// diagnostics as the context.
   924  	codeActions []string
   925  	// relatedInfo is related info message prefixed by the file base.
   926  	// See summarizeRelatedInfo.
   927  	relatedInfo []vulnRelatedInfo
   928  	// diagnostic source.
   929  	source string
   930  }
   931  
   932  // vulnDiagExpectation maps a module path in the require
   933  // section of a go.mod to diagnostics that will be returned
   934  // when running vulncheck.
   935  type vulnDiagExpectation struct {
   936  	// applyAction is the title of the code action to run for this module.
   937  	// If empty, no code actions will be executed.
   938  	applyAction string
   939  	// diagnostics is the list of diagnostics we expect at the require line for
   940  	// the module path.
   941  	diagnostics []vulnDiag
   942  	// codeActions is a list titles of code actions that we get with context
   943  	// diagnostics.
   944  	codeActions []string
   945  	// hover message is the list of expected hover message parts for this go.mod require line.
   946  	// all parts must appear in the hover message.
   947  	hover []string
   948  }