source.go (3659B)
1 // Copyright 2023 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 package client 6 7 import ( 8 "compress/gzip" 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "io/fs" 14 "net/http" 15 "os" 16 "path/filepath" 17 18 "golang.org/x/vuln/internal/derrors" 19 "golang.org/x/vuln/internal/osv" 20 ) 21 22 type source interface { 23 // get returns the raw, uncompressed bytes at the 24 // requested endpoint, which should be bare with no file extensions 25 // (e.g., "index/modules" instead of "index/modules.json.gz"). 26 // It errors if the endpoint cannot be reached or does not exist 27 // in the expected form. 28 get(ctx context.Context, endpoint string) ([]byte, error) 29 } 30 31 func newHTTPSource(url string, opts *Options) *httpSource { 32 c := http.DefaultClient 33 if opts != nil && opts.HTTPClient != nil { 34 c = opts.HTTPClient 35 } 36 return &httpSource{url: url, c: c} 37 } 38 39 // httpSource reads a vulnerability database from an http(s) source. 40 type httpSource struct { 41 url string 42 c *http.Client 43 } 44 45 func (hs *httpSource) get(ctx context.Context, endpoint string) (_ []byte, err error) { 46 derrors.Wrap(&err, "get(%s)", endpoint) 47 48 method := http.MethodGet 49 reqURL := fmt.Sprintf("%s/%s", hs.url, endpoint+".json.gz") 50 req, err := http.NewRequestWithContext(ctx, method, reqURL, nil) 51 if err != nil { 52 return nil, err 53 } 54 resp, err := hs.c.Do(req) 55 if err != nil { 56 return nil, err 57 } 58 defer resp.Body.Close() 59 if resp.StatusCode != http.StatusOK { 60 return nil, fmt.Errorf("HTTP %s %s returned unexpected status: %s", method, reqURL, resp.Status) 61 } 62 63 // Uncompress the result. 64 r, err := gzip.NewReader(resp.Body) 65 if err != nil { 66 return nil, err 67 } 68 defer r.Close() 69 70 return io.ReadAll(r) 71 } 72 73 func newLocalSource(dir string) *localSource { 74 return &localSource{fs: os.DirFS(dir)} 75 } 76 77 // localSource reads a vulnerability database from a local file system. 78 type localSource struct { 79 fs fs.FS 80 } 81 82 func (ls *localSource) get(ctx context.Context, endpoint string) (_ []byte, err error) { 83 derrors.Wrap(&err, "get(%s)", endpoint) 84 85 return fs.ReadFile(ls.fs, endpoint+".json") 86 } 87 88 func newHybridSource(dir string) (*hybridSource, error) { 89 index, err := indexFromDir(dir) 90 if err != nil { 91 return nil, err 92 } 93 94 return &hybridSource{ 95 index: &inMemorySource{data: index}, 96 osv: &localSource{fs: os.DirFS(dir)}, 97 }, nil 98 } 99 100 // hybridSource reads OSV entries from a local file system, but reads 101 // indexes from an in-memory map. 102 type hybridSource struct { 103 index *inMemorySource 104 osv *localSource 105 } 106 107 func (hs *hybridSource) get(ctx context.Context, endpoint string) (_ []byte, err error) { 108 derrors.Wrap(&err, "get(%s)", endpoint) 109 110 dir, file := filepath.Split(endpoint) 111 112 if filepath.Dir(dir) == indexDir { 113 return hs.index.get(ctx, endpoint) 114 } 115 116 return hs.osv.get(ctx, file) 117 } 118 119 // newInMemorySource creates a new in-memory source from OSV entries. 120 // Adapted from x/vulndb/internal/database.go. 121 func newInMemorySource(entries []*osv.Entry) (*inMemorySource, error) { 122 data, err := indexFromEntries(entries) 123 if err != nil { 124 return nil, err 125 } 126 127 for _, entry := range entries { 128 b, err := json.Marshal(entry) 129 if err != nil { 130 return nil, err 131 } 132 data[entryEndpoint(entry.ID)] = b 133 } 134 135 return &inMemorySource{data: data}, nil 136 } 137 138 // inMemorySource reads databases from an in-memory map. 139 // Currently intended for use only in unit tests. 140 type inMemorySource struct { 141 data map[string][]byte 142 } 143 144 func (db *inMemorySource) get(ctx context.Context, endpoint string) ([]byte, error) { 145 b, ok := db.data[endpoint] 146 if !ok { 147 return nil, fmt.Errorf("no data found at endpoint %q", endpoint) 148 } 149 return b, nil 150 }