// mirror files from upstream NVD source package nvdmirror import ( "fmt" "github.com/rs/zerolog/log" "io" "net/http" "net/url" "os" "path/filepath" "time" ) // default user agent (FIXME: make configurable) var defaultUserAgent = "cvez/0.1.0" // Sync() configuration. type SyncConfig struct { // CVE 1.1 Base URL. The full meta and JSON URLs are constructed by // appending the file name to this base. Cve11BaseUrl string // CPE 1.0 base URL. The full meta and JSON URLs are constructed by // appending the file name to this base. Cpe10MatchBaseUrl string // CPE 2.3 dictionary URL. Cpe23DictUrl string // User agent string. Set to "" for default user agent string. UserAgent string // Maximum number of idle connections. MaxIdleConns int // Idle connection timeout. IdleConnTimeout time.Duration } // Get user agent string. func (me SyncConfig) GetUserAgent() string { if len(me.UserAgent) > 0 { return me.UserAgent } else { return defaultUserAgent } } // NVD URLs var DefaultConfig = SyncConfig { Cve11BaseUrl: "https://nvd.nist.gov/feeds/json/cve/1.1", Cpe10MatchBaseUrl: "https://nvd.nist.gov/feeds/json/cpematch/1.0", Cpe23DictUrl: "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 modified bool // was the result modified? lastModified string // last modified response header etag string // etag response header } // base CVE year const baseYear = 2002 func fetch(ch chan fetchResult, config SyncConfig, cache Cache, client *http.Client, 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", config.GetUserAgent()) 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, modified: false } } else if err = os.Rename(f.Name(), path); err != nil { // rename failed ch <- fetchResult { src: srcUrl, err: err, modified: false } } else { log.Debug().Str("url", srcUrl).Int64("size", size).Send() ch <- fetchResult { src: srcUrl, modified: true, lastModified: resp.Header.Get("last-modified"), etag: resp.Header.Get("etag"), } } case 304: ch <- fetchResult { src: srcUrl, modified: false } default: code := resp.StatusCode err := fmt.Errorf("%d: %s", code, http.StatusText(code)) ch <- fetchResult { src: srcUrl, err: err } } } func Sync(config SyncConfig, 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{ MaxIdleConns: config.MaxIdleConns, IdleConnTimeout: config.IdleConnTimeout, } client := &http.Client{Transport: tr} // calculate total number of years numYears := time.Now().Year() - baseYear + 1 // fetch cve feed metas for year := baseYear; year < baseYear + numYears; year++ { // build url url := fmt.Sprintf("%s/nvdcve-1.1-%04d.meta", config.Cve11BaseUrl, year) log.Debug().Int("year", year).Str("url", url).Send() go fetch(ch, config, cache, client, url, dstDir) } // fetch cve extra file metas for _, s := range(extraFiles) { url := fmt.Sprintf("%s/%s.meta", config.Cve11BaseUrl, s) log.Debug().Str("file", s).Str("url", url).Send() go fetch(ch, config, cache, client, 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.modified { // build request headers headers := map[string]string { "if-none-match": r.etag, "if-modified-since": r.lastModified, } // save headers to cache if err := cache.Set(r.src, headers); err != nil { log.Error().Str("url", r.src).Err(r.err).Msg("Set") return err } } } } // TODO: fetch cpe dictionary // TODO: fetch cpematch // "nvdcpematch-1.0.{meta,json}", // return success return nil }