providers/nvd/cpe.go (180 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package nvd import ( "context" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "github.com/facebookincubator/flog" "github.com/facebookincubator/nvdtools/providers/lib/client" ) // CPE defines the CPE data feed for synchronization. type CPE int // Supported CPE feeds. const ( cpe23xmlGz CPE = iota // CPE database in XML 2.3 format, gzip compressed. cpe23xmlZip // CPE database in XML 2.3 format, zip compressed. cpe22xmlGz // CPE database in XML 2.2 format, gzip compressed. cpe22xmlZip // CPE database in XML 2.2 format, zip compressed. ) // SupportedCPE contains all supported CPE data feeds indexed by name. var SupportedCPE = map[string]CPE{ "cpe-2.2.xml.gz": cpe22xmlGz, "cpe-2.2.xml.zip": cpe22xmlZip, "cpe-2.3.xml.gz": cpe23xmlGz, "cpe-2.3.xml.zip": cpe23xmlZip, } // Set implements the flag.Value interface. func (c *CPE) Set(v string) error { feed, exists := SupportedCPE[v] if !exists { return fmt.Errorf("unsupported CPE feed: %q", v) } *c = feed return nil } // String implements the fmt.Stringer interface. func (c CPE) String() string { return "cpe-" + c.version() + ".xml." + c.compression() } // Help returns the CPE flag help. func (c CPE) Help() string { opts := make([]string, 0, len(SupportedCPE)) for k := range SupportedCPE { opts = append(opts, k) } sort.Strings(opts) return fmt.Sprintf( "CPE feed to sync (default: %s)\navailable:\n%s", c, strings.Join(opts, "\n"), ) } // compression returns the data feed compression: gz or zip. func (c CPE) compression() string { switch c { case cpe22xmlGz, cpe23xmlGz: return "gz" case cpe22xmlZip, cpe23xmlZip: return "zip" default: panic("unsupported CPE compression") } } // version returns the data feed version. func (c CPE) version() string { switch c { case cpe22xmlGz, cpe22xmlZip: return "2.2" case cpe23xmlGz, cpe23xmlZip: return "2.3" default: panic("unsupported CPE version") } } // Sync synchronizes the CPE feed to a local directory. func (c CPE) Sync(ctx context.Context, src SourceConfig, localdir string) error { basename := "official-cpe-dictionary_v" + c.version() cf := cpeFile{ CPE: c, EtagFile: basename + ".etag", DataFile: basename + ".xml." + c.compression(), } return cf.Sync(ctx, src, localdir) } type cpeFile struct { CPE EtagFile string DataFile string } func (cf cpeFile) baseURL(src SourceConfig) string { u := url.URL{ Scheme: src.Scheme, Host: src.Host, Path: src.CPEFeedPath, } baseURL := u.String() if !strings.HasSuffix(baseURL, "/") { baseURL += "/" } return baseURL } func (cf cpeFile) Sync(ctx context.Context, src SourceConfig, localdir string) error { baseURL := cf.baseURL(src) sourceURL := baseURL + cf.DataFile needsUpdate, err := cf.needsUpdate(ctx, sourceURL, localdir) if err != nil { return err } if !needsUpdate { return nil } etag, tempDataFilename, err := cf.download(ctx, sourceURL) if err != nil { return err } defer os.Remove(tempDataFilename) // write etag file etagFilename := filepath.Join(localdir, cf.EtagFile) err = ioutil.WriteFile(etagFilename, []byte(etag), 0644) if err != nil { return err } // write data file dataFilename := filepath.Join(localdir, cf.DataFile) bakDataFilename := dataFilename + ".bak" xRename(dataFilename, bakDataFilename) if err = xRename(tempDataFilename, dataFilename); err != nil { xRename(bakDataFilename, dataFilename) return err } os.Remove(bakDataFilename) return nil } func (cf cpeFile) needsUpdate(ctx context.Context, targetURL, localdir string) (bool, error) { flog.V(1).Infof("checking etag for %q", targetURL) req, err := httpNewRequestContext(ctx, "HEAD", targetURL) if err != nil { return false, err } resp, err := client.Default().Do(req) if err != nil { return false, err } defer resp.Body.Close() if err = httpResponseNotOK(resp); err != nil { return false, err } remoteEtag := resp.Header.Get("Etag") if remoteEtag == "" { return false, fmt.Errorf("server not returning etag header for %q", targetURL) } etagBytes, err := ioutil.ReadFile(filepath.Join(localdir, cf.EtagFile)) if err != nil { flog.V(1).Infof("etag file %q for not exist in %q, needs sync", cf.EtagFile, localdir) return true, nil } localEtag := string(etagBytes) if localEtag != remoteEtag { flog.V(1).Infof("data file %q needs update in %q: hash mismatch %q != %q", cf.DataFile, localdir, localEtag, remoteEtag) return true, nil } return false, nil } // download file from targetURL, returns etag and path to local file. func (cf cpeFile) download(ctx context.Context, targetURL string) (string, string, error) { flog.V(1).Infof("downloading data file %q", targetURL) req, err := http.NewRequest("GET", targetURL, nil) if err != nil { return "", "", err } resp, err := client.Default().Do(req.WithContext(ctx)) if err != nil { return "", "", err } defer resp.Body.Close() if err = httpResponseNotOK(resp); err != nil { return "", "", err } dataFile, err := ioutil.TempFile("", "nvdsync-data-") if err != nil { return "", "", err } _, err = io.Copy(dataFile, resp.Body) if err != nil { return "", "", err } return resp.Header.Get("Etag"), dataFile.Name(), nil }