|
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 | | -import re |
25 | | -import xml.etree.ElementTree as ET |
26 | | -from typing import Set |
| 23 | +import logging |
| 24 | +from datetime import timezone |
| 25 | +from typing import Iterable |
| 26 | +from urllib.parse import urljoin |
27 | 27 |
|
| 28 | +import defusedxml.ElementTree as DET |
28 | 29 | import requests |
| 30 | +from dateutil import parser as dateparser |
29 | 31 | from packageurl import PackageURL |
| 32 | +from univers.version_range import OpensslVersionRange |
| 33 | +from univers.versions import OpensslVersion |
30 | 34 |
|
31 | | -from vulnerabilities.helpers import create_etag |
32 | | -from vulnerabilities.helpers import nearest_patched_package |
33 | | -from vulnerabilities.importer import Advisory |
| 35 | +from vulnerabilities.importer import AdvisoryData |
| 36 | +from vulnerabilities.importer import AffectedPackage |
34 | 37 | from vulnerabilities.importer import Importer |
35 | 38 | from vulnerabilities.importer import Reference |
| 39 | +from vulnerabilities.importer import VulnerabilitySeverity |
| 40 | +from vulnerabilities.severity_systems import SCORING_SYSTEMS |
36 | 41 |
|
| 42 | +logger = logging.getLogger(__name__) |
37 | 43 |
|
38 | | -class OpenSSLImporter(Importer): |
39 | 44 |
|
| 45 | +class OpensslImporter(Importer): |
| 46 | + spdx_license_expression = "Apache-2.0" |
| 47 | + license_url = "https://github.com/openssl/openssl/blob/master/LICENSE.txt" |
40 | 48 | url = "https://www.openssl.org/news/vulnerabilities.xml" |
41 | 49 |
|
42 | | - def updated_advisories(self) -> Set[Advisory]: |
43 | | - # Etags are like hashes of web responses. We maintain |
44 | | - # (url, etag) mappings in the DB. `create_etag` creates |
45 | | - # (url, etag) pair. If a (url, etag) already exists then the code |
46 | | - # skips processing the response further to avoid duplicate work |
47 | | - if create_etag(data_src=self, url=self.url, etag_key="ETag"): |
48 | | - raw_data = self.fetch() |
49 | | - advisories = self.to_advisories(raw_data) |
50 | | - return self.batch_advisories(advisories) |
51 | | - |
52 | | - return [] |
53 | | - |
54 | 50 | def fetch(self): |
55 | | - return requests.get(self.url).content |
56 | | - |
57 | | - @staticmethod |
58 | | - def to_advisories(xml_response: str) -> Set[Advisory]: |
59 | | - advisories = [] |
60 | | - pkg_name = "openssl" |
61 | | - pkg_type = "generic" |
62 | | - root = ET.fromstring(xml_response) |
63 | | - for element in root: |
64 | | - if element.tag == "issue": |
65 | | - cve_id = "" |
66 | | - summary = "" |
67 | | - safe_pkg_versions = [] |
68 | | - vuln_pkg_versions = [] |
69 | | - ref_urls = [] |
70 | | - for info in element: |
71 | | - |
72 | | - if info.tag == "cve": |
73 | | - if info.attrib.get("name"): |
74 | | - cve_id = "CVE-" + info.attrib.get("name") |
75 | | - |
76 | | - else: |
77 | | - continue |
78 | | - |
79 | | - if cve_id == "CVE-2007-5502": |
80 | | - # This CVE has weird version "fips-1.1.2".This is |
81 | | - # probably a submodule. Skip this for now. |
82 | | - continue |
83 | | - |
84 | | - if info.tag == "affects": |
85 | | - # Vulnerable package versions |
86 | | - vuln_pkg_versions.append(info.attrib.get("version")) |
87 | | - |
88 | | - if info.tag == "fixed": |
89 | | - # Fixed package versions |
90 | | - safe_pkg_versions.append(info.attrib.get("version")) |
91 | | - |
92 | | - if info: |
93 | | - commit_hash = info[0].attrib["hash"] |
94 | | - ref_urls.append( |
95 | | - Reference( |
96 | | - url="https://github.com/openssl/openssl/commit/" + commit_hash |
97 | | - ) |
98 | | - ) |
99 | | - if info.tag == "description": |
100 | | - # Description |
101 | | - summary = re.sub(r"\s+", " ", info.text).strip() |
102 | | - |
103 | | - safe_purls = [ |
104 | | - PackageURL(name=pkg_name, type=pkg_type, version=version) |
105 | | - for version in safe_pkg_versions |
106 | | - ] |
107 | | - vuln_purls = [ |
108 | | - PackageURL(name=pkg_name, type=pkg_type, version=version) |
109 | | - for version in vuln_pkg_versions |
110 | | - ] |
111 | | - |
112 | | - advisory = Advisory( |
113 | | - vulnerability_id=cve_id, |
114 | | - summary=summary, |
115 | | - affected_packages=nearest_patched_package(vuln_purls, safe_purls), |
116 | | - references=ref_urls, |
| 51 | + response = requests.get(url=self.url) |
| 52 | + if not response.status_code == 200: |
| 53 | + logger.error(f"Error while fetching {self.url}: {response.status_code}") |
| 54 | + return |
| 55 | + return response.content |
| 56 | + |
| 57 | + def advisory_data(self) -> Iterable[AdvisoryData]: |
| 58 | + xml_response = self.fetch() |
| 59 | + return parse_vulnerabilities(xml_response) |
| 60 | + |
| 61 | + |
| 62 | +def parse_vulnerabilities(xml_response) -> Iterable[AdvisoryData]: |
| 63 | + root = DET.fromstring(xml_response) |
| 64 | + for xml_issue in root: |
| 65 | + if xml_issue.tag == "issue": |
| 66 | + advisory = to_advisory_data(xml_issue) |
| 67 | + if advisory: |
| 68 | + yield advisory |
| 69 | + |
| 70 | + |
| 71 | +def to_advisory_data(xml_issue) -> AdvisoryData: |
| 72 | + """ |
| 73 | + Return AdvisoryData from given xml_issue |
| 74 | + """ |
| 75 | + |
| 76 | + purl = PackageURL(type="openssl", name="openssl") |
| 77 | + cve = advisory_url = severity = summary = None |
| 78 | + safe_pkg_versions = {} |
| 79 | + vuln_pkg_versions_by_base_version = {} |
| 80 | + aliases = [] |
| 81 | + references = [] |
| 82 | + affected_packages = [] |
| 83 | + date_published = xml_issue.attrib["public"].strip() |
| 84 | + |
| 85 | + for info in xml_issue: |
| 86 | + if info.tag == "impact": |
| 87 | + severity = VulnerabilitySeverity( |
| 88 | + system=SCORING_SYSTEMS["generic_textual"], value=info.attrib["severity"] |
| 89 | + ) |
| 90 | + |
| 91 | + elif info.tag == "advisory": |
| 92 | + advisory_url = info.attrib["url"] |
| 93 | + if not advisory_url.startswith("https://web.archive.org"): |
| 94 | + advisory_url = urljoin("https://www.openssl.org", advisory_url) |
| 95 | + |
| 96 | + elif info.tag == "cve": |
| 97 | + cve = info.attrib.get("name") |
| 98 | + # use made up alias to compensate for case when advisory doesn't have CVE-ID |
| 99 | + madeup_alias = f"VC-OPENSSL-{date_published}" |
| 100 | + if cve: |
| 101 | + cve = f"CVE-{cve}" |
| 102 | + madeup_alias = f"{madeup_alias}-{cve}" |
| 103 | + aliases.append(cve) |
| 104 | + references.append(Reference(reference_id=cve)) |
| 105 | + aliases.append(madeup_alias) |
| 106 | + |
| 107 | + elif info.tag == "affects": |
| 108 | + affected_base = info.attrib["base"] |
| 109 | + affected_version = info.attrib["version"] |
| 110 | + if affected_base.startswith("fips"): |
| 111 | + logger.error( |
| 112 | + f"{affected_base!r} is a OpenSSL-FIPS Object Module and isn't supported by OpensslImporter. Use a different importer." |
| 113 | + ) |
| 114 | + return |
| 115 | + if affected_base in vuln_pkg_versions_by_base_version: |
| 116 | + vuln_pkg_versions_by_base_version[affected_base].append(affected_version) |
| 117 | + else: |
| 118 | + vuln_pkg_versions_by_base_version[affected_base] = [affected_version] |
| 119 | + |
| 120 | + elif info.tag == "fixed": |
| 121 | + fixed_base = info.attrib["base"] |
| 122 | + fixed_version = info.attrib["version"] |
| 123 | + safe_pkg_versions[fixed_base] = fixed_version |
| 124 | + for commit in info: |
| 125 | + commit_hash = commit.attrib["hash"] |
| 126 | + references.append( |
| 127 | + Reference( |
| 128 | + url=urljoin("https://github.com/openssl/openssl/commit/", commit_hash) |
| 129 | + ) |
117 | 130 | ) |
118 | | - advisories.append(advisory) |
119 | 131 |
|
120 | | - return advisories |
| 132 | + elif info.tag == "description": |
| 133 | + summary = " ".join(info.text.split()) |
| 134 | + |
| 135 | + elif info.tag in ("reported", "problemtype", "title"): |
| 136 | + # as of now, these info isn't useful for AdvisoryData |
| 137 | + # for more see: https://github.com/nexB/vulnerablecode/issues/688 |
| 138 | + continue |
| 139 | + else: |
| 140 | + logger.error( |
| 141 | + f"{info.tag!r} is a newly introduced tag. Modify the importer to make use of this new info." |
| 142 | + ) |
| 143 | + |
| 144 | + for base_version, affected_versions in vuln_pkg_versions_by_base_version.items(): |
| 145 | + affected_version_range = OpensslVersionRange.from_versions(affected_versions) |
| 146 | + fixed_version = None |
| 147 | + if base_version in safe_pkg_versions: |
| 148 | + fixed_version = OpensslVersion(safe_pkg_versions[base_version]) |
| 149 | + affected_package = AffectedPackage( |
| 150 | + package=purl, |
| 151 | + affected_version_range=affected_version_range, |
| 152 | + fixed_version=fixed_version, |
| 153 | + ) |
| 154 | + affected_packages.append(affected_package) |
| 155 | + |
| 156 | + if severity and advisory_url: |
| 157 | + references.append(Reference(url=advisory_url, severities=[severity])) |
| 158 | + elif advisory_url: |
| 159 | + references.append(Reference(url=advisory_url)) |
| 160 | + |
| 161 | + parsed_date_published = dateparser.parse(date_published, yearfirst=True).replace( |
| 162 | + tzinfo=timezone.utc |
| 163 | + ) |
| 164 | + |
| 165 | + return AdvisoryData( |
| 166 | + aliases=aliases, |
| 167 | + summary=summary, |
| 168 | + affected_packages=affected_packages, |
| 169 | + references=references, |
| 170 | + date_published=parsed_date_published, |
| 171 | + ) |
0 commit comments