Skip to content

Commit b80e2ef

Browse files
committed
✨ Add NVD importer
Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
1 parent a44d173 commit b80e2ef

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

vulnerabilities/importers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@
3737
from vulnerabilities.importers.openssl import OpenSSLDataSource
3838
from vulnerabilities.importers.ubuntu_usn import UbuntuUSNDataSource
3939
from vulnerabilities.importers.github import GitHubAPIDataSource
40+
from vulnerabilities.importers.nvd import NVDDataSource

vulnerabilities/importers/nvd.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright (c) nexB Inc. and others. All rights reserved.
2+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
3+
# The VulnerableCode software is licensed under the Apache License version 2.0.
4+
# Data generated with VulnerableCode require an acknowledgment.
5+
#
6+
# You may not use this software except in compliance with the License.
7+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software distributed
9+
# under the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR
10+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
# specific language governing permissions and limitations under the License.
12+
#
13+
# When you publish or redistribute any data created with VulnerableCode or any VulnerableCode
14+
# derivative work, you must accompany this data with the following acknowledgment:
15+
#
16+
# Generated with VulnerableCode and provided on an 'AS IS' BASIS, WITHOUT WARRANTIES
17+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
18+
# VulnerableCode should be considered or used as legal advice. Consult an Attorney
19+
# for any legal advice.
20+
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
23+
import gzip
24+
import json
25+
import dataclasses
26+
from dateutil import parser as dateparser
27+
from datetime import date
28+
29+
import requests
30+
31+
from vulnerabilities.data_source import DataSource
32+
from vulnerabilities.data_source import Reference
33+
from vulnerabilities.data_source import Advisory
34+
from vulnerabilities.data_source import DataSourceConfiguration
35+
36+
37+
@dataclasses.dataclass
38+
class NVDDataSourceConfiguration(DataSourceConfiguration):
39+
etags: dict
40+
41+
42+
BASE_URL = "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-{}.json.gz"
43+
44+
45+
class NVDDataSource(DataSource):
46+
47+
CONFIG_CLASS = NVDDataSourceConfiguration
48+
49+
def updated_advisories(self):
50+
years = [1]
51+
current_year = date.today().year
52+
# NVD json feeds start from 2002.
53+
for year in range(2002, current_year + 1):
54+
download_url = BASE_URL.format(year)
55+
if self.create_etag(download_url):
56+
data = self.fetch(download_url)
57+
yield self.to_advisories(data)
58+
59+
@staticmethod
60+
def fetch(url):
61+
gz_file = requests.get(url)
62+
data = gzip.decompress(gz_file.content)
63+
return json.loads(data)
64+
65+
def to_advisories(self, nvd_data):
66+
for cve_item in nvd_data["CVE_Items"]:
67+
if self.is_outdated(cve_item):
68+
continue
69+
70+
if self.related_to_hardware(cve_item):
71+
continue
72+
73+
cve_id = cve_item["cve"]["CVE_data_meta"]["ID"]
74+
references = self.extract_references(cve_item)
75+
summary = self.extract_summary(cve_item)
76+
yield Advisory(
77+
cve_id=cve_id, summary=summary, vuln_references=references, impacted_package_urls=[]
78+
)
79+
80+
@staticmethod
81+
def extract_summary(cve_item):
82+
# In 99% of cases len(cve_item['cve']['description']['description_data']) == 1 , so
83+
# this usually returns cve_item['cve']['description']['description_data'][0]['value']
84+
# In the remaining 1% cases this returns the longest summary.
85+
summaries = [desc["value"] for desc in cve_item["cve"]["description"]["description_data"]]
86+
return max(summaries, key=len)
87+
88+
def extract_references(self, cve_item):
89+
refs = []
90+
for reference in cve_item["cve"]["references"]["reference_data"]:
91+
ref_id = self.find_ref_id(reference)
92+
ref_url = reference["url"]
93+
94+
# Skip references which exceed db constraints
95+
if ref_id and len(ref_id) > 50:
96+
continue
97+
98+
refs.append(Reference(url=ref_url, reference_id=ref_id))
99+
100+
return refs
101+
102+
@staticmethod
103+
def find_ref_id(reference):
104+
if "https://" in reference["name"] or "http://" in reference["name"]:
105+
if "bugzilla" in reference["url"]:
106+
_, _, bugzilla_id = reference["url"].partition("?id=")
107+
return bugzilla_id
108+
109+
return ""
110+
111+
else:
112+
return reference["name"]
113+
114+
def is_outdated(self, cve_item):
115+
cve_last_modified_date = cve_item["lastModifiedDate"]
116+
cve_last_modified_date_obj = dateparser.parse(cve_last_modified_date)
117+
118+
if self.config.cutoff_date:
119+
return cve_last_modified_date_obj < self.config.cutoff_date
120+
121+
if self.config.last_run_date:
122+
return cve_last_modified_date_obj < self.config.last_run_date
123+
124+
return False
125+
126+
def related_to_hardware(self, cve_item):
127+
for cpe in self.extract_cpes(cve_item):
128+
cpe_comps = cpe.split(":")
129+
# CPE follow the format cpe:cpe_version:product_type:vendor:product
130+
if cpe_comps[2] == "h":
131+
return True
132+
133+
return False
134+
135+
@staticmethod
136+
def extract_cpes(cve_item):
137+
cpes = set()
138+
for node in cve_item["configurations"]["nodes"]:
139+
for cpe_data in node.get("cpe_match", []):
140+
cpes.add(cpe_data["cpe23Uri"])
141+
return cpes
142+
143+
def create_etag(self, url):
144+
etag = requests.head(url).headers.get("etag")
145+
if not etag:
146+
# Kind of inaccurate to return True since etag is
147+
# not created
148+
return True
149+
elif url in self.config.etags:
150+
if self.config.etags[url] == etag:
151+
return False
152+
self.config.etags[url] = etag
153+
return True

0 commit comments

Comments
 (0)