diff --git a/ext/vulnmdsrc/nvd/nvd.go b/ext/vulnmdsrc/nvd/nvd.go index 19f8dc1d..9ff44201 100644 --- a/ext/vulnmdsrc/nvd/nvd.go +++ b/ext/vulnmdsrc/nvd/nvd.go @@ -95,21 +95,11 @@ func (a *appender) BuildCache(datastore database.Datastore) error { log.WithError(err).WithField(logDataFeedName, dataFeedName).Error("could not open NVD data file") return commonerr.ErrCouldNotParse } - var nvd nvd r := bufio.NewReader(f) - if err := json.NewDecoder(r).Decode(&nvd); err != nil { - f.Close() - log.WithError(err).WithField(logDataFeedName, dataFeedName).Error("could not decode NVD data feed") - return commonerr.ErrCouldNotParse - } - - // For each entry of this data feed: - for _, nvdEntry := range nvd.Entries { - // Create metadata entry. - if metadata := nvdEntry.Metadata(); metadata != nil { - a.metadata[nvdEntry.Name()] = *metadata - } + if err := a.parseDataFeed(r); err != nil { + log.WithError(err).WithField(logDataFeedName, dataFeedName).Error("could not parse NVD data file") + return err } f.Close() } @@ -117,6 +107,23 @@ func (a *appender) BuildCache(datastore database.Datastore) error { return nil } +func (a *appender) parseDataFeed(r io.Reader) error { + var nvd nvd + + if err := json.NewDecoder(r).Decode(&nvd); err != nil { + return commonerr.ErrCouldNotParse + } + + for _, nvdEntry := range nvd.Entries { + // Create metadata entry. + if metadata := nvdEntry.Metadata(); metadata != nil { + a.metadata[nvdEntry.Name()] = *metadata + } + } + + return nil +} + func (a *appender) Append(vulnName string, appendFunc vulnmdsrc.AppendFunc) error { if nvdMetadata, ok := a.metadata[vulnName]; ok { appendFunc(appenderName, nvdMetadata, SeverityFromCVSS(nvdMetadata.CVSSv2.Score)) diff --git a/ext/vulnmdsrc/nvd/nvd_test.go b/ext/vulnmdsrc/nvd/nvd_test.go new file mode 100644 index 00000000..700b91dd --- /dev/null +++ b/ext/vulnmdsrc/nvd/nvd_test.go @@ -0,0 +1,93 @@ +// Copyright 2018 clair authors +// +// 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 ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNVDParser(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + path := filepath.Join(filepath.Dir(filename)) + + dataFilePath := filepath.Join(path, "/testdata/nvd_test.json") + testData, err := os.Open(dataFilePath) + if err != nil { + t.Fatalf("Error opening %q: %v", dataFilePath, err) + } + defer testData.Close() + + a := &appender{} + a.metadata = make(map[string]NVDMetadata) + + err = a.parseDataFeed(testData) + if err != nil { + t.Fatalf("Error parsing %q: %v", dataFilePath, err) + } + + var gotMetadata, wantMetadata NVDMetadata + + // Items without CVSSv2 aren't returned. + assert.Len(t, a.metadata, 2) + gotMetadata, ok := a.metadata["CVE-2002-0001"] + assert.False(t, ok) + + // Item with only CVSSv2. + gotMetadata, ok = a.metadata["CVE-2012-0001"] + assert.True(t, ok) + wantMetadata = NVDMetadata{ + CVSSv2: NVDmetadataCVSSv2{ + Vectors: "AV:N/AC:L/Au:S/C:P/I:N/A:N", + Score: 4.0, + }, + } + assert.Equal(t, wantMetadata, gotMetadata) + + // Item with both CVSSv2 and CVSSv3 has CVSSv2 information returned. + gotMetadata, ok = a.metadata["CVE-2018-0001"] + assert.True(t, ok) + wantMetadata = NVDMetadata{ + CVSSv2: NVDmetadataCVSSv2{ + Vectors: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + Score: 7.5, + }, + } + assert.Equal(t, wantMetadata, gotMetadata) +} + +func TestNVDParserErrors(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + path := filepath.Join(filepath.Dir(filename)) + + dataFilePath := filepath.Join(path, "/testdata/nvd_test_incorrect_format.json") + testData, err := os.Open(dataFilePath) + if err != nil { + t.Fatalf("Error opening %q: %v", dataFilePath, err) + } + defer testData.Close() + + a := &appender{} + a.metadata = make(map[string]NVDMetadata) + + err = a.parseDataFeed(testData) + if err == nil { + t.Fatalf("Expected error parsing NVD data file: %q", dataFilePath) + } +} diff --git a/ext/vulnmdsrc/nvd/testdata/nvd_test.json b/ext/vulnmdsrc/nvd/testdata/nvd_test.json new file mode 100644 index 00000000..eb878741 --- /dev/null +++ b/ext/vulnmdsrc/nvd/testdata/nvd_test.json @@ -0,0 +1,92 @@ +{ + "CVE_Items" : [ { + "_comment": "A CVE without CVSSv2 or CVSSv3", + "cve" : { + "data_type" : "CVE", + "data_format" : "MITRE", + "data_version" : "4.0", + "CVE_data_meta" : { + "ID" : "CVE-2002-0001" + } + }, + "impact" : { }, + "publishedDate" : "2018-01-10T22:29Z" + }, { + "_comment": "A CVE with only CVSSv2", + "cve" : { + "CVE_data_meta" : { + "ID" : "CVE-2012-0001" + } + }, + "impact" : { + "baseMetricV3" : { }, + "baseMetricV2" : { + "cvssV2" : { + "version" : "2.0", + "vectorString" : "(AV:N/AC:L/Au:S/C:P/I:N/A:N)", + "accessVector" : "NETWORK", + "accessComplexity" : "LOW", + "authentication" : "SINGLE", + "confidentialityImpact" : "PARTIAL", + "integrityImpact" : "NONE", + "availabilityImpact" : "NONE", + "baseScore" : 4.0 + }, + "severity" : "MEDIUM", + "exploitabilityScore" : 8.0, + "impactScore" : 2.9, + "obtainAllPrivilege" : false, + "obtainUserPrivilege" : false, + "obtainOtherPrivilege" : false, + "userInteractionRequired" : false + } + } + }, { + "_comment": "A CVE with standard CVSSv2 and CVSSv3", + "cve" : { + "CVE_data_meta" : { + "ID" : "CVE-2018-0001" + } + }, + "impact" : { + "baseMetricV3" : { + "cvssV3" : { + "version" : "3.0", + "vectorString" : "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector" : "NETWORK", + "attackComplexity" : "LOW", + "privilegesRequired" : "NONE", + "userInteraction" : "NONE", + "scope" : "UNCHANGED", + "confidentialityImpact" : "HIGH", + "integrityImpact" : "HIGH", + "availabilityImpact" : "HIGH", + "baseScore" : 9.8, + "baseSeverity" : "CRITICAL" + }, + "exploitabilityScore" : 3.9, + "impactScore" : 5.9 + }, + "baseMetricV2" : { + "cvssV2" : { + "version" : "2.0", + "vectorString" : "(AV:N/AC:L/Au:N/C:P/I:P/A:P)", + "accessVector" : "NETWORK", + "accessComplexity" : "LOW", + "authentication" : "NONE", + "confidentialityImpact" : "PARTIAL", + "integrityImpact" : "PARTIAL", + "availabilityImpact" : "PARTIAL", + "baseScore" : 7.5 + }, + "severity" : "HIGH", + "exploitabilityScore" : 10.0, + "impactScore" : 6.4, + "obtainAllPrivilege" : false, + "obtainUserPrivilege" : false, + "obtainOtherPrivilege" : false, + "userInteractionRequired" : false + } + } + } ] +} diff --git a/ext/vulnmdsrc/nvd/testdata/nvd_test_incorrect_format.json b/ext/vulnmdsrc/nvd/testdata/nvd_test_incorrect_format.json new file mode 100644 index 00000000..0aa25d8d --- /dev/null +++ b/ext/vulnmdsrc/nvd/testdata/nvd_test_incorrect_format.json @@ -0,0 +1 @@ +Not a JSON file.