diff options
author | Paul Duncan <pabs@pablotron.org> | 2022-02-22 19:59:26 -0500 |
---|---|---|
committer | Paul Duncan <pabs@pablotron.org> | 2022-02-22 19:59:26 -0500 |
commit | a4b802d22fa0940fd1862f2176dfb41f4a1be973 (patch) | |
tree | 5171b7fac94031ee9eba208a4afc25c125d2b963 /nvdmirror/nvdmirror.go | |
parent | 041f114958746a29121ec9e8b0672ca8a9a701d1 (diff) | |
download | cvez-a4b802d22fa0940fd1862f2176dfb41f4a1be973.tar.bz2 cvez-a4b802d22fa0940fd1862f2176dfb41f4a1be973.zip |
add nvdmirror, including cache iface, jsoncache impl, Sync() func, and test data
Diffstat (limited to 'nvdmirror/nvdmirror.go')
-rw-r--r-- | nvdmirror/nvdmirror.go | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/nvdmirror/nvdmirror.go b/nvdmirror/nvdmirror.go new file mode 100644 index 0000000..7b092b1 --- /dev/null +++ b/nvdmirror/nvdmirror.go @@ -0,0 +1,184 @@ +// mirror files from upstream NVD source +package nvdmirror + +import ( + "fmt" + "github.com/rs/zerolog/log" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "time" +) + +// URLs +type Urls struct { + // CVE 1.1 Base URL. The full meta and JSON URLs are constructed from + // this appended to this base.. + Cve11Base string + + // CPE 1.0 match meta URL + Cpe10MatchMeta string + + // CPE 1.0 match URL + Cpe10MatchJson string + + // CPE 2.3 dictionary URL + Cpe23Dict string +} + +// NVD URLs +var DefaultUrls = Urls { + Cve11Base: "https://nvd.nist.gov/feeds/json/cve/1.1", + Cpe10MatchMeta: "https://nvd.nist.gov/feeds/json/cpematch/1.0/nvdcpematch-1.0.meta", + Cpe10MatchJson: "https://nvd.nist.gov/feeds/json/cpematch/1.0/nvdcpematch-1.0.gz", + Cpe23Dict: "https://nvd.nist.gov/feeds/xml/cpe/dictionary/official-cpe-dictionary_v2.3.xml.gz", +} + +var extraFiles = []string { + "nvdcve-1.1-modified", + "nvdcve-1.1-recent", +} + +// fetch result +type fetchResult struct { + src string // source URL + err error // fetch result + notModified bool // was the result unmodified? + lastModified string // last modified response header + etag string // etag response header +} + +// base CVE year +const baseYear = 2002 + +// default user agent (FIXME: make configurable) +var userAgent = "cvez/0.1.0" + +func fetch(ch chan fetchResult, client *http.Client, cache Cache, srcUrl string, dstDir string) { + // parse source url + src, err := url.Parse(srcUrl) + if err != nil { + ch <- fetchResult { src: srcUrl, err: err } + return + } + + // build destination path + path := filepath.Join(dstDir, filepath.Base(src.Path)) + log.Debug().Str("url", srcUrl).Str("path", path).Send() + + // create temporary output file + f, err := os.CreateTemp(filepath.Dir(path), "") + if err != nil { + ch <- fetchResult { src: srcUrl, err: err } + return + } + defer os.Remove(f.Name()) + + // create request + req, err := http.NewRequest("GET", srcUrl, nil) + if err != nil { + ch <- fetchResult { src: srcUrl, err: err } + return + } + + // add request headers + req.Header.Add("user-agent", userAgent) + if headers, ok := cache.Get(srcUrl); ok { + for k, v := range(headers) { + req.Header.Add(k, v) + } + } + + // send request + resp, err := client.Do(req) + if err != nil { + ch <- fetchResult { src: srcUrl, err: err } + return + } + defer resp.Body.Close() + + switch resp.StatusCode { + case 200: + // copy body to result + if size, err := io.Copy(f, resp.Body); err != nil { + // copy failed + ch <- fetchResult { src: srcUrl, err: err } + } else if err = os.Rename(f.Name(), path); err != nil { + // rename failed + ch <- fetchResult { src: srcUrl, err: err } + } else { + log.Debug().Str("url", srcUrl).Int64("size", size).Send() + ch <- fetchResult { + src: srcUrl, + lastModified: resp.Header.Get("last-modified"), + etag: resp.Header.Get("etag"), + } + } + case 304: + ch <- fetchResult { src: srcUrl, notModified: true } + default: + code := resp.StatusCode + err := fmt.Errorf("%d: %s", code, http.StatusText(code)) + ch <- fetchResult { src: srcUrl, err: err } + } +} + +func Sync(urls Urls, cache Cache, dstDir string) error { + log.Debug().Str("dstDir", dstDir).Msg("Sync") + + // create fetch result channel + ch := make(chan fetchResult) + + // create shared transport and client + tr := &http.Transport{ + // FIXME: make configurable + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + } + client := &http.Client{Transport: tr} + + // calculate total number of years + numYears := time.Now().Year() - 2002 + 1 + + // fetch metas + for year := baseYear; year < baseYear + numYears; year++ { + // build url + url := fmt.Sprintf("%s/nvdcve-1.1-%04d.meta", urls.Cve11Base, year) + log.Debug().Int("year", year).Str("url", url).Send() + go fetch(ch, client, cache, url, dstDir) + } + + for _, s := range(extraFiles) { + url := fmt.Sprintf("%s/%s.meta", urls.Cve11Base, s) + log.Debug().Str("file", s).Str("url", url).Send() + go fetch(ch, client, cache, url, dstDir) + } + + // read results + for i := 0; i < numYears + len(extraFiles); i++ { + if r := <-ch; r.err != nil { + log.Error().Str("url", r.src).Err(r.err).Send() + // FIXME: errs = append(errs, r) + } else { + log.Info().Str("url", r.src).Str("etag", r.etag).Msg("ok") + + if !r.notModified { + // cache headers + err := cache.Set(r.src, map[string]string { + "if-none-match": r.etag, + "if-modified-since": r.lastModified, + }) + + if err != nil { + log.Error().Str("url", r.src).Err(r.err).Msg("Set") + return err + } + } + } + } + + // return success + return nil +} |