Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 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 | // Only run this on Go 1.18 or higher, because govulncheck can't |
| 6 | // run on binaries before 1.18. |
| 7 | |
| 8 | //go:build go1.18 |
| 9 | // +build go1.18 |
| 10 | |
| 11 | package main |
| 12 | |
| 13 | import ( |
Ian Cottrell | dc4d501 | 2023-04-23 16:23:07 -0400 | [diff] [blame] | 14 | "bytes" |
| 15 | "context" |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 16 | "flag" |
| 17 | "fmt" |
| 18 | "os" |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 19 | "path/filepath" |
| 20 | "regexp" |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 21 | "runtime" |
Ian Cottrell | cc61b4c | 2023-05-15 20:06:41 -0400 | [diff] [blame] | 22 | "sync" |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 23 | "testing" |
Bryan C. Mills | e55aa3a | 2023-04-26 16:36:56 -0400 | [diff] [blame] | 24 | "unsafe" |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 25 | |
| 26 | "github.com/google/go-cmdtest" |
Ian Cottrell | 7b0106d | 2023-06-05 22:42:20 -0400 | [diff] [blame] | 27 | "golang.org/x/vuln/internal/govulncheck" |
Julie Qiu | 2195bd1 | 2023-03-07 22:09:32 -0500 | [diff] [blame] | 28 | "golang.org/x/vuln/internal/test" |
Hana (Hyang-Ah) Kim | 2aa0553 | 2022-10-03 13:20:49 -0400 | [diff] [blame] | 29 | "golang.org/x/vuln/internal/web" |
Julie Qiu | 62092d5 | 2023-05-03 10:26:25 -0400 | [diff] [blame] | 30 | "golang.org/x/vuln/scan" |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 31 | ) |
| 32 | |
| 33 | var update = flag.Bool("update", false, "update test files with results") |
| 34 | |
Ian Cottrell | bb1cd57 | 2023-05-15 16:06:54 -0400 | [diff] [blame] | 35 | type fixup struct { |
| 36 | pattern string |
| 37 | compiled *regexp.Regexp |
| 38 | replace string |
| 39 | replaceFunc func(b []byte) []byte |
| 40 | } |
| 41 | |
| 42 | var fixups = []fixup{ |
| 43 | { |
| 44 | // modifies paths to Go files by replacing their directory with "...". |
| 45 | // For example,/a/b/c.go becomes .../c.go . |
| 46 | // This makes it possible to compare govulncheck output across systems, because |
| 47 | // Go filenames include setup-specific paths. |
| 48 | pattern: `[^\s"]*\.go[\s":]`, |
| 49 | replaceFunc: func(b []byte) []byte { |
| 50 | s := string(b) |
| 51 | return []byte(fmt.Sprintf(`.../%s%c`, filepath.Base(s[:len(s)-1]), s[len(s)-1])) |
| 52 | }, |
| 53 | }, { |
| 54 | // There was a one-line change in container/heap/heap.go between 1.18 |
| 55 | // and 1.19 that makes the stack traces different. Ignore it. |
| 56 | pattern: `heap\.go:(\d+)`, |
| 57 | replace: `N`, |
| 58 | }, { |
| 59 | pattern: `Scanning your code and (\d+) packages across (\d+)`, |
| 60 | replace: `Scanning your code and P packages across M`, |
| 61 | }, { |
Ian Cottrell | e3a5c49 | 2023-08-15 17:52:15 -0400 | [diff] [blame] | 62 | pattern: `Scanner: govulncheck@v.*`, |
| 63 | replace: `Scanner: govulncheck@v1.0.0`, |
Ian Cottrell | bb1cd57 | 2023-05-15 16:06:54 -0400 | [diff] [blame] | 64 | }, { |
| 65 | pattern: `"([^"]*") is a file`, |
| 66 | replace: `govulncheck: myfile is a file`, |
| 67 | }, { |
| 68 | pattern: `"scanner_version": "[^"]*"`, |
| 69 | replace: `"scanner_version": "v0.0.0-00000000000-20000101010101"`, |
| 70 | }, { |
| 71 | pattern: `file:///(.*)/testdata/vulndb`, |
| 72 | replace: `testdata/vulndb`, |
| 73 | }, { |
| 74 | pattern: `package (.*) is not in (GOROOT|std) (.*)`, |
| 75 | replace: `package foo is not in GOROOT (/tmp/foo)`, |
| 76 | }, { |
| 77 | pattern: `modified (.*)\)`, |
| 78 | replace: `modified 01 Jan 21 00:00 UTC)`, |
| 79 | }, { |
Ian Cottrell | e3a5c49 | 2023-08-15 17:52:15 -0400 | [diff] [blame] | 80 | pattern: `Go: (go1.[\.\d]*|devel).*`, |
| 81 | replace: `Go: go1.18`, |
Ian Cottrell | bb1cd57 | 2023-05-15 16:06:54 -0400 | [diff] [blame] | 82 | }, { |
| 83 | pattern: `"go_version": "go[^\s"]*"`, |
| 84 | replace: `"go_version": "go1.18"`, |
| 85 | }, |
| 86 | } |
| 87 | |
| 88 | func (f *fixup) init() { |
| 89 | f.compiled = regexp.MustCompile(f.pattern) |
| 90 | } |
| 91 | |
| 92 | func (f *fixup) apply(data []byte) []byte { |
| 93 | if f.replaceFunc != nil { |
| 94 | return f.compiled.ReplaceAllFunc(data, f.replaceFunc) |
| 95 | } |
| 96 | return f.compiled.ReplaceAll(data, []byte(f.replace)) |
| 97 | } |
| 98 | |
| 99 | func init() { |
| 100 | for i := range fixups { |
| 101 | fixups[i].init() |
| 102 | } |
| 103 | } |
| 104 | |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 105 | func TestCommand(t *testing.T) { |
Dmitri Shuralyov | 1568f33 | 2023-07-18 14:17:03 -0400 | [diff] [blame] | 106 | if testing.Short() { |
| 107 | t.Skip("skipping test that uses internet in short mode") |
| 108 | } |
| 109 | |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 110 | testDir, err := os.Getwd() |
| 111 | if err != nil { |
| 112 | t.Fatal(err) |
| 113 | } |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 114 | vulndbDir, err := filepath.Abs(filepath.Join(testDir, "testdata", "vulndb-v1")) |
| 115 | if err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
Ian Cottrell | 06356f8 | 2023-05-15 21:51:36 -0400 | [diff] [blame] | 118 | govulndbURI, err := web.URLFromFilePath(vulndbDir) |
| 119 | if err != nil { |
| 120 | t.Fatalf("failed to create make vulndb url: %v", err) |
| 121 | } |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 122 | |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 123 | moduleDirs, err := filepath.Glob("testdata/modules/*") |
| 124 | if err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
Zvonimir Pavlinovic | bf90c09 | 2022-08-23 11:21:49 -0700 | [diff] [blame] | 127 | |
Jonathan Amsterdam | cf2b565 | 2022-09-21 08:14:20 -0400 | [diff] [blame] | 128 | os.Setenv("moddir", filepath.Join(testDir, "testdata", "modules")) |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 129 | for _, md := range moduleDirs { |
Zvonimir Pavlinovic | 9a72d68 | 2023-01-18 17:24:09 -0800 | [diff] [blame] | 130 | // Skip nogomod module. It has intended build issues. |
| 131 | if filepath.Base(md) == "nogomod" { |
Zvonimir Pavlinovic | bf90c09 | 2022-08-23 11:21:49 -0700 | [diff] [blame] | 132 | continue |
| 133 | } |
Julie Qiu | 22f32fd | 2023-04-18 17:52:36 -0400 | [diff] [blame] | 134 | |
| 135 | // Build test module binary. |
Ian Cottrell | b57773a | 2023-05-15 20:15:18 -0400 | [diff] [blame] | 136 | binary, cleanup := test.GoBuild(t, md, "", filepath.Base(md) == "strip") |
Jonathan Amsterdam | cf2b565 | 2022-09-21 08:14:20 -0400 | [diff] [blame] | 137 | t.Cleanup(cleanup) |
Jonathan Amsterdam | f78f145 | 2022-06-09 12:38:39 -0400 | [diff] [blame] | 138 | // Set an environment variable to the path to the binary, so tests |
| 139 | // can refer to it. |
| 140 | varName := filepath.Base(md) + "_binary" |
| 141 | os.Setenv(varName, binary) |
| 142 | } |
Ian Cottrell | 78e1e63 | 2023-05-15 22:33:29 -0400 | [diff] [blame] | 143 | runTestSuite(t, filepath.Join(testDir, "testdata"), govulndbURI.String(), *update) |
Ian Cottrell | b57773a | 2023-05-15 20:15:18 -0400 | [diff] [blame] | 144 | if runtime.GOOS != "darwin" { |
Zvonimir Pavlinovic | 2f47882 | 2023-04-19 15:10:36 -0700 | [diff] [blame] | 145 | // TODO(https://go.dev/issue/61051): binaries are not currently stripped on darwin. |
| 146 | // This is expected to change in Go 1.22. |
Ian Cottrell | 78e1e63 | 2023-05-15 22:33:29 -0400 | [diff] [blame] | 147 | runTestSuite(t, filepath.Join(testDir, "testdata/strip"), govulndbURI.String(), *update) |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 148 | } |
| 149 | } |
| 150 | |
Ian Cottrell | cc61b4c | 2023-05-15 20:06:41 -0400 | [diff] [blame] | 151 | // Limit the number of concurrent scans. Scanning is implemented using |
| 152 | // x/tools/go/ssa, which is known to be memory-hungry |
| 153 | // (see https://go.dev/issue/14113), and by default the testing package |
| 154 | // allows up to GOMAXPROCS parallel tests at a time. |
| 155 | // |
| 156 | // For now we arbitrarily limit to ⌈GOMAXPROCS/4⌉, on the theory that many Go |
| 157 | // developer and CI machines have at least 8 logical cores and we want most |
| 158 | // runs of the test to exercise at least a little concurrency. If that turns |
| 159 | // out to still be too high, we may consider reducing it further. |
| 160 | // |
| 161 | // Since all of the scans run in the same process, we need an especially low |
| 162 | // limit on 32-bit platforms: we may run out of virtual address space well |
| 163 | // before we run out of system RAM. |
| 164 | var ( |
| 165 | parallelLimiter chan struct{} |
| 166 | parallelLimiterInit sync.Once |
| 167 | ) |
| 168 | |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 169 | // testSuite creates a cmdtest suite from dir. It also defines |
| 170 | // a govulncheck command on the suite that runs govulncheck |
| 171 | // against vulnerability database available at vulndbDir. |
Ian Cottrell | 06356f8 | 2023-05-15 21:51:36 -0400 | [diff] [blame] | 172 | func runTestSuite(t *testing.T, dir string, govulndb string, update bool) { |
Ian Cottrell | b57773a | 2023-05-15 20:15:18 -0400 | [diff] [blame] | 173 | parallelLimiterInit.Do(func() { |
| 174 | limit := (runtime.GOMAXPROCS(0) + 3) / 4 |
| 175 | if limit > 2 && unsafe.Sizeof(uintptr(0)) < 8 { |
| 176 | limit = 2 |
| 177 | } |
| 178 | parallelLimiter = make(chan struct{}, limit) |
| 179 | }) |
| 180 | |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 181 | ts, err := cmdtest.Read(dir) |
| 182 | if err != nil { |
Ian Cottrell | b57773a | 2023-05-15 20:15:18 -0400 | [diff] [blame] | 183 | t.Fatal(err) |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 184 | } |
Ian Cottrell | d881805 | 2023-04-21 22:20:55 -0400 | [diff] [blame] | 185 | ts.DisableLogging = true |
Bryan C. Mills | e55aa3a | 2023-04-26 16:36:56 -0400 | [diff] [blame] | 186 | |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 187 | ts.Commands["govulncheck"] = func(args []string, inputFile string) ([]byte, error) { |
Ian Cottrell | cc61b4c | 2023-05-15 20:06:41 -0400 | [diff] [blame] | 188 | parallelLimiter <- struct{}{} |
| 189 | defer func() { <-parallelLimiter }() |
Bryan C. Mills | e55aa3a | 2023-04-26 16:36:56 -0400 | [diff] [blame] | 190 | |
Ian Cottrell | 06356f8 | 2023-05-15 21:51:36 -0400 | [diff] [blame] | 191 | newargs := append([]string{"-db", govulndb}, args...) |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 192 | |
Ian Cottrell | dc4d501 | 2023-04-23 16:23:07 -0400 | [diff] [blame] | 193 | buf := &bytes.Buffer{} |
| 194 | cmd := scan.Command(context.Background(), newargs...) |
| 195 | cmd.Stdout = buf |
| 196 | cmd.Stderr = buf |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 197 | if inputFile != "" { |
Ian Cottrell | 78e1e63 | 2023-05-15 22:33:29 -0400 | [diff] [blame] | 198 | input, err := os.Open(filepath.Join(dir, inputFile)) |
| 199 | if err != nil { |
| 200 | return nil, err |
| 201 | } |
| 202 | defer input.Close() |
| 203 | cmd.Stdin = input |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 204 | } |
| 205 | // We set GOVERSION to always get the same results regardless of the underlying Go build system. |
Ian Cottrell | 9e5885c | 2023-05-26 19:38:17 -0400 | [diff] [blame] | 206 | cmd.Env = append(os.Environ(), "GOVERSION=go1.18") |
Ian Cottrell | a2fb4c4 | 2023-05-11 19:29:44 -0400 | [diff] [blame] | 207 | if err := cmd.Start(); err != nil { |
| 208 | return nil, err |
| 209 | } |
Ian Cottrell | 06356f8 | 2023-05-15 21:51:36 -0400 | [diff] [blame] | 210 | err := cmd.Wait() |
Ian Cottrell | dc4d501 | 2023-04-23 16:23:07 -0400 | [diff] [blame] | 211 | switch e := err.(type) { |
| 212 | case nil: |
| 213 | case interface{ ExitCode() int }: |
| 214 | err = &cmdtest.ExitCodeErr{Msg: err.Error(), Code: e.ExitCode()} |
| 215 | if e.ExitCode() == 0 { |
| 216 | err = nil |
| 217 | } |
| 218 | default: |
| 219 | fmt.Fprintln(buf, err) |
| 220 | err = &cmdtest.ExitCodeErr{Msg: err.Error(), Code: 1} |
| 221 | } |
Ian Cottrell | 7b0106d | 2023-06-05 22:42:20 -0400 | [diff] [blame] | 222 | sorted := buf |
| 223 | if err == nil && isJSONMode(args) { |
| 224 | // parse, sort and reprint the output for test stability |
| 225 | gather := test.NewMockHandler() |
| 226 | if err := govulncheck.HandleJSON(buf, gather); err != nil { |
| 227 | return nil, err |
| 228 | } |
| 229 | sorted = &bytes.Buffer{} |
| 230 | h := govulncheck.NewJSONHandler(sorted) |
| 231 | if err := gather.Write(h); err != nil { |
| 232 | return nil, err |
| 233 | } |
| 234 | } |
| 235 | out := sorted.Bytes() |
Ian Cottrell | bb1cd57 | 2023-05-15 16:06:54 -0400 | [diff] [blame] | 236 | for _, fix := range fixups { |
| 237 | out = fix.apply(out) |
| 238 | } |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 239 | return out, err |
| 240 | } |
Ian Cottrell | f97f4a9 | 2023-05-19 18:10:24 -0400 | [diff] [blame] | 241 | if update { |
| 242 | ts.Run(t, true) |
| 243 | return |
| 244 | } |
| 245 | ts.RunParallel(t, false) |
Zvonimir Pavlinovic | 4782cc1 | 2023-04-17 10:33:37 -0700 | [diff] [blame] | 246 | } |
Ian Cottrell | 7b0106d | 2023-06-05 22:42:20 -0400 | [diff] [blame] | 247 | |
| 248 | func isJSONMode(args []string) bool { |
| 249 | for _, arg := range args { |
| 250 | if arg == "-json" { |
| 251 | return true |
| 252 | } |
| 253 | } |
| 254 | return false |
| 255 | } |