diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index ea23d708d..b3521796a 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -65,8 +65,8 @@ xen.XenImporter, ubuntu_usn.UbuntuUSNImporter, fireeye.FireyeImporter, - ruby.RubyImporter, 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 57457cf0b..b36383d24 100644 --- a/vulnerabilities/importers/ruby.py +++ b/vulnerabilities/importers/ruby.py @@ -12,39 +12,29 @@ from typing import Iterable from dateutil.parser import parse -from django.db.models import QuerySet from packageurl import PackageURL from pytz import UTC from univers.version_range import GemVersionRange -from univers.versions import RubygemsVersion from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackage -from vulnerabilities.importer import GitImporter +from vulnerabilities.importer import Importer from vulnerabilities.importer import Reference from vulnerabilities.importer import VulnerabilitySeverity -from vulnerabilities.improver import Improver -from vulnerabilities.improver import Inference -from vulnerabilities.improvers.valid_versions import ValidVersionImprover -from vulnerabilities.models import Advisory -from vulnerabilities.package_managers import RubyVersionAPI from vulnerabilities.severity_systems import SCORING_SYSTEMS from vulnerabilities.utils import build_description -from vulnerabilities.utils import evolve_purl from vulnerabilities.utils import load_yaml logger = logging.getLogger(__name__) -class RubyImporter(GitImporter): +class RubyImporter(Importer): license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt" spdx_license_expression = "unknown" - - def __init__(self): - super().__init__(repo_url="git+https://github.com/rubysec/ruby-advisory-db") + repo_url = "git+https://github.com/rubysec/ruby-advisory-db" def advisory_data(self) -> Iterable[AdvisoryData]: - self.clone() + self.clone(self.repo_url) base_path = Path(self.vcs_response.dest_dir) supported_subdir = ["rubies", "gems"] for subdir in supported_subdir: @@ -63,9 +53,9 @@ def parse_ruby_advisory(record, schema_type): """ if schema_type == "gems": package_name = record.get("gem") - library = record.get("library") - framework = record.get("framework") - platform = record.get("platform") + library = record.get("library") # not used + framework = record.get("framework") # not used + platform = record.get("platform") # not used purl = PackageURL(type="gem", name=package_name) return AdvisoryData( @@ -160,39 +150,3 @@ def get_summary(record): title = record.get("title") description = record.get("description", "") return build_description(summary=title, description=description) - - -class RubyImprover(Improver): - pkg_manager_api = RubyVersionAPI() - - @property - def interesting_advisories(self) -> QuerySet: - return Advisory.objects.filter(created_by=RubyImporter.qualified_name) - - def get_inferences(self, advisory_data) -> Iterable[Inference]: - for affected_package in advisory_data.affected_packages: - purl = affected_package.package - pkg_name = purl.name - all_vers_pkgs = self.pkg_manager_api.fetch(pkg_name) - - safe_versions = [] - affected_purls = [] - for pkg_version in all_vers_pkgs: - vobj = RubygemsVersion(pkg_version.value) - try: - if vobj in affected_package.affected_version_range: - new_purl = evolve_purl(purl=purl, version=str(pkg_version.value)) - affected_purls.append(new_purl) - else: - safe_versions.append(pkg_version.value) - except Exception as e: - logger.error(f"{e}") - - for fixed_version in safe_versions: - fixed_purl = evolve_purl(purl=purl, version=str(fixed_version)) - yield Inference.from_advisory_data( - advisory_data, - confidence=90, - affected_purls=affected_purls, - fixed_purl=fixed_purl, - ) diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index 629ece67f..ed451bb43 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -24,6 +24,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..9fe688afa 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,47 @@ class DebianOvalImprover(ValidVersionImprover): class UbuntuOvalImprover(ValidVersionImprover): importer = UbuntuImporter ignorable_versions = [] + + +class RubyImprover(ValidVersionImprover): + importer = RubyImporter + ignorable_versions = [] + + def get_inferences(self, advisory_data) -> Iterable[Inference]: + + try: + purl, affected_version_ranges, fixed_versions = AffectedPackage.merge( + advisory_data.affected_packages + ) + except UnMergeablePackageError: + logger.error( + f"RubyImprover: Cannot merge with different purls: " + f"{advisory_data.affected_packages!r}" + ) + return iter([]) + + all_vers_pkgs = self.get_package_versions(purl) + affected_purls = [] + + for pkg_version in all_vers_pkgs: + vobj = RubygemsVersion(pkg_version) + + affected_version = True + for affected_version_range in affected_version_ranges: + if vobj not in affected_version_range: + affected_version = False + + if affected_version: + new_purl = evolve_purl(purl=purl, version=str(pkg_version)) + affected_purls.append(new_purl) + else: + fixed_versions.append(RubygemsVersion(pkg_version)) + + for fixed_version in fixed_versions: + fixed_purl = evolve_purl(purl=purl, version=str(fixed_version)) + yield Inference.from_advisory_data( + advisory_data, + confidence=90, + affected_purls=affected_purls, + fixed_purl=fixed_purl, + ) 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..408d35ede --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby/ruby-improver-expected.json @@ -0,0 +1,238 @@ +[ + { + "vulnerability_id": null, + "aliases": [ + "CVE-2018-7212" + ], + "confidence": 90, + "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 + } + ], + "fixed_purl": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "0.2.6", + "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": 90, + "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 + } + ], + "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": 90, + "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 + } + ], + "fixed_purl": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "3.0.2", + "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": 90, + "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 + } + ], + "fixed_purl": { + "type": "gem", + "namespace": null, + "name": "sinatra", + "version": "3.0.5", + "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": [] + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_ruby.py b/vulnerabilities/tests/test_ruby.py index 13377e099..30edb11e8 100644 --- a/vulnerabilities/tests/test_ruby.py +++ b/vulnerabilities/tests/test_ruby.py @@ -6,11 +6,17 @@ # 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 +from unittest.mock import patch import pytest +from vulnerabilities.importer import AdvisoryData 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 @@ -33,3 +39,19 @@ def test_advisories(filename, expected_filename, schema_type): 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()] + 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)