diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index f508790c7..95a7d601b 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -59,6 +59,7 @@ from vulnerabilities.pipelines.v2_importers import pypa_importer as pypa_importer_v2 from vulnerabilities.pipelines.v2_importers import pysec_importer as pysec_importer_v2 from vulnerabilities.pipelines.v2_importers import redhat_importer as redhat_importer_v2 +from vulnerabilities.pipelines.v2_importers import ruby_importer as ruby_importer_v2 from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2 from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2 from vulnerabilities.utils import create_registry @@ -83,6 +84,7 @@ github_osv_importer_v2.GithubOSVImporterPipeline, redhat_importer_v2.RedHatImporterPipeline, aosp_importer_v2.AospImporterPipeline, + ruby_importer_v2.RubyImporterPipeline, nvd_importer.NVDImporterPipeline, github_importer.GitHubAPIImporterPipeline, gitlab_importer.GitLabImporterPipeline, diff --git a/vulnerabilities/pipelines/v2_importers/ruby_importer.py b/vulnerabilities/pipelines/v2_importers/ruby_importer.py new file mode 100644 index 000000000..d362d0e90 --- /dev/null +++ b/vulnerabilities/pipelines/v2_importers/ruby_importer.py @@ -0,0 +1,241 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import logging +from pathlib import Path +from typing import Iterable + +from dateutil.parser import parse +from fetchcode.vcs import fetch_via_vcs +from packageurl import PackageURL +from pytz import UTC +from univers.version_range import GemVersionRange +from univers.version_range import InvalidVersionRange + +from vulnerabilities.importer import AdvisoryData +from vulnerabilities.importer import AffectedPackageV2 +from vulnerabilities.importer import ReferenceV2 +from vulnerabilities.importer import VulnerabilitySeverity +from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 +from vulnerabilities.severity_systems import CVSSV2 +from vulnerabilities.severity_systems import CVSSV3 +from vulnerabilities.severity_systems import CVSSV4 +from vulnerabilities.utils import build_description +from vulnerabilities.utils import get_advisory_url +from vulnerabilities.utils import load_yaml + +logger = logging.getLogger(__name__) + + +class RubyImporterPipeline(VulnerableCodeBaseImporterPipelineV2): + 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" + pipeline_id = "ruby_importer_v2" + 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. + """ + + @classmethod + def steps(cls): + return ( + cls.clone, + cls.collect_and_store_advisories, + cls.clean_downloads, + ) + + def clone(self): + self.log(f"Cloning `{self.repo_url}`") + self.vcs_response = fetch_via_vcs(self.repo_url) + + def advisories_count(self): + base_path = Path(self.vcs_response.dest_dir) + return sum(1 for _ in base_path.rglob("*.yml")) + + def collect_advisories(self) -> Iterable[AdvisoryData]: + base_path = Path(self.vcs_response.dest_dir) + for file_path in base_path.rglob("*.yml"): + if file_path.name.startswith("OSVDB-"): + continue + + if "gems" in file_path.parts: + subdir = "gems" + elif "rubies" in file_path.parts: + subdir = "rubies" + else: + continue + + raw_data = load_yaml(file_path) + advisory_id = file_path.stem + 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(advisory_id, raw_data, subdir, advisory_url) + + def clean_downloads(self): + if self.vcs_response: + self.log(f"Removing cloned repository") + self.vcs_response.delete() + + def on_failure(self): + self.clean_downloads() + + +def parse_ruby_advisory(advisory_id, 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: + logger.error("Invalid package name") + return + + purl = PackageURL(type="gem", name=package_name) + return AdvisoryData( + advisory_id=advisory_id, + aliases=get_aliases(record), + summary=get_summary(record), + affected_packages=get_affected_packages(record, purl), + references=get_references(record), + severities=get_severities(record), + date_published=get_publish_time(record), + url=advisory_url, + ) + + elif schema_type == "rubies": + engine = record.get("engine") # engine enum: [jruby, rbx, ruby] + if not engine: + logger.error("Invalid engine name") + return + + purl = PackageURL(type="ruby", name=engine) + return AdvisoryData( + advisory_id=advisory_id, + aliases=get_aliases(record), + summary=get_summary(record), + affected_packages=get_affected_packages(record, purl), + severities=get_severities(record), + references=get_references(record), + date_published=get_publish_time(record), + url=advisory_url, + ) + + +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 + """ + affected_packages = [] + for unaffected_version in record.get("unaffected_versions", []): + try: + affected_version_range = GemVersionRange.from_native(unaffected_version).invert() + affected_packages.append( + AffectedPackageV2( + package=purl, + affected_version_range=affected_version_range, + fixed_version_range=None, + ) + ) + except InvalidVersionRange as e: + logger.error(f"InvalidVersionRange {e}") + + for patched_version in record.get("patched_versions", []): + try: + fixed_version_range = GemVersionRange.from_native(patched_version) + affected_packages.append( + AffectedPackageV2( + package=purl, + affected_version_range=None, + fixed_version_range=fixed_version_range, + ) + ) + except InvalidVersionRange as e: + logger.error(f"InvalidVersionRange {e}") + 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) -> [ReferenceV2]: + references = [] + if record.get("url"): + references.append( + ReferenceV2( + url=record.get("url"), + ) + ) + return references + + +def get_severities(record): + """ + Extract CVSS severity and return a list of VulnerabilitySeverity objects + """ + + severities = [] + cvss_v4 = record.get("cvss_v4") + if cvss_v4: + severities.append( + VulnerabilitySeverity(system=CVSSV4, value=cvss_v4), + ) + + cvss_v3 = record.get("cvss_v3") + if cvss_v3: + severities.append(VulnerabilitySeverity(system=CVSSV3, value=cvss_v3)) + + cvss_v2 = record.get("cvss_v2") + if cvss_v2: + severities.append(VulnerabilitySeverity(system=CVSSV2, value=cvss_v2)) + + return severities + + +def get_publish_time(record): + date = record.get("date") + if not date: + return + return parse(date).replace(tzinfo=UTC) + + +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/tests/pipelines/v2_importers/test_ruby_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_ruby_importer_v2.py new file mode 100644 index 000000000..6f7c6644b --- /dev/null +++ b/vulnerabilities/tests/pipelines/v2_importers/test_ruby_importer_v2.py @@ -0,0 +1,39 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +from pathlib import Path +from unittest.mock import Mock +from unittest.mock import patch + +import pytest + +from vulnerabilities.pipelines.v2_importers.ruby_importer import RubyImporterPipeline +from vulnerabilities.tests import util_tests + +TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "ruby-v2" + +TEST_CVE_FILES = [ + TEST_DATA / "gems/CVE-2020-5257.yml", + TEST_DATA / "gems/CVE-2024-6531.yml", + TEST_DATA / "rubies/CVE-2011-2686.yml", + TEST_DATA / "rubies/CVE-2022-25857.yml", +] + + +@pytest.mark.django_db +@pytest.mark.parametrize("yml_file", TEST_CVE_FILES) +def test_ruby_advisories_per_file(yml_file): + pipeline = RubyImporterPipeline() + pipeline.vcs_response = Mock(dest_dir=TEST_DATA) + + with patch.object(Path, "rglob", return_value=[yml_file]): + result = [adv.to_dict() for adv in pipeline.collect_advisories()] + + expected_file = yml_file.with_name(yml_file.stem + "-expected.json") + util_tests.check_results_against_json(result, expected_file) diff --git a/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2020-5257-expected.json b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2020-5257-expected.json new file mode 100644 index 000000000..5130f5d89 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2020-5257-expected.json @@ -0,0 +1,38 @@ +[ + { + "advisory_id": "CVE-2020-5257", + "aliases": [ + "CVE-2020-5257", + "GHSA-2p5p-m353-833w" + ], + "summary": "Sort order SQL injection via `direction` parameter in administrate\nIn Administrate (rubygem) before version 0.13.0, when sorting by attributes\non a dashboard, the direction parameter was not validated before being\ninterpolated into the SQL query.\n\nThis could present a SQL injection if the attacker were able to modify the\ndirection parameter and bypass ActiveRecord SQL protections.\n\nWhilst this does have a high-impact, to exploit this you need access to the\nAdministrate dashboards, which should generally be behind authentication.", + "affected_packages": [ + { + "package": { + "type": "gem", + "namespace": "", + "name": "administrate", + "version": "", + "qualifiers": "", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:gem/>=0.13.0", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "cvssv3", + "value": "7.7", + "scoring_elements": "" + } + ], + "date_published": "2020-03-14T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/CVE-2020-5257.yml" + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2020-5257.yml b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2020-5257.yml new file mode 100644 index 000000000..cde0fda72 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2020-5257.yml @@ -0,0 +1,24 @@ +--- +gem: administrate +cve: 2020-5257 +ghsa: 2p5p-m353-833w +title: Sort order SQL injection via `direction` parameter in administrate +date: 2020-03-14 +url: https://github.com/advisories/GHSA-2p5p-m353-833w +description: | + In Administrate (rubygem) before version 0.13.0, when sorting by attributes + on a dashboard, the direction parameter was not validated before being + interpolated into the SQL query. + + This could present a SQL injection if the attacker were able to modify the + direction parameter and bypass ActiveRecord SQL protections. + + Whilst this does have a high-impact, to exploit this you need access to the + Administrate dashboards, which should generally be behind authentication. +cvss_v3: 7.7 +patched_versions: + - ">= 0.13.0" + +related: + url: + - https://github.com/thoughtbot/administrate/commit/3ab838b83c5f565fba50e0c6f66fe4517f98eed3 \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2024-6531-expected.json b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2024-6531-expected.json new file mode 100644 index 000000000..5d0cace48 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2024-6531-expected.json @@ -0,0 +1,52 @@ +[ + { + "advisory_id": "CVE-2024-6531", + "aliases": [ + "CVE-2024-6531", + "GHSA-vc8w-jr9v-vj7f" + ], + "summary": "Bootstrap Cross-Site Scripting (XSS) vulnerability\nA vulnerability has been identified in Bootstrap that exposes users\nto Cross-Site Scripting (XSS) attacks. The issue is present in the\ncarousel component, where the data-slide and data-slide-to attributes\ncan be exploited through the href attribute of an tag due to\ninadequate sanitization. This vulnerability could potentially enable\nattackers to execute arbitrary JavaScript within the victim's browser.", + "affected_packages": [ + { + "package": { + "type": "gem", + "namespace": "", + "name": "bootstrap", + "version": "", + "qualifiers": "", + "subpath": "" + }, + "affected_version_range": "vers:gem/>=4.0.0", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + }, + { + "package": { + "type": "gem", + "namespace": "", + "name": "bootstrap", + "version": "", + "qualifiers": "", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:gem/>4.6.2", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "cvssv3", + "value": "6.4", + "scoring_elements": "" + } + ], + "date_published": "2024-07-11T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/CVE-2024-6531.yml" + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2024-6531.yml b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2024-6531.yml new file mode 100644 index 000000000..0cdc7c5f2 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/gems/CVE-2024-6531.yml @@ -0,0 +1,24 @@ +--- +gem: bootstrap +cve: 2024-6531 +ghsa: vc8w-jr9v-vj7f +url: https://github.com/advisories/GHSA-vc8w-jr9v-vj7f +title: Bootstrap Cross-Site Scripting (XSS) vulnerability +date: 2024-07-11 +description: | + A vulnerability has been identified in Bootstrap that exposes users + to Cross-Site Scripting (XSS) attacks. The issue is present in the + carousel component, where the data-slide and data-slide-to attributes + can be exploited through the href attribute of an tag due to + inadequate sanitization. This vulnerability could potentially enable + attackers to execute arbitrary JavaScript within the victim's browser. +cvss_v3: 6.4 +unaffected_versions: + - "< 4.0.0" +patched_versions: + - "> 4.6.2" +related: + url: + - https://nvd.nist.gov/vuln/detail/CVE-2024-6531 + - https://www.herodevs.com/vulnerability-directory/cve-2024-6531 + - https://github.com/advisories/GHSA-vc8w-jr9v-vj7f \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2011-2686-expected.json b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2011-2686-expected.json new file mode 100644 index 000000000..e8ed301bd --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2011-2686-expected.json @@ -0,0 +1,51 @@ +[ + { + "advisory_id": "CVE-2011-2686", + "aliases": [ + "CVE-2011-2686" + ], + "summary": "Ruby Random Number Generation Local Denial Of Service Vulnerability\nRuby before 1.8.7-p352 does not reset the random seed upon forking, which\nmakes it easier for context-dependent attackers to predict the values of\nrandom numbers by leveraging knowledge of the number sequence obtained in a\ndifferent child process, a related issue to CVE-2003-0900. NOTE: this issue\nexists because of a regression during Ruby 1.8.6 development.", + "affected_packages": [ + { + "package": { + "type": "ruby", + "namespace": "", + "name": "ruby", + "version": "", + "qualifiers": "", + "subpath": "" + }, + "affected_version_range": "vers:gem/>=1.8.6.399", + "fixed_version_range": null, + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + }, + { + "package": { + "type": "ruby", + "namespace": "", + "name": "ruby", + "version": "", + "qualifiers": "", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:gem/>=1.8.7.352", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "cvssv2", + "value": "5.0", + "scoring_elements": "" + } + ], + "date_published": "2011-07-02T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/rubies/CVE-2011-2686.yml" + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2011-2686.yml b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2011-2686.yml new file mode 100644 index 000000000..747813073 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2011-2686.yml @@ -0,0 +1,17 @@ +--- +engine: ruby +cve: 2011-2686 +url: https://osdir.com/ml/lang-ruby-core/2011-01/msg00917.html +title: Ruby Random Number Generation Local Denial Of Service Vulnerability +date: 2011-07-02 +description: | + Ruby before 1.8.7-p352 does not reset the random seed upon forking, which + makes it easier for context-dependent attackers to predict the values of + random numbers by leveraging knowledge of the number sequence obtained in a + different child process, a related issue to CVE-2003-0900. NOTE: this issue + exists because of a regression during Ruby 1.8.6 development. +cvss_v2: 5.0 +unaffected_versions: + - "< 1.8.6.399" +patched_versions: + - ">= 1.8.7.352" \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2022-25857-expected.json b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2022-25857-expected.json new file mode 100644 index 000000000..2c044a942 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2022-25857-expected.json @@ -0,0 +1,37 @@ +[ + { + "advisory_id": "CVE-2022-25857", + "aliases": [ + "CVE-2022-25857" + ], + "summary": "CVE-2022-25857 jruby/psych/snakeyaml: Denial of Service (DoS) due missing to nested depth limitation for collections\nThe package org.yaml:snakeyaml from 0 and before 1.31 are vulnerable to Denial of Service (DoS) due missing to nested depth limitation for collections.\nThis package is bundled into Psych which is in turn bundled into jruby.", + "affected_packages": [ + { + "package": { + "type": "ruby", + "namespace": "", + "name": "jruby", + "version": "", + "qualifiers": "", + "subpath": "" + }, + "affected_version_range": null, + "fixed_version_range": "vers:gem/>=9.3.8.0", + "introduced_by_commit_patches": [], + "fixed_by_commit_patches": [] + } + ], + "references_v2": [], + "patches": [], + "severities": [ + { + "system": "cvssv3", + "value": "7.5", + "scoring_elements": "" + } + ], + "date_published": "2022-02-24T00:00:00+00:00", + "weaknesses": [], + "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/rubies/CVE-2022-25857.yml" + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2022-25857.yml b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2022-25857.yml new file mode 100644 index 000000000..47e1e52d6 --- /dev/null +++ b/vulnerabilities/tests/test_data/ruby-v2/rubies/CVE-2022-25857.yml @@ -0,0 +1,12 @@ +--- +engine: jruby +cve: 2022-25857 +url: https://github.com/jruby/jruby/issues/7342 +title: "CVE-2022-25857 jruby/psych/snakeyaml: Denial of Service (DoS) due missing to nested depth limitation for collections" +date: 2022-02-24 +description: | + The package org.yaml:snakeyaml from 0 and before 1.31 are vulnerable to Denial of Service (DoS) due missing to nested depth limitation for collections. + This package is bundled into Psych which is in turn bundled into jruby. +cvss_v3: 7.5 +patched_versions: + - ">= 9.3.8.0" \ No newline at end of file