| // 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 cveutils |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "net/http" |
| "strings" |
| "time" |
| |
| "golang.org/x/exp/slices" |
| ) |
| |
| const deltaLogURL = "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/deltaLog.json" |
| |
| // List returns the ids for all CVEs added or updated in the |
| // cvelistV5 repo at or after the given 'since' time. |
| // |
| // The value of "since" must be within the past month, because |
| // the CVEs are pulled from a log maintained by the CVE program |
| // which only contains updates from the past month. |
| func List(since time.Time) ([]string, error) { |
| return list(http.DefaultClient, deltaLogURL, since) |
| } |
| |
| // Latest returns the ids of all CVEs that were added or updated |
| // in the past month, according to the latest version of the "delta log". |
| func Latest() ([]string, error) { |
| return latest(http.DefaultClient, deltaLogURL) |
| } |
| |
| type deltaLog []*updateMeta |
| |
| type updateMeta struct { |
| FetchTime string `json:"fetchTime"` |
| New []*cveMeta `json:"new,omitempty"` |
| Updated []*cveMeta `json:"updated,omitempty"` |
| } |
| |
| type cveMeta struct { |
| ID string `json:"cveId"` |
| // The documentation isn't 100% clear, but this value appears to be |
| // pulled directly from the CVE record field cveMetadata.dateUpdated. |
| // The value cveMetadata.dateUpdated is changed when a |
| // CNA makes an update to a CVE record, but not when MITRE makes an update |
| // to the CVE that wasn't initiated by the CNA. As far as we are aware, |
| // this only happens in cases where new references are added. For this reason, |
| // CVEs that were considered updated a long time ago |
| // can appear in the current delta log. (See for example CVE-2023-26035 in |
| // testdata/deltaLog.txtar, which was last "updated" in Feb 2023 but appears |
| // in the log for Nov-Dec 2023.) |
| // |
| // For purposes of the List function, we will consider this value (the CNA update time) |
| // to be canonical. This is OK because references added by MITRE would not change |
| // our triage decision about a CVE. |
| Updated string `json:"dateUpdated"` |
| } |
| |
| var errSinceTooEarly = errors.New("earliest entry in delta log is after since") |
| |
| func list(c *http.Client, url string, since time.Time) ([]string, error) { |
| b, err := fetch(c, url) |
| if err != nil { |
| return nil, err |
| } |
| |
| var dl deltaLog |
| if err := json.Unmarshal(b, &dl); err != nil { |
| return nil, err |
| } |
| |
| if earliest, err := dl.earliest(); err != nil { |
| return nil, err |
| } else if earliest.After(since) { |
| return nil, fmt.Errorf("%w (earliest=%s, since=%s)", errSinceTooEarly, earliest, since) |
| } |
| |
| var cves []string |
| for _, um := range dl { |
| fetched, err := parseTime(um.FetchTime) |
| if err != nil { |
| return nil, err |
| } |
| if fetched.Before(since) { |
| continue |
| } |
| for _, c := range um.cves() { |
| updated, err := parseTime(c.Updated) |
| if err != nil { |
| return nil, err |
| } |
| if updated.Before(since) { |
| continue |
| } |
| cves = append(cves, c.ID) |
| } |
| } |
| |
| // Remove any duplicates. |
| slices.Sort(cves) |
| cves = slices.Compact(cves) |
| |
| return cves, nil |
| } |
| |
| func latest(c *http.Client, url string) ([]string, error) { |
| b, err := fetch(c, url) |
| if err != nil { |
| return nil, err |
| } |
| |
| var dl deltaLog |
| if err := json.Unmarshal(b, &dl); err != nil { |
| return nil, err |
| } |
| |
| var cves []string |
| for _, um := range dl { |
| for _, c := range um.cves() { |
| cves = append(cves, c.ID) |
| } |
| } |
| |
| // Remove any duplicates. |
| slices.Sort(cves) |
| cves = slices.Compact(cves) |
| |
| return cves, nil |
| } |
| |
| func fetch(c *http.Client, url string) ([]byte, error) { |
| resp, err := c.Get(url) |
| if err != nil { |
| return nil, err |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusOK { |
| return nil, fmt.Errorf("HTTP GET %s returned non-OK status %s", url, resp.Status) |
| } |
| return io.ReadAll(resp.Body) |
| } |
| |
| func parseTime(s string) (time.Time, error) { |
| t, err := time.Parse(time.RFC3339Nano, s) |
| if err != nil { |
| // Try adding a "Z" if the time string doesn't have one. |
| if !strings.HasSuffix(s, "Z") { |
| return time.Parse(time.RFC3339Nano, s+"Z") |
| } |
| return time.Time{}, err |
| } |
| return t, nil |
| } |
| |
| func (um *updateMeta) cves() []*cveMeta { |
| return append(slices.Clone(um.New), um.Updated...) |
| } |
| |
| // earliest returns the earliest fetch time in the deltaLog, |
| // assuming the updateMeta entries are sorted from latest to earliest |
| // fetch time. |
| func (dl deltaLog) earliest() (time.Time, error) { |
| last := dl[len(dl)-1] |
| return parseTime(last.FetchTime) |
| } |