|
20 | 20 | # VulnerableCode is a free software code scanning tool from nexB Inc. and others. |
21 | 21 | # Visit https://github.com/nexB/vulnerablecode/ for support and download. |
22 | 22 |
|
23 | | -import dataclasses |
24 | 23 | import gzip |
25 | 24 | import json |
26 | 25 | from datetime import date |
| 26 | +from typing import Iterable |
27 | 27 |
|
28 | 28 | import requests |
29 | 29 | from dateutil import parser as dateparser |
| 30 | +from django.db.models.query import QuerySet |
30 | 31 |
|
31 | | -from vulnerabilities.helpers import create_etag |
32 | | -from vulnerabilities.importer import Advisory |
| 32 | +from vulnerabilities.helpers import get_item |
| 33 | +from vulnerabilities.importer import AdvisoryData |
33 | 34 | from vulnerabilities.importer import Importer |
34 | 35 | from vulnerabilities.importer import Reference |
35 | 36 | from vulnerabilities.importer import VulnerabilitySeverity |
36 | | -from vulnerabilities.severity_systems import scoring_systems |
37 | | - |
38 | | -BASE_URL = "https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-{}.json.gz" |
| 37 | +from vulnerabilities.improver import Improver |
| 38 | +from vulnerabilities.improver import Inference |
| 39 | +from vulnerabilities.models import Advisory |
| 40 | +from vulnerabilities.severity_systems import SCORING_SYSTEMS |
39 | 41 |
|
40 | 42 |
|
41 | 43 | class NVDImporter(Importer): |
42 | | - def updated_advisories(self): |
| 44 | + # See https://github.com/nexB/vulnerablecode/issues/665 for follow up |
| 45 | + spdx_license_expression = "LicenseRef-scancode-unknown" |
| 46 | + |
| 47 | + def advisory_data(self): |
| 48 | + advisory_data = [] |
43 | 49 | current_year = date.today().year |
44 | 50 | # NVD json feeds start from 2002. |
45 | 51 | for year in range(2002, current_year + 1): |
46 | | - download_url = BASE_URL.format(year) |
47 | | - # Etags are like hashes of web responses. We maintain |
48 | | - # (url, etag) mappings in the DB. `create_etag` creates |
49 | | - # (url, etag) pair. If a (url, etag) already exists then the code |
50 | | - # skips processing the response further to avoid duplicate work |
51 | | - if create_etag(data_src=self, url=download_url, etag_key="etag"): |
52 | | - data = self.fetch(download_url) |
53 | | - yield self.to_advisories(data) |
54 | | - |
55 | | - @staticmethod |
56 | | - def fetch(url): |
57 | | - gz_file = requests.get(url) |
58 | | - data = gzip.decompress(gz_file.content) |
59 | | - return json.loads(data) |
60 | | - |
61 | | - def to_advisories(self, nvd_data): |
62 | | - for cve_item in nvd_data["CVE_Items"]: |
63 | | - if self.is_outdated(cve_item): |
64 | | - continue |
65 | | - |
66 | | - if self.related_to_hardware(cve_item): |
67 | | - continue |
68 | | - |
69 | | - cve_id = cve_item["cve"]["CVE_data_meta"]["ID"] |
70 | | - ref_urls = self.extract_reference_urls(cve_item) |
71 | | - references = [Reference(url=url) for url in ref_urls] |
72 | | - severity_scores = self.extract_severity_scores(cve_item) |
| 52 | + download_url = f"https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-{year}.json.gz" |
| 53 | + data = fetch(download_url) |
| 54 | + advisory_data.extend(to_advisories(data)) |
| 55 | + return advisory_data |
| 56 | + |
| 57 | + |
| 58 | +# Isolating network calls for simplicity of testing |
| 59 | +def fetch(url): |
| 60 | + gz_file = requests.get(url) |
| 61 | + data = gzip.decompress(gz_file.content) |
| 62 | + return json.loads(data) |
| 63 | + |
| 64 | + |
| 65 | +def extract_summary(cve_item): |
| 66 | + """ |
| 67 | + Return a summary for a given CVE item. |
| 68 | + """ |
| 69 | + # In 99% of cases len(cve_item['cve']['description']['description_data']) == 1 , so |
| 70 | + # this usually returns cve_item['cve']['description']['description_data'][0]['value'] |
| 71 | + # In the remaining 1% cases this returns the longest summary. |
| 72 | + summaries = [] |
| 73 | + for desc in get_item(cve_item, "cve", "description", "description_data") or []: |
| 74 | + if desc.get("value"): |
| 75 | + summaries.append(desc["value"]) |
| 76 | + return max(summaries, key=len) if summaries else None |
| 77 | + |
| 78 | + |
| 79 | +def to_advisories(nvd_data): |
| 80 | + """ |
| 81 | + Yield AdvisoryData objects from a NVD json feed. |
| 82 | + """ |
| 83 | + for cve_item in nvd_data.get("CVE_Items") or []: |
| 84 | + cpes = extract_cpes(cve_item) |
| 85 | + if related_to_hardware(cpes): |
| 86 | + continue |
| 87 | + |
| 88 | + aliases = [] |
| 89 | + cve_id = get_item(cve_item, "cve", "CVE_data_meta", "ID") |
| 90 | + ref_urls = extract_reference_urls(cve_item) |
| 91 | + references = [] |
| 92 | + severity_scores = list(extract_severity_scores(cve_item)) |
| 93 | + for cpe in cpes: |
73 | 94 | references.append( |
74 | 95 | Reference( |
75 | | - url=f"https://nvd.nist.gov/vuln/detail/{cve_id}", |
76 | | - reference_id=cve_id, |
77 | | - severities=severity_scores, |
78 | | - ) |
79 | | - ) |
80 | | - summary = self.extract_summary(cve_item) |
81 | | - yield Advisory( |
82 | | - vulnerability_id=cve_id, |
83 | | - summary=summary, |
84 | | - references=references, |
85 | | - ) |
86 | | - |
87 | | - @staticmethod |
88 | | - def extract_summary(cve_item): |
89 | | - # In 99% of cases len(cve_item['cve']['description']['description_data']) == 1 , so |
90 | | - # this usually returns cve_item['cve']['description']['description_data'][0]['value'] |
91 | | - # In the remaining 1% cases this returns the longest summary. |
92 | | - summaries = [desc["value"] for desc in cve_item["cve"]["description"]["description_data"]] |
93 | | - return max(summaries, key=len) |
94 | | - |
95 | | - @staticmethod |
96 | | - def extract_severity_scores(cve_item): |
97 | | - severity_scores = [] |
98 | | - |
99 | | - if cve_item["impact"].get("baseMetricV3"): |
100 | | - severity_scores.append( |
101 | | - VulnerabilitySeverity( |
102 | | - system=scoring_systems["cvssv3"], |
103 | | - value=str(cve_item["impact"]["baseMetricV3"]["cvssV3"]["baseScore"]), |
| 96 | + reference_id=cpe, |
104 | 97 | ) |
105 | 98 | ) |
106 | | - severity_scores.append( |
107 | | - VulnerabilitySeverity( |
108 | | - system=scoring_systems["cvssv3_vector"], |
109 | | - value=str(cve_item["impact"]["baseMetricV3"]["cvssV3"]["vectorString"]), |
110 | | - ) |
111 | | - ) |
112 | | - |
113 | | - if cve_item["impact"].get("baseMetricV2"): |
114 | | - severity_scores.append( |
115 | | - VulnerabilitySeverity( |
116 | | - system=scoring_systems["cvssv2"], |
117 | | - value=str(cve_item["impact"]["baseMetricV2"]["cvssV2"]["baseScore"]), |
118 | | - ) |
| 99 | + references.append( |
| 100 | + Reference( |
| 101 | + url=f"https://nvd.nist.gov/vuln/detail/{cve_id}", |
| 102 | + reference_id=cve_id, |
| 103 | + severities=severity_scores, |
119 | 104 | ) |
120 | | - severity_scores.append( |
121 | | - VulnerabilitySeverity( |
122 | | - system=scoring_systems["cvssv2_vector"], |
123 | | - value=str(cve_item["impact"]["baseMetricV2"]["cvssV2"]["vectorString"]), |
124 | | - ) |
| 105 | + ) |
| 106 | + if "https://nvd.nist.gov/vuln/detail/{cve_id}" in ref_urls: |
| 107 | + ref_urls.remove(f"https://nvd.nist.gov/vuln/detail/{cve_id}") |
| 108 | + references.extend([Reference(url=url) for url in ref_urls]) |
| 109 | + if cve_id: |
| 110 | + aliases.append(cve_id) |
| 111 | + summary = extract_summary(cve_item) |
| 112 | + yield AdvisoryData( |
| 113 | + aliases=aliases, |
| 114 | + summary=summary, |
| 115 | + references=sorted(references), |
| 116 | + date_published=dateparser.parse(cve_item.get("publishedDate")), |
| 117 | + ) |
| 118 | + |
| 119 | + |
| 120 | +def extract_reference_urls(cve_item): |
| 121 | + """ |
| 122 | + Return a list of reference URLs for a given CVE item. |
| 123 | + """ |
| 124 | + urls = set() |
| 125 | + for reference in get_item(cve_item, "cve", "references", "reference_data") or []: |
| 126 | + ref_url = reference.get("url") |
| 127 | + |
| 128 | + if not ref_url: |
| 129 | + continue |
| 130 | + |
| 131 | + if ref_url.startswith( |
| 132 | + ( |
| 133 | + "http", |
| 134 | + "ftp", |
125 | 135 | ) |
126 | | - |
127 | | - return severity_scores |
128 | | - |
129 | | - def extract_reference_urls(self, cve_item): |
130 | | - urls = set() |
131 | | - for reference in cve_item["cve"]["references"]["reference_data"]: |
132 | | - ref_url = reference["url"] |
133 | | - |
134 | | - if not ref_url: |
135 | | - continue |
136 | | - |
137 | | - if ref_url.startswith("http") or ref_url.startswith("ftp"): |
138 | | - urls.add(ref_url) |
139 | | - |
140 | | - return urls |
141 | | - |
142 | | - def is_outdated(self, cve_item): |
143 | | - cve_last_modified_date = cve_item["lastModifiedDate"] |
144 | | - cve_last_modified_date_obj = dateparser.parse(cve_last_modified_date) |
145 | | - |
146 | | - if self.config.cutoff_date: |
147 | | - return cve_last_modified_date_obj < self.config.cutoff_date |
148 | | - |
149 | | - if self.config.last_run_date: |
150 | | - return cve_last_modified_date_obj < self.config.last_run_date |
151 | | - |
152 | | - return False |
153 | | - |
154 | | - def related_to_hardware(self, cve_item): |
155 | | - for cpe in self.extract_cpes(cve_item): |
156 | | - cpe_comps = cpe.split(":") |
157 | | - # CPE follow the format cpe:cpe_version:product_type:vendor:product |
158 | | - if cpe_comps[2] == "h": |
159 | | - return True |
160 | | - |
161 | | - return False |
162 | | - |
163 | | - @staticmethod |
164 | | - def extract_cpes(cve_item): |
165 | | - cpes = set() |
166 | | - for node in cve_item["configurations"]["nodes"]: |
167 | | - for cpe_data in node.get("cpe_match", []): |
168 | | - cpes.add(cpe_data["cpe23Uri"]) |
169 | | - return cpes |
| 136 | + ): |
| 137 | + urls.add(ref_url) |
| 138 | + |
| 139 | + return urls |
| 140 | + |
| 141 | + |
| 142 | +def related_to_hardware(cpes): |
| 143 | + """ |
| 144 | + Return True if the CVE item is related to hardware. |
| 145 | + """ |
| 146 | + for cpe in cpes: |
| 147 | + cpe_comps = cpe.split(":") |
| 148 | + # CPE follow the format cpe:cpe_version:product_type:vendor:product |
| 149 | + if len(cpe_comps) > 2 and cpe_comps[2] == "h": |
| 150 | + return True |
| 151 | + |
| 152 | + return False |
| 153 | + |
| 154 | + |
| 155 | +def extract_cpes(cve_item): |
| 156 | + """ |
| 157 | + Return a list of CPEs for a given CVE item. |
| 158 | + """ |
| 159 | + cpes = set() |
| 160 | + for node in get_item(cve_item, "configurations", "nodes") or []: |
| 161 | + for cpe_data in node.get("cpe_match") or []: |
| 162 | + cpe23_uri = cpe_data.get("cpe23Uri") |
| 163 | + if cpe23_uri: |
| 164 | + cpes.add(cpe23_uri) |
| 165 | + return cpes |
| 166 | + |
| 167 | + |
| 168 | +def extract_severity_scores(cve_item): |
| 169 | + """ |
| 170 | + Yield a vulnerability severity for each `cve_item`. |
| 171 | + """ |
| 172 | + if not isinstance(cve_item, dict): |
| 173 | + return None |
| 174 | + impact = cve_item.get("impact") or {} |
| 175 | + base_metric_v3 = impact.get("baseMetricV3") or {} |
| 176 | + if base_metric_v3: |
| 177 | + cvss_v3 = get_item(base_metric_v3, "cvssV3") |
| 178 | + yield VulnerabilitySeverity( |
| 179 | + system=SCORING_SYSTEMS["cvssv3"], |
| 180 | + value=str(cvss_v3.get("baseScore") or ""), |
| 181 | + ) |
| 182 | + yield VulnerabilitySeverity( |
| 183 | + system=SCORING_SYSTEMS["cvssv3_vector"], |
| 184 | + value=str(cvss_v3.get("vectorString") or ""), |
| 185 | + ) |
| 186 | + |
| 187 | + base_metric_v2 = impact.get("baseMetricV2") or {} |
| 188 | + if base_metric_v2: |
| 189 | + cvss_v2 = base_metric_v2.get("cvssV2") or {} |
| 190 | + yield VulnerabilitySeverity( |
| 191 | + system=SCORING_SYSTEMS["cvssv2"], |
| 192 | + value=str(cvss_v2.get("baseScore") or ""), |
| 193 | + ) |
| 194 | + yield VulnerabilitySeverity( |
| 195 | + system=SCORING_SYSTEMS["cvssv2_vector"], |
| 196 | + value=str(cvss_v2.get("vectorString") or ""), |
| 197 | + ) |
| 198 | + |
| 199 | + |
| 200 | +class NVDBasicImprover(Improver): |
| 201 | + @property |
| 202 | + def interesting_advisories(self) -> QuerySet: |
| 203 | + return Advisory.objects.filter(created_by=NVDImporter.qualified_name) |
| 204 | + |
| 205 | + def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]: |
| 206 | + yield Inference.from_advisory_data( |
| 207 | + advisory_data=advisory_data, confidence=100, fixed_purl=None |
| 208 | + ) |
0 commit comments