client.go (7912B)
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 provides an interface for accessing vulnerability 6 // databases, via either HTTP or local filesystem access. 7 // 8 // The protocol is described at https://go.dev/security/vuln/database. 9 package client 10 11 import ( 12 "bytes" 13 "context" 14 "encoding/json" 15 "errors" 16 "fmt" 17 "net/http" 18 "net/url" 19 "os" 20 "path/filepath" 21 "sort" 22 "strings" 23 "time" 24 25 "golang.org/x/sync/errgroup" 26 "golang.org/x/vuln/internal/derrors" 27 "golang.org/x/vuln/internal/osv" 28 isem "golang.org/x/vuln/internal/semver" 29 "golang.org/x/vuln/internal/web" 30 ) 31 32 // A Client for reading vulnerability databases. 33 type Client struct { 34 source 35 } 36 37 type Options struct { 38 HTTPClient *http.Client 39 } 40 41 // NewClient returns a client that reads the vulnerability database 42 // in source (an "http" or "file" prefixed URL). 43 // 44 // It supports databases following the API described 45 // in https://go.dev/security/vuln/database#api. 46 func NewClient(source string, opts *Options) (_ *Client, err error) { 47 source = strings.TrimRight(source, "/") 48 uri, err := url.Parse(source) 49 if err != nil { 50 return nil, err 51 } 52 switch uri.Scheme { 53 case "http", "https": 54 return newHTTPClient(uri, opts) 55 case "file": 56 return newLocalClient(uri) 57 default: 58 return nil, fmt.Errorf("source %q has unsupported scheme", uri) 59 } 60 } 61 62 var errUnknownSchema = errors.New("unrecognized vulndb format; see https://go.dev/security/vuln/database#api for accepted schema") 63 64 func newHTTPClient(uri *url.URL, opts *Options) (*Client, error) { 65 source := uri.String() 66 67 // v1 returns true if the source likely follows the V1 schema. 68 v1 := func() bool { 69 return source == "https://vuln.go.dev" || 70 endpointExistsHTTP(source, "index/modules.json.gz") 71 } 72 73 if v1() { 74 return &Client{source: newHTTPSource(uri.String(), opts)}, nil 75 } 76 77 return nil, errUnknownSchema 78 } 79 80 func endpointExistsHTTP(source, endpoint string) bool { 81 r, err := http.Head(source + "/" + endpoint) 82 return err == nil && r.StatusCode == http.StatusOK 83 } 84 85 func newLocalClient(uri *url.URL) (*Client, error) { 86 dir, err := toDir(uri) 87 if err != nil { 88 return nil, err 89 } 90 91 // Check if the DB likely follows the v1 schema by 92 // looking for the "index/modules.json" endpoint. 93 if endpointExistsDir(dir, modulesEndpoint+".json") { 94 return &Client{source: newLocalSource(dir)}, nil 95 } 96 97 // If the DB doesn't follow the v1 schema, 98 // attempt to intepret it as a flat list of OSV files. 99 // This is currently a "hidden" feature, so don't output the 100 // specific error if this fails. 101 src, err := newHybridSource(dir) 102 if err != nil { 103 return nil, errUnknownSchema 104 } 105 return &Client{source: src}, nil 106 } 107 108 func toDir(uri *url.URL) (string, error) { 109 dir, err := web.URLToFilePath(uri) 110 if err != nil { 111 return "", err 112 } 113 fi, err := os.Stat(dir) 114 if err != nil { 115 return "", err 116 } 117 if !fi.IsDir() { 118 return "", fmt.Errorf("%s is not a directory", dir) 119 } 120 return dir, nil 121 } 122 123 func endpointExistsDir(dir, endpoint string) bool { 124 _, err := os.Stat(filepath.Join(dir, endpoint)) 125 return err == nil 126 } 127 128 func NewInMemoryClient(entries []*osv.Entry) (*Client, error) { 129 s, err := newInMemorySource(entries) 130 if err != nil { 131 return nil, err 132 } 133 return &Client{source: s}, nil 134 } 135 136 func (c *Client) LastModifiedTime(ctx context.Context) (_ time.Time, err error) { 137 derrors.Wrap(&err, "LastModifiedTime()") 138 139 b, err := c.source.get(ctx, dbEndpoint) 140 if err != nil { 141 return time.Time{}, err 142 } 143 144 var dbMeta dbMeta 145 if err := json.Unmarshal(b, &dbMeta); err != nil { 146 return time.Time{}, err 147 } 148 149 return dbMeta.Modified, nil 150 } 151 152 type ModuleRequest struct { 153 // The module path to filter on. 154 // This must be set (if empty, ByModule errors). 155 Path string 156 // (Optional) If set, only return vulnerabilities affected 157 // at this version. 158 Version string 159 } 160 161 type ModuleResponse struct { 162 Path string 163 Version string 164 Entries []*osv.Entry 165 } 166 167 // ByModules returns a list of responses 168 // containing the OSV entries corresponding to each request. 169 // 170 // The order of the requests is preserved, and each request has 171 // a response even if there are no entries (in which case the Entries 172 // field is nil). 173 func (c *Client) ByModules(ctx context.Context, reqs []*ModuleRequest) (_ []*ModuleResponse, err error) { 174 derrors.Wrap(&err, "ByModules(%v)", reqs) 175 176 metas, err := c.moduleMetas(ctx, reqs) 177 if err != nil { 178 return nil, err 179 } 180 181 resps := make([]*ModuleResponse, len(reqs)) 182 g, gctx := errgroup.WithContext(ctx) 183 g.SetLimit(10) 184 for i, req := range reqs { 185 i, req := i, req 186 g.Go(func() error { 187 entries, err := c.byModule(gctx, req, metas[i]) 188 if err != nil { 189 return err 190 } 191 resps[i] = &ModuleResponse{ 192 Path: req.Path, 193 Version: req.Version, 194 Entries: entries, 195 } 196 return nil 197 }) 198 } 199 if err := g.Wait(); err != nil { 200 return nil, err 201 } 202 203 return resps, nil 204 } 205 206 func (c *Client) moduleMetas(ctx context.Context, reqs []*ModuleRequest) (_ []*moduleMeta, err error) { 207 b, err := c.source.get(ctx, modulesEndpoint) 208 if err != nil { 209 return nil, err 210 } 211 212 dec, err := newStreamDecoder(b) 213 if err != nil { 214 return nil, err 215 } 216 217 metas := make([]*moduleMeta, len(reqs)) 218 for dec.More() { 219 var m moduleMeta 220 err := dec.Decode(&m) 221 if err != nil { 222 return nil, err 223 } 224 for i, req := range reqs { 225 if m.Path == req.Path { 226 metas[i] = &m 227 } 228 } 229 } 230 231 return metas, nil 232 } 233 234 // byModule returns the OSV entries matching the ModuleRequest, 235 // or (nil, nil) if there are none. 236 func (c *Client) byModule(ctx context.Context, req *ModuleRequest, m *moduleMeta) (_ []*osv.Entry, err error) { 237 // This module isn't in the database. 238 if m == nil { 239 return nil, nil 240 } 241 242 if req.Path == "" { 243 return nil, fmt.Errorf("module path must be set") 244 } 245 246 if req.Version != "" && !isem.Valid(req.Version) { 247 return nil, fmt.Errorf("version %s is not valid semver", req.Version) 248 } 249 250 var ids []string 251 for _, v := range m.Vulns { 252 if v.Fixed == "" || isem.Less(req.Version, v.Fixed) { 253 ids = append(ids, v.ID) 254 } 255 } 256 257 if len(ids) == 0 { 258 return nil, nil 259 } 260 261 entries, err := c.byIDs(ctx, ids) 262 if err != nil { 263 return nil, err 264 } 265 266 // Filter by version. 267 if req.Version != "" { 268 affected := func(e *osv.Entry) bool { 269 for _, a := range e.Affected { 270 if a.Module.Path == req.Path && isem.Affects(a.Ranges, req.Version) { 271 return true 272 } 273 } 274 return false 275 } 276 277 var filtered []*osv.Entry 278 for _, entry := range entries { 279 if affected(entry) { 280 filtered = append(filtered, entry) 281 } 282 } 283 if len(filtered) == 0 { 284 return nil, nil 285 } 286 } 287 288 sort.SliceStable(entries, func(i, j int) bool { 289 return entries[i].ID < entries[j].ID 290 }) 291 292 return entries, nil 293 } 294 295 func (c *Client) byIDs(ctx context.Context, ids []string) (_ []*osv.Entry, err error) { 296 entries := make([]*osv.Entry, len(ids)) 297 g, gctx := errgroup.WithContext(ctx) 298 g.SetLimit(10) 299 for i, id := range ids { 300 i, id := i, id 301 g.Go(func() error { 302 e, err := c.byID(gctx, id) 303 if err != nil { 304 return err 305 } 306 entries[i] = e 307 return nil 308 }) 309 } 310 if err := g.Wait(); err != nil { 311 return nil, err 312 } 313 314 return entries, nil 315 } 316 317 // byID returns the OSV entry with the given ID, 318 // or an error if it does not exist / cannot be unmarshaled. 319 func (c *Client) byID(ctx context.Context, id string) (_ *osv.Entry, err error) { 320 derrors.Wrap(&err, "byID(%s)", id) 321 322 b, err := c.source.get(ctx, entryEndpoint(id)) 323 if err != nil { 324 return nil, err 325 } 326 327 var entry osv.Entry 328 if err := json.Unmarshal(b, &entry); err != nil { 329 return nil, err 330 } 331 332 return &entry, nil 333 } 334 335 // newStreamDecoder returns a decoder that can be used 336 // to read an array of JSON objects. 337 func newStreamDecoder(b []byte) (*json.Decoder, error) { 338 dec := json.NewDecoder(bytes.NewBuffer(b)) 339 340 // skip open bracket 341 _, err := dec.Token() 342 if err != nil { 343 return nil, err 344 } 345 346 return dec, nil 347 }