providers/rustsec/rustsec.go (246 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 rustsec provides a converter for rustsec advisories to nvd. package rustsec import ( "io" "io/ioutil" "os" "path/filepath" "regexp" "sort" "strings" "time" nvd "github.com/facebookincubator/nvdtools/cvefeed/nvd/schema" "github.com/facebookincubator/nvdtools/wfn" "github.com/BurntSushi/toml" "github.com/pkg/errors" ) // Convert scans a directory recursively for rustsec advisory files and convert to NVD CVE JSON 1.0 format. func Convert(dir string) (*nvd.NVDCVEFeedJSON10, error) { feed := &nvd.NVDCVEFeedJSON10{} walker := func(path string, info os.FileInfo, err error) error { if err != nil { return err } _, fn := filepath.Split(path) if !(strings.HasPrefix(fn, "RUSTSEC") && strings.HasSuffix(fn, ".md")) { return nil } f, err := os.Open(path) if err != nil { return err } defer f.Close() cve, err := ConvertAdvisory(f) if err != nil { return errors.Wrapf(err, "error parsing file: %s", path) } feed.CVEItems = append(feed.CVEItems, cve) return nil } err := filepath.Walk(dir, walker) if err != nil { return nil, err } return feed, nil } // ConvertAdvisory converts the rustsec toml advisory data from r to NVD CVE JSON 1.0 format. func ConvertAdvisory(r io.Reader) (*nvd.NVDCVEFeedJSON10DefCVEItem, error) { var spec advisoryFile bs, err := ioutil.ReadAll(r) if err != nil { return nil, errors.Wrap(err, "cannot read RUSTSEC file") } reg := regexp.MustCompile("(?s)" + // global flag - . also matches newline "```toml\\n(.+?)```" + // 1 group - match toml part (between ```toml and ```) in lazy mode "(?s:[\\s\\n]*)" + // match any number of new lines and spaces before first # "#\\s*(\\S.*?)\\n" + // get title - from # to new line in lazy mode "(?s:[\\s\\n]*)" + // skip the newlines and spaces "(.*)") // everything else is description data := reg.FindStringSubmatch(string(bs)) if (data == nil) || (len(data) != 4) { return nil, errors.New("cannot parse md advisory structure") } _, err = toml.Decode(data[1], &spec) if err != nil { return nil, errors.Wrap(err, "cannot decode rustsec toml advisory part of md file") } spec.Item.Title = data[2] spec.Item.Description = data[3] return spec.Item.Convert() } // advisoryFile is the toml spec for rustsec advisories. // Ref: https://github.com/RustSec/advisory-db type advisoryFile struct { Item advisoryItem `toml:"advisory"` } type advisoryItem struct { ID string `toml:"id"` Package string `toml:"package"` Date string `toml:"date"` Title string `toml:"title"` Description string `toml:"description"` URL string `toml:"url"` Aliases []string `toml:"aliases"` Keywords []string `toml:"keywords"` References []string `toml:"references"` PatchedVersions []string `toml:"patched_versions"` AffectedArch []string `toml:"affected_arch"` AffectedOS []string `toml:"affected_os"` AffectedFunctions []string `toml:"affected_functions"` UnaffectedVersions []string `toml:"unaffected_versions"` } const advisoryTimeLayout = "2006-01-02" func (item *advisoryItem) Convert() (*nvd.NVDCVEFeedJSON10DefCVEItem, error) { // TODO: Add CVSS score: https://github.com/RustSec/advisory-db/issues/20 t, err := time.Parse(advisoryTimeLayout, item.Date) if err != nil { return nil, errors.Wrapf(err, "malformed date layout in %#v: %q", item, item.Date) } conf, err := item.newConfigurations() if err != nil { return nil, err } cve := &nvd.NVDCVEFeedJSON10DefCVEItem{ CVE: &nvd.CVEJSON40{ CVEDataMeta: &nvd.CVEJSON40CVEDataMeta{ ID: item.ID, ASSIGNER: "RustSec", }, DataFormat: "MITRE", DataType: "CVE", DataVersion: "4.0", Description: &nvd.CVEJSON40Description{ DescriptionData: []*nvd.CVEJSON40LangString{ { Lang: "en", Value: item.Description, }, }, }, References: item.newReferences(), }, Configurations: conf, LastModifiedDate: t.Format(nvd.TimeLayout), PublishedDate: t.Format(nvd.TimeLayout), } return cve, nil } func (item *advisoryItem) newReferences() *nvd.CVEJSON40References { if len(item.References) == 0 { return nil } nrefs := 1 + len(item.Aliases) + len(item.References) refs := &nvd.CVEJSON40References{ ReferenceData: make([]*nvd.CVEJSON40Reference, 0, nrefs), } addRef := func(name, url string) { refs.ReferenceData = append(refs.ReferenceData, &nvd.CVEJSON40Reference{ Name: name, URL: url, }) } if item.Title != "" || item.URL != "" { addRef(item.Title, item.URL) } for _, ref := range item.Aliases { addRef(ref, "") } for _, ref := range item.References { addRef(ref, "") } rd := refs.ReferenceData sort.Slice(rd, func(i, j int) bool { return strings.Compare(rd[i].Name, rd[j].Name) < 0 }) return refs } func (item *advisoryItem) newConfigurations() (*nvd.NVDCVEFeedJSON10DefConfigurations, error) { pkg, err := wfn.WFNize(item.Package) if err != nil { return nil, errors.Wrapf(err, "cannot wfn-ize: %q", item.Package) } cpe := wfn.Attributes{Part: "a", Product: pkg} cpe22uri := cpe.BindToURI() cpe23uri := cpe.BindToFmtString() matches := []*nvd.NVDCVEFeedJSON10DefCPEMatch{} unnafected := append(item.UnaffectedVersions, item.PatchedVersions...) for _, version := range unnafected { if len(version) < 2 { return nil, errors.Errorf("malformed version schema in %#v: %q", item, version) } var curver string switch version[:1] { case "=", "^": curver = strings.TrimSpace(version[1:]) wfnver, err := wfn.WFNize(curver) if err != nil { return nil, errors.Wrapf(err, "cannot wfn-ize version: %q", curver) } cpe := wfn.Attributes{Part: "a", Product: pkg, Version: wfnver} cpe22uri := cpe.BindToURI() cpe23uri := cpe.BindToFmtString() match := &nvd.NVDCVEFeedJSON10DefCPEMatch{ CPEName: []*nvd.NVDCVEFeedJSON10DefCPEName{ { Cpe22Uri: cpe22uri, Cpe23Uri: cpe23uri, }, }, Cpe23Uri: cpe23uri, Vulnerable: version[:1] == "=", } matches = append(matches, match) case ">", "<": match := &nvd.NVDCVEFeedJSON10DefCPEMatch{ CPEName: []*nvd.NVDCVEFeedJSON10DefCPEName{ { Cpe22Uri: cpe22uri, Cpe23Uri: cpe23uri, }, }, Cpe23Uri: cpe23uri, Vulnerable: false, // these are patched + unaffected versions } curver = strings.TrimSpace(version[2:]) switch version[:2] { case "> ": match.VersionStartExcluding = curver case ">=": match.VersionStartIncluding = curver case "< ": match.VersionEndExcluding = curver case "<=": match.VersionEndIncluding = curver default: return nil, errors.Errorf("malformed version schema in %#v: %q", item, version) } matches = append(matches, match) default: return nil, errors.Errorf("malformed version schema in %#v: %q", item, version) } } conf := &nvd.NVDCVEFeedJSON10DefConfigurations{ CVEDataVersion: "4.0", Nodes: []*nvd.NVDCVEFeedJSON10DefNode{ { Operator: "AND", Children: []*nvd.NVDCVEFeedJSON10DefNode{ { CPEMatch: []*nvd.NVDCVEFeedJSON10DefCPEMatch{ { CPEName: []*nvd.NVDCVEFeedJSON10DefCPEName{ { Cpe22Uri: cpe22uri, Cpe23Uri: cpe23uri, }, }, Cpe23Uri: cpe23uri, Vulnerable: false, VersionStartIncluding: "0", }, }, }, { Negate: true, CPEMatch: matches, }, }, }, }, } return conf, nil }