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 }