diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index 4a2fd6ac7..b3521796a 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -31,6 +31,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 @@ -65,6 +66,7 @@ ubuntu_usn.UbuntuUSNImporter, fireeye.FireyeImporter, apache_kafka.ApacheKafkaImporter, + 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..cd9aa3683 100644 --- a/vulnerabilities/importers/ruby.py +++ b/vulnerabilities/importers/ruby.py @@ -7,132 +7,147 @@ # 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 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" + spdx_license_expression = "unknown" + repo_url = "git+https://github.com/rubysec/ruby-advisory-db" + + 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) + yield parse_ruby_advisory(raw_data, subdir) + finally: + if self.vcs_response: + self.vcs_response.delete() + + +def parse_ruby_advisory(record, schema_type): + """ + 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"]) - 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, - ) - for version in affected_versions - ] - - resolved_purls = [ - PackageURL( - name=package_name, - type="gem", - version=version, - ) - for version in safe_versions - ] + purl = PackageURL(type="gem", name=package_name) - references = [] - if record.get("url"): - references.append(Reference(url=record.get("url"))) + 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), + ) + elif schema_type == "rubies": + engine = record.get("engine") # engine enum: [jruby, rbx, ruby] + purl = PackageURL(type="ruby", name=engine) return AdvisoryData( - summary=record.get("description", ""), - affected_packages=nearest_patched_package(impacted_purls, resolved_purls), - references=references, - vulnerability_id=cve_id, + 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), ) - @staticmethod - def categorize_versions(all_versions, unaffected_version_ranges): - for id, elem in enumerate(unaffected_version_ranges): - unaffected_version_ranges[id] = VersionRange.from_scheme_version_spec_string( - "semver", elem +def get_affected_packages(record, purl): + 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 + + +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 + + +def get_references(record) -> [Reference]: + references = [] + cvss_v2 = record.get("cvss_v2") + cvss_v3 = record.get("cvss_v3") + + if record.get("url"): + if not (cvss_v2 or cvss_v3): + references.append(Reference(url=record.get("url"))) + if cvss_v2: + references.append( + Reference( + url=record.get("url"), + severities=[ + VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv2"], value=cvss_v2) + ], + ) + ) + if cvss_v3: + references.append( + Reference( + url=record.get("url"), + severities=[ + VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv3"], value=cvss_v3) + ], + ) + ) + return references + + +def get_publish_time(record): + return parse(record["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") + description = record.get("description", "") + return build_description(summary=title, description=description) diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index dd17f2f4e..54aa3d7c5 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -51,6 +51,7 @@ valid_versions.IstioImprover, valid_versions.DebianOvalImprover, valid_versions.UbuntuOvalImprover, + valid_versions.RubyImprover, ] IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY} diff --git a/vulnerabilities/improvers/valid_versions.py b/vulnerabilities/improvers/valid_versions.py index 26ee5033b..1445a4c70 100644 --- a/vulnerabilities/improvers/valid_versions.py +++ b/vulnerabilities/improvers/valid_versions.py @@ -19,6 +19,7 @@ from django.db.models.query import QuerySet from packageurl import PackageURL from univers.versions import NginxVersion +from univers.versions import RubygemsVersion from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackage @@ -35,6 +36,7 @@ from vulnerabilities.importers.istio import IstioImporter from vulnerabilities.importers.nginx import NginxImporter from vulnerabilities.importers.npm import NpmImporter +from vulnerabilities.importers.ruby import RubyImporter from vulnerabilities.importers.ubuntu import UbuntuImporter from vulnerabilities.improver import MAX_CONFIDENCE from vulnerabilities.improver import Improver @@ -43,6 +45,7 @@ from vulnerabilities.package_managers import GitHubTagsAPI from vulnerabilities.package_managers import GoproxyVersionAPI from vulnerabilities.package_managers import PackageVersion +from vulnerabilities.package_managers import RubyVersionAPI from vulnerabilities.package_managers import VersionAPI from vulnerabilities.package_managers import get_api_package_name from vulnerabilities.package_managers import get_version_fetcher @@ -477,3 +480,8 @@ class DebianOvalImprover(ValidVersionImprover): class UbuntuOvalImprover(ValidVersionImprover): importer = UbuntuImporter ignorable_versions = [] + + +class RubyImprover(ValidVersionImprover): + importer = RubyImporter + ignorable_versions = [] diff --git a/vulnerabilities/tests/conftest.py b/vulnerabilities/tests/conftest.py index de7bb560a..dcd179c6e 100644 --- a/vulnerabilities/tests/conftest.py +++ b/vulnerabilities/tests/conftest.py @@ -27,7 +27,6 @@ def no_rmtree(monkeypatch): collect_ignore = [ "test_models.py", "test_package_managers.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..0f756942e --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2007-5770-expected.json @@ -0,0 +1,47 @@ +{ + "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": [ + { + "system": "cvssv2", + "value": "4.3", + "scoring_elements": "" + } + ] + } + ], + "date_published": "2007-10-08T00:00:00+00:00", + "weaknesses": [] +} \ 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..63c669b4f --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2010-1330-expected.json @@ -0,0 +1,36 @@ +{ + "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": [ + { + "system": "cvssv2", + "value": "4.3", + "scoring_elements": "" + } + ] + } + ], + "date_published": "2010-04-26T00:00:00+00:00", + "weaknesses": [] +} \ 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..7211fae3c --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2018-11627-expected.json @@ -0,0 +1,35 @@ +{ + "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": [] +} \ 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..1bb38bcd7 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/CVE-2018-7212-expected.json @@ -0,0 +1,58 @@ +{ + "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/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_ruby.py b/vulnerabilities/tests/test_ruby.py index e57026fe3..68ada8b83 100644 --- a/vulnerabilities/tests/test_ruby.py +++ b/vulnerabilities/tests/test_ruby.py @@ -6,135 +6,52 @@ # 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 -from packageurl import PackageURL +import pytest 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.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()) - - -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) - - 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( + "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).to_dict() + expected_file = os.path.join(TEST_DATA, expected_filename) + check_results_against_json(results=results, expected_file=expected_file) + + +@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)