blob: 9b2ca263e1c2dd0e973f639f4319083432d666e2 [file] [log] [blame]
Filippo Valsorda087c0612021-04-13 22:58:27 +02001// Copyright 2021 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
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -07005package client
6
7import (
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -05008 "context"
Roland Shoemaker3361bb72021-07-09 11:28:45 -07009 "encoding/json"
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070010 "fmt"
Jonathan Amsterdam125db722021-10-21 17:54:25 -040011 "io"
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070012 "io/ioutil"
13 "net/http"
Roland Shoemaker3361bb72021-07-09 11:28:45 -070014 "net/http/httptest"
Zvonimir Pavlinovic5966fd22021-09-10 16:55:17 -070015 "net/url"
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070016 "os"
17 "path"
Roland Shoemaker3361bb72021-07-09 11:28:45 -070018 "reflect"
Roland Shoemaker68c54ab2021-06-01 16:24:12 -070019 "runtime"
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070020 "testing"
21 "time"
22
Jonathan Amsterdam5b125212021-10-14 15:09:11 -040023 "github.com/google/go-cmp/cmp"
Julie Qiuffed8632021-11-01 14:50:16 -040024 "golang.org/x/vuln/internal"
25 "golang.org/x/vuln/osv"
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070026)
27
Jonathan Amsterdam5b125212021-10-14 15:09:11 -040028var (
29 testVuln = `
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -070030 {"ID":"ID","Package":{"Name":"golang.org/example/one","Ecosystem":"go"}, "Summary":"",
Roland Shoemakerc856ba82021-05-20 09:58:22 -070031 "Severity":2,"Affects":{"Ranges":[{"Type":"SEMVER","Introduced":"","Fixed":"v2.2.0"}]},
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070032 "ecosystem_specific":{"Symbols":["some_symbol_1"]
Jonathan Amsterdam5b125212021-10-14 15:09:11 -040033 }}`
34
35 testVulns = "[" + testVuln + "]"
36)
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070037
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -040038var (
39 // index containing timestamps for package in testVuln.
40 index string = `{
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -070041 "golang.org/example/one": "2020-03-09T10:00:00.81362141-07:00"
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070042 }`
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -040043 // index of IDs
44 idIndex string = `["ID"]`
45)
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -070046
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -070047// testCache for testing purposes
48type testCache struct {
49 indexMap map[string]osv.DBIndex
50 indexStamp map[string]time.Time
51 vulnMap map[string]map[string][]*osv.Entry
52}
53
54func freshTestCache() *testCache {
55 return &testCache{
56 indexMap: make(map[string]osv.DBIndex),
57 indexStamp: make(map[string]time.Time),
58 vulnMap: make(map[string]map[string][]*osv.Entry),
59 }
60}
61
62func (tc *testCache) ReadIndex(db string) (osv.DBIndex, time.Time, error) {
63 index, ok := tc.indexMap[db]
64 if !ok {
65 return nil, time.Time{}, nil
66 }
67 stamp, ok := tc.indexStamp[db]
68 if !ok {
69 return nil, time.Time{}, nil
70 }
71 return index, stamp, nil
72}
73
74func (tc *testCache) WriteIndex(db string, index osv.DBIndex, stamp time.Time) error {
75 tc.indexMap[db] = index
76 tc.indexStamp[db] = stamp
77 return nil
78}
79
80func (tc *testCache) ReadEntries(db, module string) ([]*osv.Entry, error) {
81 mMap, ok := tc.vulnMap[db]
82 if !ok {
83 return nil, nil
84 }
85 return mMap[module], nil
86}
87
88func (tc *testCache) WriteEntries(db, module string, entries []*osv.Entry) error {
89 mMap, ok := tc.vulnMap[db]
90 if !ok {
91 mMap = make(map[string][]*osv.Entry)
92 tc.vulnMap[db] = mMap
93 }
94 mMap[module] = append(mMap[module], entries...)
95 return nil
96}
97
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -070098// cachedTestVuln returns a function creating a local cache
99// for db with `dbName` with a version of testVuln where
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700100// Summary="cached" and LastModified happened after entry
101// in the `index` for the same pkg.
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -0700102func cachedTestVuln(dbName string) Cache {
103 c := freshTestCache()
104 e := &osv.Entry{
105 ID: "ID1",
106 Details: "cached",
107 Modified: time.Now(),
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700108 }
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -0700109 c.WriteEntries(dbName, "golang.org/example/one", []*osv.Entry{e})
110 return c
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700111}
112
113// createDirAndFile creates a directory `dir` if such directory does
114// not exist and creates a `file` with `content` in the directory.
115func createDirAndFile(dir, file, content string) error {
116 if err := os.MkdirAll(dir, 0755); err != nil {
117 return err
118 }
119 return ioutil.WriteFile(path.Join(dir, file), []byte(content), 0644)
120}
121
Jonathan Amsterdam5b125212021-10-14 15:09:11 -0400122// localDB creates a local db with testVulns and index as contents.
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700123func localDB(t *testing.T) (string, error) {
124 dbName := t.TempDir()
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400125 for _, f := range []struct {
126 dir, filename, content string
127 }{
128 {"golang.org/example", "one.json", testVulns},
129 {"", "index.json", index},
130 {internal.IDDirectory, "ID.json", testVuln},
131 {internal.IDDirectory, "index.json", idIndex},
132 } {
133 if err := createDirAndFile(path.Join(dbName, f.dir), f.filename, f.content); err != nil {
134 return "", err
135 }
Jonathan Amsterdam5b125212021-10-14 15:09:11 -0400136 }
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700137 return dbName, nil
138}
139
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400140func newTestServer() *httptest.Server {
141 dataHandler := func(data string) http.HandlerFunc {
142 return func(w http.ResponseWriter, _ *http.Request) {
143 io.WriteString(w, data)
144 }
145 }
146
147 mux := http.NewServeMux()
148 mux.HandleFunc("/golang.org/example/one.json", dataHandler(testVulns))
149 mux.HandleFunc("/index.json", dataHandler(index))
150 mux.HandleFunc(fmt.Sprintf("/%s/ID.json", internal.IDDirectory), dataHandler(testVuln))
151 mux.HandleFunc("/ID/index.json", func(w http.ResponseWriter, r *http.Request) {
152 io.WriteString(w, idIndex)
153 })
154 return httptest.NewServer(mux)
155}
156
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700157func TestClient(t *testing.T) {
Roland Shoemaker68c54ab2021-06-01 16:24:12 -0700158 if runtime.GOOS == "js" {
159 t.Skip("skipping test: no network on js")
160 }
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -0500161 ctx := context.Background()
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700162 // Create a local http database.
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400163 srv := newTestServer()
164 defer srv.Close()
165 u, err := url.Parse(srv.URL)
Roland Shoemaker090c04e2021-05-27 11:06:59 -0700166 if err != nil {
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400167 t.Fatal(err)
Roland Shoemaker090c04e2021-05-27 11:06:59 -0700168 }
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400169 dbName := u.Hostname()
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700170
171 // Create a local file database.
172 localDBName, err := localDB(t)
173 if err != nil {
174 t.Fatal(err)
175 }
176 defer os.RemoveAll(localDBName)
177
178 for _, test := range []struct {
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -0700179 name string
180 source string
181 cache Cache
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -0700182 // cache summary for testVuln
183 summary string
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700184 }{
185 // Test the http client without any cache.
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400186 {name: "http-no-cache", source: srv.URL, cache: nil, summary: ""},
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700187 // Test the http client with empty cache.
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400188 {name: "http-empty-cache", source: srv.URL, cache: freshTestCache(), summary: ""},
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700189 // Test the client with non-stale cache containing a version of testVuln2 where Summary="cached".
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400190 {name: "http-cache", source: srv.URL, cache: cachedTestVuln(dbName), summary: "cached"},
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700191 // Repeat the same for local file client.
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -0700192 {name: "file-no-cache", source: "file://" + localDBName, cache: nil, summary: ""},
193 {name: "file-empty-cache", source: "file://" + localDBName, cache: freshTestCache(), summary: ""},
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700194 // Cache does not play a role in local file databases.
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -0700195 {name: "file-cache", source: "file://" + localDBName, cache: cachedTestVuln(localDBName), summary: ""},
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700196 } {
Zvonimir Pavlinovic650dff02021-09-02 17:30:08 -0700197 client, err := NewClient([]string{test.source}, Options{HTTPCache: test.cache})
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700198 if err != nil {
199 t.Fatal(err)
200 }
201
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -0500202 vulns, err := client.GetByModule(ctx, "golang.org/example/one")
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700203 if err != nil {
204 t.Fatal(err)
205 }
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -0700206
207 if len(vulns) != 1 {
208 t.Errorf("%s: want 1 vuln for golang.org/example/one; got %v", test.name, len(vulns))
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700209 }
210
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -0700211 if v := vulns[0]; v.Details != test.summary {
212 t.Errorf("%s: want '%s' summary for testVuln; got '%s'", test.name, test.summary, v.Details)
Zvonimir Pavlinovic0cb7a212021-03-25 10:40:06 -0700213 }
214 }
215}
Roland Shoemaker3361bb72021-07-09 11:28:45 -0700216
217func TestCorrectFetchesNoCache(t *testing.T) {
218 fetches := map[string]int{}
219 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
220 fetches[r.URL.Path]++
221 if r.URL.Path == "/index.json" {
222 j, _ := json.Marshal(osv.DBIndex{
223 "a": time.Now(),
224 "b": time.Now(),
225 })
226 w.Write(j)
227 } else {
228 w.Write([]byte("[]"))
229 }
230 }))
231 defer ts.Close()
232
233 hs := &httpSource{url: ts.URL, c: new(http.Client)}
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -0700234 for _, module := range []string{"a", "b", "c"} {
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -0500235 if _, err := hs.GetByModule(context.Background(), module); err != nil {
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -0700236 t.Fatalf("unexpected error: %s", err)
237 }
Roland Shoemaker3361bb72021-07-09 11:28:45 -0700238 }
Zvonimir Pavlinovic36ac6842021-08-24 18:51:17 -0700239 expectedFetches := map[string]int{"/index.json": 3, "/a.json": 1, "/b.json": 1}
Roland Shoemaker3361bb72021-07-09 11:28:45 -0700240 if !reflect.DeepEqual(fetches, expectedFetches) {
241 t.Errorf("unexpected fetches, got %v, want %v", fetches, expectedFetches)
242 }
243}
Zvonimir Pavlinovic5966fd22021-09-10 16:55:17 -0700244
245// Make sure that a cached index is used in the case it is stale
246// but there were no changes to it at the server side.
247func TestCorrectFetchesNoChangeIndex(t *testing.T) {
248 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
249 if r.URL.Path == "/index.json" {
250 w.WriteHeader(http.StatusNotModified)
251 }
252 }))
253 defer ts.Close()
254 url, _ := url.Parse(ts.URL)
255
256 // set timestamp so that cached index is stale,
257 // i.e., more than two hours old.
258 timeStamp := time.Now().Add(time.Hour * (-3))
259 index := osv.DBIndex{"a": timeStamp}
260 cache := freshTestCache()
261 cache.WriteIndex(url.Hostname(), index, timeStamp)
262
263 e := &osv.Entry{
264 ID: "ID1",
265 Modified: timeStamp,
266 }
267 cache.WriteEntries(url.Hostname(), "a", []*osv.Entry{e})
268
269 client, err := NewClient([]string{ts.URL}, Options{HTTPCache: cache})
270 if err != nil {
271 t.Fatal(err)
272 }
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -0500273 vulns, err := client.GetByModule(context.Background(), "a")
Zvonimir Pavlinovic5966fd22021-09-10 16:55:17 -0700274 if err != nil {
275 t.Fatal(err)
276 }
277 if !reflect.DeepEqual(vulns, []*osv.Entry{e}) {
278 t.Errorf("want %v vuln; got %v", e, vulns)
279 }
280}
Jonathan Amsterdam5b125212021-10-14 15:09:11 -0400281
282func TestClientByID(t *testing.T) {
283 if runtime.GOOS == "js" {
284 t.Skip("skipping test: no network on js")
285 }
286
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400287 srv := newTestServer()
288 defer srv.Close()
Jonathan Amsterdam5b125212021-10-14 15:09:11 -0400289
290 // Create a local file database.
291 localDBName, err := localDB(t)
292 if err != nil {
293 t.Fatal(err)
294 }
295 defer os.RemoveAll(localDBName)
296
297 var want osv.Entry
298 if err := json.Unmarshal([]byte(testVuln), &want); err != nil {
299 t.Fatal(err)
300 }
301 for _, test := range []struct {
302 name string
303 source string
304 }{
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400305 {name: "http", source: srv.URL},
306 {name: "file", source: "file://" + localDBName},
Jonathan Amsterdam5b125212021-10-14 15:09:11 -0400307 } {
308 t.Run(test.name, func(t *testing.T) {
309 client, err := NewClient([]string{test.source}, Options{})
310 if err != nil {
311 t.Fatal(err)
312 }
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -0500313 got, err := client.GetByID(context.Background(), "ID")
Jonathan Amsterdam5b125212021-10-14 15:09:11 -0400314 if err != nil {
315 t.Fatal(err)
316 }
317 if !cmp.Equal(got, &want) {
318 t.Errorf("got\n%+v\nwant\n%+v", got, &want)
319 }
320 })
321 }
322}
Jonathan Amsterdam125db722021-10-21 17:54:25 -0400323
324func TestListIDs(t *testing.T) {
325 if runtime.GOOS == "js" {
326 t.Skip("skipping test: no network on js")
327 }
328
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400329 srv := newTestServer()
330 defer srv.Close()
Jonathan Amsterdam125db722021-10-21 17:54:25 -0400331
332 // Create a local file database.
333 localDBName, err := localDB(t)
334 if err != nil {
335 t.Fatal(err)
336 }
337 defer os.RemoveAll(localDBName)
338
339 want := []string{"ID"}
340 for _, test := range []struct {
341 name string
342 source string
343 }{
Jonathan Amsterdamd6d79dd2021-10-21 18:16:36 -0400344 {name: "http", source: srv.URL},
345 {name: "file", source: "file://" + localDBName},
Jonathan Amsterdam125db722021-10-21 17:54:25 -0400346 } {
347 t.Run(test.name, func(t *testing.T) {
348 client, err := NewClient([]string{test.source}, Options{})
349 if err != nil {
350 t.Fatal(err)
351 }
Jonathan Amsterdamed8b8d52021-11-18 08:12:51 -0500352 got, err := client.ListIDs(context.Background())
Jonathan Amsterdam125db722021-10-21 17:54:25 -0400353 if err != nil {
354 t.Fatal(err)
355 }
356 if !cmp.Equal(got, want) {
357 t.Errorf("got\n%+v\nwant\n%+v", got, want)
358 }
359 })
360 }
361}