| // 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 genericosv provides utilities for working with generic | |
| // OSV structs (not specialized for Go). | |
| package genericosv | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "time" | |
| "golang.org/x/vulndb/internal/ghsa" | |
| "golang.org/x/vulndb/internal/report" | |
| ) | |
| // Entry is a a generic OSV entry, not specialized for Go. | |
| type Entry Vulnerability | |
| func NewFetcher() report.Fetcher { | |
| return &osvDevClient{http.DefaultClient, osvDevAPI} | |
| } | |
| func NewGHSAFetcher(gc ghsaClient) report.Fetcher { | |
| return &githubClient{Client: http.DefaultClient, gc: gc, url: githubAPI} | |
| } | |
| const ( | |
| osvDevAPI = "https://api.osv.dev/v1/vulns" | |
| githubAPI = "https://api.github.com/advisories" | |
| ) | |
| // Fetch returns the OSV entry from the osv.dev API for the given ID. | |
| func (c *osvDevClient) Fetch(_ context.Context, id string) (report.Source, error) { | |
| url := fmt.Sprintf("%s/%s", c.url, id) | |
| return get[Entry](c.Client, url) | |
| } | |
| type githubClient struct { | |
| *http.Client | |
| url string | |
| gc ghsaClient | |
| } | |
| // Fetch returns the OSV entry directly from the Github advisory repo | |
| // (https://github.com/github/advisory-database). | |
| // | |
| // This unfortunately requires two HTTP requests, the first to figure | |
| // out the published date of the GHSA, and the second to fetch the OSV. | |
| // | |
| // This is because the direct Github API returns a non-OSV format, | |
| // and the OSV files are available in a Github repo whose directory | |
| // structure is determined by the published year and month of each GHSA. | |
| func (c *githubClient) Fetch(ctx context.Context, id string) (report.Source, error) { | |
| sa, err := c.gc.FetchGHSA(ctx, id) | |
| if err != nil { | |
| return nil, err | |
| } | |
| pub := sa.PublishedAt | |
| if pub.IsZero() { | |
| return nil, fmt.Errorf("could not determine direct URL for GHSA OSV (need published date)") | |
| } | |
| githubURL := toGithubURL(id, pub) | |
| return get[Entry](c.Client, githubURL) | |
| } | |
| type ghsaClient interface { | |
| FetchGHSA(context.Context, string) (*ghsa.SecurityAdvisory, error) | |
| } | |
| func toGithubURL(id string, published time.Time) string { | |
| const base = "https://raw.githubusercontent.com/github/advisory-database/main/advisories/github-reviewed" | |
| year := published.Year() | |
| month := published.Month() | |
| return fmt.Sprintf("%s/%d/%02d/%s/%s.json", base, year, month, id, id) | |
| } | |
| type osvDevClient struct { | |
| *http.Client | |
| url string | |
| } | |
| func get[T any](cli *http.Client, url string) (*T, error) { | |
| var zero *T | |
| req, err := http.NewRequest(http.MethodGet, url, nil) | |
| if err != nil { | |
| return zero, err | |
| } | |
| resp, err := cli.Do(req) | |
| if err != nil { | |
| return zero, err | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != http.StatusOK { | |
| return zero, fmt.Errorf("HTTP GET %s returned unexpected status code %d", url, resp.StatusCode) | |
| } | |
| v := new(T) | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return zero, err | |
| } | |
| if err := json.Unmarshal(body, v); err != nil { | |
| return zero, err | |
| } | |
| return v, nil | |
| } | |
| func (e *Entry) AffectsGo() bool { | |
| for _, a := range e.Affected { | |
| if a.Package.Ecosystem == EcosystemGo { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func (e *Entry) IsWithdrawn() bool { | |
| return !e.Withdrawn.IsZero() | |
| } |