From 4f8c69aa9f24c297ffe87707aafdea51bf3bfbd0 Mon Sep 17 00:00:00 2001 From: ziadhany Date: Mon, 28 Aug 2023 23:54:54 +0300 Subject: [PATCH] Add RubyImporter to git_importer test_git_importer_clone Drop cvss_v2 Add ruby importer_name and Rebase Resolve merge conflicts Add advisory_url to ruby importer Add a notice and the spdx_license_expression Resolve merge conflict Add a docstring to get_affected_packages Add a unite test for get_affected_packages function Remove unused variables Fix sorted affected_package_merge Add ruby importer and improver Fix style test Fix test Rewrite affected_packages Ruby initial config Reference: #796 Clean imported data after import process Signed-off-by: Tushar Goel Fix sorted affected_package_merge Refactor Ruby importer and improver Add ruby importer and improver Fix style test Fix test Rewrite affected_packages Ruby initial config Reference: #796 Signed-off-by: ziadhany --- vulnerabilities/importers/__init__.py | 2 + vulnerabilities/importers/ruby.py | 255 ++++++++++-------- vulnerabilities/improvers/__init__.py | 1 + vulnerabilities/improvers/valid_versions.py | 6 + vulnerabilities/tests/conftest.py | 1 - .../ruby/CVE-2007-5770-expected.json | 42 +++ .../tests/test_data/ruby/CVE-2007-5770.yml | 17 ++ .../ruby/CVE-2010-1330-expected.json | 31 +++ .../tests/test_data/ruby/CVE-2010-1330.yml | 15 ++ .../ruby/CVE-2018-11627-expected.json | 36 +++ .../ruby/{sinatra => }/CVE-2018-11627.yml | 0 .../ruby/CVE-2018-7212-expected.json | 48 ++++ .../ruby/{sinatra => }/CVE-2018-7212.yml | 0 .../ruby/parse-advisory-ruby-expected.json | 60 +++++ .../ruby/ruby-improver-expected.json | 215 +++++++++++++++ .../test_data/ruby/sidekiq/OSVDB-125675.yml | 9 - .../test_data/ruby/sidekiq/OSVDB-125676.yml | 14 - .../test_data/ruby/sidekiq/OSVDB-125678.yml | 9 - vulnerabilities/tests/test_data_source.py | 2 + vulnerabilities/tests/test_ruby.py | 190 +++++-------- 20 files changed, 698 insertions(+), 255 deletions(-) create mode 100644 vulnerabilities/tests/test_data/ruby/CVE-2007-5770-expected.json create mode 100644 vulnerabilities/tests/test_data/ruby/CVE-2007-5770.yml create mode 100644 vulnerabilities/tests/test_data/ruby/CVE-2010-1330-expected.json create mode 100644 vulnerabilities/tests/test_data/ruby/CVE-2010-1330.yml create mode 100644 vulnerabilities/tests/test_data/ruby/CVE-2018-11627-expected.json rename vulnerabilities/tests/test_data/ruby/{sinatra => }/CVE-2018-11627.yml (100%) create mode 100644 vulnerabilities/tests/test_data/ruby/CVE-2018-7212-expected.json rename vulnerabilities/tests/test_data/ruby/{sinatra => }/CVE-2018-7212.yml (100%) create mode 100644 vulnerabilities/tests/test_data/ruby/parse-advisory-ruby-expected.json create mode 100644 vulnerabilities/tests/test_data/ruby/ruby-improver-expected.json delete mode 100644 vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125675.yml delete mode 100644 vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125676.yml delete mode 100644 vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125678.yml diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index add6967f8..70b70653e 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -32,6 +32,7 @@ from vulnerabilities.importers import pysec from vulnerabilities.importers import redhat from vulnerabilities.importers import retiredotnet +from vulnerabilities.importers import ruby from vulnerabilities.importers import suse_scores from vulnerabilities.importers import ubuntu from vulnerabilities.importers import ubuntu_usn @@ -67,6 +68,7 @@ fireeye.FireyeImporter, apache_kafka.ApacheKafkaImporter, oss_fuzz.OSSFuzzImporter, + ruby.RubyImporter, ] IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY} diff --git a/vulnerabilities/importers/ruby.py b/vulnerabilities/importers/ruby.py index 556e39140..6a3b5f3f1 100644 --- a/vulnerabilities/importers/ruby.py +++ b/vulnerabilities/importers/ruby.py @@ -7,132 +7,177 @@ # See https://aboutcode.org for more information about nexB OSS projects. # -import asyncio -from typing import List -from typing import Set +import logging +from pathlib import Path +from typing import Iterable from dateutil.parser import parse from packageurl import PackageURL from pytz import UTC -from univers.version_range import VersionRange -from univers.versions import SemverVersion +from univers.version_range import GemVersionRange from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importer import AffectedPackage from vulnerabilities.importer import Importer from vulnerabilities.importer import Reference -from vulnerabilities.package_managers import RubyVersionAPI +from vulnerabilities.importer import VulnerabilitySeverity +from vulnerabilities.severity_systems import SCORING_SYSTEMS +from vulnerabilities.utils import build_description +from vulnerabilities.utils import get_advisory_url from vulnerabilities.utils import load_yaml -from vulnerabilities.utils import nearest_patched_package +logger = logging.getLogger(__name__) -class RubyImporter(Importer): - def __enter__(self): - super(RubyImporter, self).__enter__() - - if not getattr(self, "_added_files", None): - self._added_files, self._updated_files = self.file_changes( - recursive=True, file_ext="yml", subdir="./gems" - ) - self.pkg_manager_api = RubyVersionAPI() - self.set_api(self.collect_packages()) - - def set_api(self, packages): - asyncio.run(self.pkg_manager_api.load_api(packages)) - - def updated_advisories(self) -> Set[AdvisoryData]: - files = self._updated_files.union(self._added_files) - advisories = [] - for f in files: - processed_data = self.process_file(f) - if processed_data: - advisories.append(processed_data) - return self.batch_advisories(advisories) - - def collect_packages(self): - packages = set() - files = self._updated_files.union(self._added_files) - for f in files: - data = load_yaml(f) - if data.get("gem"): - packages.add(data["gem"]) - - return packages - - def process_file(self, path) -> List[AdvisoryData]: - record = load_yaml(path) +class RubyImporter(Importer): + license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt" + repo_url = "git+https://github.com/rubysec/ruby-advisory-db" + importer_name = "Ruby Importer" + spdx_license_expression = "LicenseRef-scancode-public-domain-disclaimer" + notice = """ + If you submit code or data to the ruby-advisory-db that is copyrighted by + yourself, upon submission you hereby agree to release it into the public + domain. + + The data imported from the ruby-advisory-db have been filtered to exclude + any non-public domain data from the data copyrighted by the Open + Source Vulnerability Database (http://osvdb.org). + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + + def advisory_data(self) -> Iterable[AdvisoryData]: + try: + self.clone(self.repo_url) + base_path = Path(self.vcs_response.dest_dir) + supported_subdir = ["rubies", "gems"] + for subdir in supported_subdir: + for file_path in base_path.glob(f"{subdir}/**/*.yml"): + if file_path.name.startswith("OSVDB-"): + continue + raw_data = load_yaml(file_path) + advisory_url = get_advisory_url( + file=file_path, + base_path=base_path, + url="https://github.com/rubysec/ruby-advisory-db/blob/master/", + ) + yield parse_ruby_advisory(raw_data, subdir, advisory_url) + finally: + if self.vcs_response: + self.vcs_response.delete() + + +def parse_ruby_advisory(record, schema_type, advisory_url): + """ + Parse a ruby advisory file and return an AdvisoryData or None. + Each advisory file contains the advisory information in YAML format. + Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas + """ + if schema_type == "gems": package_name = record.get("gem") - if not package_name: - return - if "cve" in record: - cve_id = "CVE-{}".format(record["cve"]) + if not package_name: + logger.error("Invalid package name") else: - return - - publish_time = parse(record["date"]).replace(tzinfo=UTC) - safe_version_ranges = record.get("patched_versions", []) - # this case happens when the advisory contain only 'patched_versions' field - # and it has value None(i.e it is empty :( ). - if not safe_version_ranges: - safe_version_ranges = [] - safe_version_ranges += record.get("unaffected_versions", []) - safe_version_ranges = [i for i in safe_version_ranges if i] - - if not getattr(self, "pkg_manager_api", None): - self.pkg_manager_api = RubyVersionAPI() - all_vers = self.pkg_manager_api.get(package_name, until=publish_time).valid_versions - safe_versions, affected_versions = self.categorize_versions(all_vers, safe_version_ranges) - - impacted_purls = [ - PackageURL( - name=package_name, - type="gem", - version=version, + purl = PackageURL(type="gem", name=package_name) + + return AdvisoryData( + aliases=get_aliases(record), + summary=get_summary(record), + affected_packages=get_affected_packages(record, purl), + references=get_references(record), + date_published=get_publish_time(record), + url=advisory_url, ) - for version in affected_versions - ] - - resolved_purls = [ - PackageURL( - name=package_name, - type="gem", - version=version, + + elif schema_type == "rubies": + engine = record.get("engine") # engine enum: [jruby, rbx, ruby] + if not engine: + logger.error("Invalid engine name") + else: + purl = PackageURL(type="ruby", name=engine) + return AdvisoryData( + aliases=get_aliases(record), + summary=get_summary(record), + affected_packages=get_affected_packages(record, purl), + references=get_references(record), + date_published=get_publish_time(record), + url=advisory_url, ) - for version in safe_versions - ] - references = [] - if record.get("url"): - references.append(Reference(url=record.get("url"))) - return AdvisoryData( - summary=record.get("description", ""), - affected_packages=nearest_patched_package(impacted_purls, resolved_purls), - references=references, - vulnerability_id=cve_id, +def get_affected_packages(record, purl): + """ + Return AffectedPackage objects one for each affected_version_range and invert the safe_version_ranges + ( patched_versions , unaffected_versions ) then passing the purl and the inverted safe_version_range + to the AffectedPackage object + """ + safe_version_ranges = record.get("patched_versions", []) + # this case happens when the advisory contain only 'patched_versions' field + # and it has value None(i.e it is empty :( ). + if not safe_version_ranges: + safe_version_ranges = [] + safe_version_ranges += record.get("unaffected_versions", []) + safe_version_ranges = [i for i in safe_version_ranges if i] + + affected_packages = [] + affected_version_ranges = [ + GemVersionRange.from_native(elem).invert() for elem in safe_version_ranges + ] + + for affected_version_range in affected_version_ranges: + affected_packages.append( + AffectedPackage( + package=purl, + affected_version_range=affected_version_range, + ) ) + return affected_packages + - @staticmethod - def categorize_versions(all_versions, unaffected_version_ranges): +def get_aliases(record) -> [str]: + aliases = [] + if record.get("cve"): + aliases.append("CVE-{}".format(record.get("cve"))) + if record.get("osvdb"): + aliases.append("OSV-{}".format(record.get("osvdb"))) + if record.get("ghsa"): + aliases.append("GHSA-{}".format(record.get("ghsa"))) + return aliases - for id, elem in enumerate(unaffected_version_ranges): - unaffected_version_ranges[id] = VersionRange.from_scheme_version_spec_string( - "semver", elem + +def get_references(record) -> [Reference]: + references = [] + cvss_v3 = record.get("cvss_v3") + if record.get("url"): + if not cvss_v3: + references.append(Reference(url=record.get("url"))) + else: + references.append( + Reference( + url=record.get("url"), + severities=[ + VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv3"], value=cvss_v3) + ], + ) ) + return references + + +def get_publish_time(record): + date = record.get("date") + if not date: + return + return parse(date).replace(tzinfo=UTC) + - safe_versions = [] - vulnerable_versions = [] - for i in all_versions: - vobj = SemverVersion(i) - is_vulnerable = False - for ver_rng in unaffected_version_ranges: - if vobj in ver_rng: - safe_versions.append(i) - is_vulnerable = True - break - - if not is_vulnerable: - vulnerable_versions.append(i) - - return safe_versions, vulnerable_versions +def get_summary(record): + title = record.get("title") or "" + description = record.get("description") or "" + return build_description(summary=title, description=description) diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index 9880bf9ee..d593b1e77 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -24,6 +24,7 @@ valid_versions.DebianOvalImprover, valid_versions.UbuntuOvalImprover, valid_versions.OSSFuzzImprover, + valid_versions.RubyImprover, vulnerability_status.VulnerabilityStatusImprover, ] diff --git a/vulnerabilities/improvers/valid_versions.py b/vulnerabilities/improvers/valid_versions.py index cada4bbb6..7db7a73e1 100644 --- a/vulnerabilities/improvers/valid_versions.py +++ b/vulnerabilities/improvers/valid_versions.py @@ -37,6 +37,7 @@ from vulnerabilities.importers.nginx import NginxImporter from vulnerabilities.importers.npm import NpmImporter from vulnerabilities.importers.oss_fuzz import OSSFuzzImporter +from vulnerabilities.importers.ruby import RubyImporter from vulnerabilities.importers.ubuntu import UbuntuImporter from vulnerabilities.improver import MAX_CONFIDENCE from vulnerabilities.improver import Improver @@ -460,3 +461,8 @@ class UbuntuOvalImprover(ValidVersionImprover): class OSSFuzzImprover(ValidVersionImprover): importer = OSSFuzzImporter ignorable_versions = [] + + +class RubyImprover(ValidVersionImprover): + importer = RubyImporter + ignorable_versions = [] diff --git a/vulnerabilities/tests/conftest.py b/vulnerabilities/tests/conftest.py index 8076f4b9b..f9216c742 100644 --- a/vulnerabilities/tests/conftest.py +++ b/vulnerabilities/tests/conftest.py @@ -26,7 +26,6 @@ def no_rmtree(monkeypatch): # Step 3: Migrate all the tests collect_ignore = [ "test_models.py", - "test_ruby.py", "test_rust.py", "test_suse_backports.py", "test_suse.py", diff --git a/vulnerabilities/tests/test_data/ruby/CVE-2007-5770-expected.json b/vulnerabilities/tests/test_data/ruby/CVE-2007-5770-expected.json new file mode 100644 index 000000000..aa3b4088c --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2007-5770-expected.json @@ -0,0 +1,42 @@ +{ + "aliases": [ + "CVE-2007-5770" + ], + "summary": "Ruby Net::HTTPS library does not validate server certificate CN\nThe (1) Net::ftptls, (2) Net::telnets, (3) Net::imap, (4) Net::pop, and (5)\nNet::smtp libraries in Ruby 1.8.5 and 1.8.6 do not verify that the\ncommonName (CN) field in a server certificate matches the domain name in a\nrequest sent over SSL, which makes it easier for remote attackers to\nintercept SSL transmissions via a man-in-the-middle attack or spoofed web\nsite, different components than CVE-2007-5162.", + "affected_packages": [ + { + "package": { + "type": "ruby", + "namespace": null, + "name": "ruby", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/<1.8.6.230|>=1.8.7", + "fixed_version": null + }, + { + "package": { + "type": "ruby", + "namespace": null, + "name": "ruby", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/<1.8.7", + "fixed_version": null + } + ], + "references": [ + { + "reference_id": "", + "url": "http://www.cvedetails.com/cve/CVE-2007-5770/", + "severities": [] + } + ], + "date_published": "2007-10-08T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db" +} \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby/CVE-2007-5770.yml b/vulnerabilities/tests/test_data/ruby/CVE-2007-5770.yml new file mode 100644 index 000000000..fcb1c372b --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2007-5770.yml @@ -0,0 +1,17 @@ +--- +engine: ruby +cve: 2007-5770 +url: http://www.cvedetails.com/cve/CVE-2007-5770/ +title: Ruby Net::HTTPS library does not validate server certificate CN +date: 2007-10-08 +description: | + The (1) Net::ftptls, (2) Net::telnets, (3) Net::imap, (4) Net::pop, and (5) + Net::smtp libraries in Ruby 1.8.5 and 1.8.6 do not verify that the + commonName (CN) field in a server certificate matches the domain name in a + request sent over SSL, which makes it easier for remote attackers to + intercept SSL transmissions via a man-in-the-middle attack or spoofed web + site, different components than CVE-2007-5162. +cvss_v2: 4.3 +patched_versions: +- ~> 1.8.6.230 +- '>= 1.8.7' diff --git a/vulnerabilities/tests/test_data/ruby/CVE-2010-1330-expected.json b/vulnerabilities/tests/test_data/ruby/CVE-2010-1330-expected.json new file mode 100644 index 000000000..bd09931cc --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2010-1330-expected.json @@ -0,0 +1,31 @@ +{ + "aliases": [ + "CVE-2010-1330", + "OSV-77297" + ], + "summary": "CVE-2010-1330 jruby: XSS in the regular expression engine when processing invalid UTF-8 byte sequences\nThe regular expression engine in JRuby before 1.4.1, when $KCODE is set to 'u', does not properly handle characters immediately after a UTF-8 character, which allows remote attackers to conduct cross-site scripting (XSS) attacks via a crafted string.", + "affected_packages": [ + { + "package": { + "type": "ruby", + "namespace": null, + "name": "jruby", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/<1.4.1", + "fixed_version": null + } + ], + "references": [ + { + "reference_id": "", + "url": "http://jruby.org/2010/04/26/jruby-1-4-1-xss-vulnerability", + "severities": [] + } + ], + "date_published": "2010-04-26T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db" +} \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby/CVE-2010-1330.yml b/vulnerabilities/tests/test_data/ruby/CVE-2010-1330.yml new file mode 100644 index 000000000..78e0dfdd9 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2010-1330.yml @@ -0,0 +1,15 @@ +--- +engine: jruby +cve: 2010-1330 +osvdb: 77297 +url: http://jruby.org/2010/04/26/jruby-1-4-1-xss-vulnerability +title: 'CVE-2010-1330 jruby: XSS in the regular expression engine when processing + invalid UTF-8 byte sequences' +date: 2010-04-26 +description: The regular expression engine in JRuby before 1.4.1, when $KCODE is set + to 'u', does not properly handle characters immediately after a UTF-8 character, + which allows remote attackers to conduct cross-site scripting (XSS) attacks via + a crafted string. +cvss_v2: 4.3 +patched_versions: +- '>= 1.4.1' diff --git a/vulnerabilities/tests/test_data/ruby/CVE-2018-11627-expected.json b/vulnerabilities/tests/test_data/ruby/CVE-2018-11627-expected.json new file mode 100644 index 000000000..3cc824fa4 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2018-11627-expected.json @@ -0,0 +1,36 @@ +{ + "aliases": [ + "CVE-2018-11627" + ], + "summary": "Sinatra before 2.0.2 has XSS via the 400 Bad Request page that occurs upon a params parser exception.", + "affected_packages": [ + { + "package": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/<2.0.2", + "fixed_version": null + } + ], + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/issues/1428", + "severities": [ + { + "system": "cvssv3", + "value": "6.1", + "scoring_elements": "" + } + ] + } + ], + "date_published": "2018-05-31T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db" +} \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby/sinatra/CVE-2018-11627.yml b/vulnerabilities/tests/test_data/ruby/CVE-2018-11627.yml similarity index 100% rename from vulnerabilities/tests/test_data/ruby/sinatra/CVE-2018-11627.yml rename to vulnerabilities/tests/test_data/ruby/CVE-2018-11627.yml diff --git a/vulnerabilities/tests/test_data/ruby/CVE-2018-7212-expected.json b/vulnerabilities/tests/test_data/ruby/CVE-2018-7212-expected.json new file mode 100644 index 000000000..a8d31bd5f --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2018-7212-expected.json @@ -0,0 +1,48 @@ +{ + "aliases": [ + "CVE-2018-7212" + ], + "summary": "sinatra ruby gem path traversal via backslash characters on Windows\nAn issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.", + "affected_packages": [ + { + "package": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/<2.0.1", + "fixed_version": null + }, + { + "package": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/>1.0.0", + "fixed_version": null + } + ], + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "" + } + ] + } + ], + "date_published": "2018-01-09T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db" +} \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby/sinatra/CVE-2018-7212.yml b/vulnerabilities/tests/test_data/ruby/CVE-2018-7212.yml similarity index 100% rename from vulnerabilities/tests/test_data/ruby/sinatra/CVE-2018-7212.yml rename to vulnerabilities/tests/test_data/ruby/CVE-2018-7212.yml diff --git a/vulnerabilities/tests/test_data/ruby/parse-advisory-ruby-expected.json b/vulnerabilities/tests/test_data/ruby/parse-advisory-ruby-expected.json new file mode 100644 index 000000000..75f0dd4ad --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/parse-advisory-ruby-expected.json @@ -0,0 +1,60 @@ +[ + { + "aliases": [ + "CVE-2018-7212" + ], + "summary": "sinatra ruby gem path traversal via backslash characters on Windows\nAn issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.", + "affected_packages": [ + { + "package": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/<2.0.1", + "fixed_version": null + }, + { + "package": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": null, + "qualifiers": null, + "subpath": null + }, + "affected_version_range": "vers:gem/>1.0.0", + "fixed_version": null + } + ], + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv2", + "value": "5.0", + "scoring_elements": "" + } + ] + }, + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "" + } + ] + } + ], + "date_published": "2018-01-09T00:00:00+00:00", + "weaknesses": [] +} +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby/ruby-improver-expected.json b/vulnerabilities/tests/test_data/ruby/ruby-improver-expected.json new file mode 100644 index 000000000..a17ae5f00 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/ruby-improver-expected.json @@ -0,0 +1,215 @@ +[ + { + "vulnerability_id": null, + "aliases": [ + "CVE-2018-7212" + ], + "confidence": 100, + "summary": "sinatra ruby gem path traversal via backslash characters on Windows\nAn issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.", + "affected_purls": [ + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "0.2.6", + "qualifiers": null, + "subpath": null + }, + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "1.2.7", + "qualifiers": null, + "subpath": null + }, + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "1.3.6", + "qualifiers": null, + "subpath": null + } + ], + "fixed_purl": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "2.2.1", + "qualifiers": null, + "subpath": null + }, + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv2", + "value": "5.0", + "scoring_elements": "" + } + ] + }, + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "" + } + ] + } + ], + "weaknesses": [] + }, + { + "vulnerability_id": null, + "aliases": [ + "CVE-2018-7212" + ], + "confidence": 100, + "summary": "sinatra ruby gem path traversal via backslash characters on Windows\nAn issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.", + "affected_purls": [ + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "1.2.7", + "qualifiers": null, + "subpath": null + }, + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "1.3.6", + "qualifiers": null, + "subpath": null + }, + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "2.2.1", + "qualifiers": null, + "subpath": null + }, + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "3.0.2", + "qualifiers": null, + "subpath": null + }, + { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "3.0.5", + "qualifiers": null, + "subpath": null + } + ], + "fixed_purl": null, + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv2", + "value": "5.0", + "scoring_elements": "" + } + ] + }, + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "" + } + ] + } + ], + "weaknesses": [] + }, + { + "vulnerability_id": null, + "aliases": [ + "CVE-2018-7212" + ], + "confidence": 100, + "summary": "sinatra ruby gem path traversal via backslash characters on Windows\nAn issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.", + "affected_purls": [], + "fixed_purl": null, + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv2", + "value": "5.0", + "scoring_elements": "" + } + ] + }, + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "" + } + ] + } + ], + "weaknesses": [] + }, + { + "vulnerability_id": null, + "aliases": [ + "CVE-2018-7212" + ], + "confidence": 100, + "summary": "sinatra ruby gem path traversal via backslash characters on Windows\nAn issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.", + "affected_purls": [], + "fixed_purl": null, + "references": [ + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv2", + "value": "5.0", + "scoring_elements": "" + } + ] + }, + { + "reference_id": "", + "url": "https://github.com/sinatra/sinatra/pull/1379", + "severities": [ + { + "system": "cvssv3", + "value": "5.3", + "scoring_elements": "" + } + ] + } + ], + "weaknesses": [] + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125675.yml b/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125675.yml deleted file mode 100644 index 12e317da0..000000000 --- a/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125675.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -gem: sidekiq -osvdb: 125675 -url: https://github.com/mperham/sidekiq/pull/2422 -title: Sidekiq Gem for Ruby Multiple Unspecified CSRF -date: 2015-07-06 -description: Sidekiq::Web lacks CSRF protection -patched_versions: - - ">= 3.4.2" diff --git a/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125676.yml b/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125676.yml deleted file mode 100644 index 18ba94428..000000000 --- a/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125676.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -gem: sidekiq -osvdb: 125676 -url: https://github.com/mperham/sidekiq/issues/2330 -title: | - Sidekiq Gem for Ruby web/views/queue.erb CurrentMessagesInQueue Element - Reflected XSS -date: 2015-06-04 -description: XSS via queue name in Sidekiq::Web -patched_versions: - - ">= 3.4.0" -related: - osvdb: - - 125677 diff --git a/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125678.yml b/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125678.yml deleted file mode 100644 index 1566d10a7..000000000 --- a/vulnerabilities/tests/test_data/ruby/sidekiq/OSVDB-125678.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -gem: sidekiq -osvdb: 125678 -url: https://github.com/mperham/sidekiq/pull/2309 -title: Sidekiq Gem for Ruby web/views/queue.erb msg.display_class Element XSS -date: 2015-04-21 -description: XSS via job arguments display class in Sidekiq::Web -patched_versions: - - ">= 3.4.0" diff --git a/vulnerabilities/tests/test_data_source.py b/vulnerabilities/tests/test_data_source.py index 369ac9c23..9c64b5605 100644 --- a/vulnerabilities/tests/test_data_source.py +++ b/vulnerabilities/tests/test_data_source.py @@ -27,6 +27,7 @@ from vulnerabilities.importers.npm import NpmImporter from vulnerabilities.importers.pypa import PyPaImporter from vulnerabilities.importers.retiredotnet import RetireDotnetImporter +from vulnerabilities.importers.ruby import RubyImporter from vulnerabilities.oval_parser import OvalParser BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -123,6 +124,7 @@ def test_git_importer(mock_clone): NpmImporter, RetireDotnetImporter, PyPaImporter, + RubyImporter, ], ) def test_git_importer_clone(git_importer): diff --git a/vulnerabilities/tests/test_ruby.py b/vulnerabilities/tests/test_ruby.py index e57026fe3..0e06afe1d 100644 --- a/vulnerabilities/tests/test_ruby.py +++ b/vulnerabilities/tests/test_ruby.py @@ -6,135 +6,91 @@ # See https://github.com/nexB/vulnerablecode for support or download. # See https://aboutcode.org for more information about nexB OSS projects. # - - +import json import os -import pathlib -from unittest import TestCase from unittest.mock import patch +import pytest from packageurl import PackageURL +from univers.version_range import GemVersionRange from vulnerabilities.importer import AdvisoryData -from vulnerabilities.importer import Reference -from vulnerabilities.importers.ruby import RubyImporter -from vulnerabilities.package_managers import RubyVersionAPI -from vulnerabilities.package_managers import VersionResponse -from vulnerabilities.utils import AffectedPackage +from vulnerabilities.importer import AffectedPackage +from vulnerabilities.importers.ruby import get_affected_packages +from vulnerabilities.importers.ruby import parse_ruby_advisory +from vulnerabilities.improvers.default import DefaultImprover +from vulnerabilities.improvers.valid_versions import RubyImprover +from vulnerabilities.tests import util_tests +from vulnerabilities.tests.util_tests import check_results_against_json +from vulnerabilities.utils import load_yaml BASE_DIR = os.path.dirname(os.path.abspath(__file__)) TEST_DATA = os.path.join(BASE_DIR, "test_data", "ruby") -MOCK_ADDED_FILES = [] - -for filepath in pathlib.Path(TEST_DATA).glob("**/*.yml"): - MOCK_ADDED_FILES.append(filepath.absolute()) +@pytest.mark.parametrize( + "filename,expected_filename,schema_type", + [ + ("CVE-2018-7212.yml", "CVE-2018-7212-expected.json", "gems"), + ("CVE-2018-11627.yml", "CVE-2018-11627-expected.json", "gems"), + ("CVE-2007-5770.yml", "CVE-2007-5770-expected.json", "rubies"), + ("CVE-2010-1330.yml", "CVE-2010-1330-expected.json", "rubies"), + ], +) +def test_advisories(filename, expected_filename, schema_type): + file_path = os.path.join(TEST_DATA, filename) + mock_response = load_yaml(file_path) + results = parse_ruby_advisory( + mock_response, schema_type, "https://github.com/rubysec/ruby-advisory-db" + ).to_dict() + expected_file = os.path.join(TEST_DATA, expected_filename) + check_results_against_json(results=results, expected_file=expected_file) -class RubyImporterTest(TestCase): - @classmethod - def setUpClass(cls): - data_source_cfg = { - "repository_url": "https://github.com/rubysec/ruby-advisory-db.git", - } - cls.data_src = RubyImporter(1, config=data_source_cfg) - cls.data_src.pkg_manager_api = RubyVersionAPI() - - @patch( - "vulnerabilities.package_managers.RubyVersionAPI.get", - return_value=VersionResponse( - valid_versions={"1.0.0", "1.8.0", "2.0.3"}, newer_versions=set() - ), - ) - def test_process_file(self, mock_write): - expected_advisories = [ - Advisory( - summary="An issue was discovered in rack-protection/lib/rack/protection/path_traversal.rb\nin Sinatra 2.x before 2.0.1 on Windows. Path traversal is possible via backslash\ncharacters.\n", - vulnerability_id="CVE-2018-7212", - affected_packages=[ - AffectedPackage( - vulnerable_package=PackageURL( - type="gem", - namespace=None, - name="sinatra", - version="1.8.0", - ), - patched_package=PackageURL( - type="gem", - namespace=None, - name="sinatra", - version="2.0.3", - ), - ) - ], - references=[ - Reference( - reference_id="", - url="https://github.com/sinatra/sinatra/pull/1379", - severities=[], - ) - ], - ), - Advisory( - summary="Sinatra before 2.0.2 has XSS via the 400 Bad Request page that occurs upon a params parser exception.\n", - vulnerability_id="CVE-2018-11627", - affected_packages=[ - AffectedPackage( - vulnerable_package=PackageURL( - type="gem", - namespace=None, - name="sinatra", - version="1.0.0", - ), - patched_package=PackageURL( - type="gem", - namespace=None, - name="sinatra", - version="2.0.3", - ), - ), - AffectedPackage( - vulnerable_package=PackageURL( - type="gem", - namespace=None, - name="sinatra", - version="1.8.0", - ), - patched_package=PackageURL( - type="gem", - namespace=None, - name="sinatra", - version="2.0.3", - ), - ), - ], - references=[ - Reference( - reference_id="", - url="https://github.com/sinatra/sinatra/issues/1428", - severities=[], - ) - ], - ), - ] - found_advisories = [] - for p in MOCK_ADDED_FILES: - advisory = self.data_src.process_file(p) - if advisory: - found_advisories.append(advisory) - found_advisories = list(map(Advisory.normalized, found_advisories)) - expected_advisories = list(map(Advisory.normalized, expected_advisories)) - assert sorted(found_advisories) == sorted(expected_advisories) +@patch("vulnerabilities.improvers.valid_versions.RubyImprover.get_package_versions") +def test_ruby_improver(mock_response): + advisory_file = os.path.join(TEST_DATA, f"parse-advisory-ruby-expected.json") + with open(advisory_file) as exp: + advisories = [AdvisoryData.from_dict(adv) for adv in (json.load(exp))] + mock_response.return_value = ["0.2.6", "1.2.7", "1.3.6", "2.2.1", "3.0.2", "3.0.5"] + improvers = [RubyImprover(), DefaultImprover()] + result = [] + for improver in improvers: + for advisory in advisories: + inference = [data.to_dict() for data in improver.get_inferences(advisory)] + result.extend(inference) + expected_file = os.path.join(TEST_DATA, f"ruby-improver-expected.json") + util_tests.check_results_against_json(result, expected_file) - def test_categorize_versions(self): - all_versions = ["1.0.0", "1.2.0", "9.0.2", "0.2.3"] - safe_ver_ranges = ["==1.0.0", ">1.2.0"] - - exp_safe_vers = ["1.0.0", "9.0.2"] - exp_aff_vers = ["1.2.0", "0.2.3"] - - safe_vers, aff_vers = self.data_src.categorize_versions(all_versions, safe_ver_ranges) - assert exp_aff_vers == aff_vers - assert exp_safe_vers == safe_vers +@pytest.mark.parametrize( + "record,purl,result", + [ + ( + {"patched_versions": [">= 1.6.5.1"]}, + PackageURL(type="gem", name="jruby"), + [ + AffectedPackage( + package=PackageURL(type="gem", name="jruby"), + affected_version_range=GemVersionRange.from_string("vers:gem/<1.6.5.1"), + ) + ], + ), + ( + {"patched_versions": [">= 1.1.3"], "unaffected_versions": ["< 0.1.33"]}, + PackageURL(type="gem", name="'devise_token_auth'"), + [ + AffectedPackage( + package=PackageURL(type="gem", name="'devise_token_auth'"), + affected_version_range=GemVersionRange.from_string("vers:gem/<1.1.3"), + ), + AffectedPackage( + package=PackageURL(type="gem", name="'devise_token_auth'"), + affected_version_range=GemVersionRange.from_string("vers:gem/>=0.1.33"), + ), + ], + ), + ], +) +def test_get_affected_packages(record, purl, result): + assert get_affected_packages(record, purl) == result