| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package vuln |
| |
| import ( |
| "compress/gzip" |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "net/url" |
| "os" |
| "path/filepath" |
| "sort" |
| |
| "golang.org/x/pkgsite/internal/osv" |
| ) |
| |
| type source interface { |
| // get returns the raw, uncompressed bytes at the |
| // requested endpoint, which should be bare with no file extensions |
| // (e.g., "index/modules" instead of "index/modules.json.gz"). |
| // It errors if the endpoint cannot be reached or does not exist |
| // in the expected form. |
| get(ctx context.Context, endpoint string) ([]byte, error) |
| } |
| |
| // NewSource returns a source interface from a http:// or file:// prefixed |
| // url src. It errors if the given url is invalid or does not exist. |
| func NewSource(src string) (source, error) { |
| uri, err := url.Parse(src) |
| if err != nil { |
| return nil, err |
| } |
| switch uri.Scheme { |
| case "http", "https": |
| return &httpSource{url: uri.String(), c: http.DefaultClient}, nil |
| case "file": |
| dir, err := URLToFilePath(uri) |
| if err != nil { |
| return nil, err |
| } |
| fi, err := os.Stat(dir) |
| if err != nil { |
| return nil, err |
| } |
| if !fi.IsDir() { |
| return nil, fmt.Errorf("%s is not a directory", dir) |
| } |
| return &localSource{dir: dir}, nil |
| default: |
| return nil, fmt.Errorf("src %q has unsupported scheme", uri) |
| } |
| } |
| |
| // httpSource reads databases from an http(s) source. |
| // Intended for use in production. |
| type httpSource struct { |
| url string |
| c *http.Client |
| } |
| |
| func (hs *httpSource) get(ctx context.Context, endpoint string) ([]byte, error) { |
| reqURL := fmt.Sprintf("%s/%s", hs.url, endpoint+".json.gz") |
| req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) |
| if err != nil { |
| return nil, err |
| } |
| resp, err := hs.c.Do(req) |
| if err != nil { |
| return nil, err |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusOK { |
| return nil, fmt.Errorf("GET %s: unexpected status code: %d", req.URL, resp.StatusCode) |
| } |
| |
| // Uncompress the result. |
| r, err := gzip.NewReader(resp.Body) |
| if err != nil { |
| return nil, err |
| } |
| defer r.Close() |
| |
| return io.ReadAll(r) |
| } |
| |
| // localSource reads databases from a local directory. |
| // Intended for use in unit tests and screentests. |
| type localSource struct { |
| dir string |
| } |
| |
| func (db *localSource) get(ctx context.Context, endpoint string) ([]byte, error) { |
| return os.ReadFile(filepath.Join(db.dir, endpoint+".json")) |
| } |
| |
| // Create a new in-memory source for testing. |
| // Adapted from x/vulndb/internal/database.go. |
| func newInMemorySource(entries []*osv.Entry) (*inMemorySource, error) { |
| data := make(map[string][]byte) |
| db := DBMeta{} |
| modulesMap := make(map[string]*ModuleMeta) |
| vulnsMap := make(map[string]*VulnMeta) |
| for _, entry := range entries { |
| if entry.ID == "" { |
| return nil, fmt.Errorf("entry %v has no ID", entry) |
| } |
| if _, ok := vulnsMap[entry.ID]; ok { |
| return nil, fmt.Errorf("id %q appears twice", entry.ID) |
| } |
| if entry.Modified.After(db.Modified) { |
| db.Modified = entry.Modified |
| } |
| for _, affected := range entry.Affected { |
| modulePath := affected.Module.Path |
| if _, ok := modulesMap[modulePath]; !ok { |
| modulesMap[modulePath] = &ModuleMeta{ |
| Path: modulePath, |
| Vulns: []ModuleVuln{}, |
| } |
| } |
| module := modulesMap[modulePath] |
| module.Vulns = append(module.Vulns, ModuleVuln{ |
| ID: entry.ID, |
| Modified: entry.Modified, |
| Fixed: osv.LatestFixedVersion(affected.Ranges), |
| }) |
| } |
| vulnsMap[entry.ID] = &VulnMeta{ |
| ID: entry.ID, |
| Modified: entry.Modified, |
| Aliases: entry.Aliases, |
| } |
| b, err := json.Marshal(entry) |
| if err != nil { |
| return nil, err |
| } |
| data[idDir+"/"+entry.ID] = b |
| } |
| |
| b, err := json.Marshal(db) |
| if err != nil { |
| return nil, err |
| } |
| data[dbEndpoint] = b |
| |
| // Add the modules endpoint. |
| modules := make([]*ModuleMeta, 0, len(modulesMap)) |
| for _, module := range modulesMap { |
| modules = append(modules, module) |
| } |
| sort.SliceStable(modules, func(i, j int) bool { |
| return modules[i].Path < modules[j].Path |
| }) |
| for _, module := range modules { |
| sort.SliceStable(module.Vulns, func(i, j int) bool { |
| return module.Vulns[i].ID < module.Vulns[j].ID |
| }) |
| } |
| b, err = json.Marshal(modules) |
| if err != nil { |
| return nil, err |
| } |
| data[modulesEndpoint] = b |
| |
| // Add the vulns endpoint. |
| vulns := make([]*VulnMeta, 0, len(vulnsMap)) |
| for _, vuln := range vulnsMap { |
| vulns = append(vulns, vuln) |
| } |
| sort.SliceStable(vulns, func(i, j int) bool { |
| return vulns[i].ID < vulns[j].ID |
| }) |
| b, err = json.Marshal(vulns) |
| if err != nil { |
| return nil, err |
| } |
| data[vulnsEndpoint] = b |
| |
| return &inMemorySource{data: data}, nil |
| } |
| |
| // inMemorySource reads databases from an in-memory map. |
| // Intended for use in unit tests. |
| type inMemorySource struct { |
| data map[string][]byte |
| } |
| |
| func (db *inMemorySource) get(ctx context.Context, endpoint string) ([]byte, error) { |
| b, ok := db.data[endpoint] |
| if !ok { |
| return nil, fmt.Errorf("no data found at endpoint %q", endpoint) |
| } |
| return b, nil |
| } |