Skip to content

Commit fc3cd05

Browse files
authored
Migrate Ruby importer to advisory V2 (#2086)
* Initial Ruby importer migration to Advisory V2 Signed-off-by: ziad hany <ziadhany2016@gmail.com> * Migrate Ruby importer to Advisory V2 Add a test Signed-off-by: ziad hany <ziadhany2016@gmail.com> * Catch invalid version range constraints. Signed-off-by: ziad hany <ziadhany2016@gmail.com> * Update the Ruby importer so the advisory ID is unique Signed-off-by: ziad hany <ziadhany2016@gmail.com> --------- Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 5da8c44 commit fc3cd05

File tree

11 files changed

+556
-0
lines changed

11 files changed

+556
-0
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from vulnerabilities.pipelines.v2_importers import pypa_importer as pypa_importer_v2
6161
from vulnerabilities.pipelines.v2_importers import pysec_importer as pysec_importer_v2
6262
from vulnerabilities.pipelines.v2_importers import redhat_importer as redhat_importer_v2
63+
from vulnerabilities.pipelines.v2_importers import ruby_importer as ruby_importer_v2
6364
from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2
6465
from vulnerabilities.pipelines.v2_importers import xen_importer as xen_importer_v2
6566
from vulnerabilities.utils import create_registry
@@ -84,6 +85,7 @@
8485
github_osv_importer_v2.GithubOSVImporterPipeline,
8586
redhat_importer_v2.RedHatImporterPipeline,
8687
aosp_importer_v2.AospImporterPipeline,
88+
ruby_importer_v2.RubyImporterPipeline,
8789
epss_importer_v2.EPSSImporterPipeline,
8890
nvd_importer.NVDImporterPipeline,
8991
github_importer.GitHubAPIImporterPipeline,
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
import logging
11+
from pathlib import Path
12+
from typing import Iterable
13+
14+
from dateutil.parser import parse
15+
from fetchcode.vcs import fetch_via_vcs
16+
from packageurl import PackageURL
17+
from pytz import UTC
18+
from univers.version_constraint import validate_comparators
19+
from univers.version_range import GemVersionRange
20+
21+
from vulnerabilities.importer import AdvisoryData
22+
from vulnerabilities.importer import AffectedPackageV2
23+
from vulnerabilities.importer import ReferenceV2
24+
from vulnerabilities.importer import VulnerabilitySeverity
25+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
26+
from vulnerabilities.severity_systems import CVSSV2
27+
from vulnerabilities.severity_systems import CVSSV3
28+
from vulnerabilities.severity_systems import CVSSV4
29+
from vulnerabilities.utils import build_description
30+
from vulnerabilities.utils import get_advisory_url
31+
from vulnerabilities.utils import load_yaml
32+
33+
logger = logging.getLogger(__name__)
34+
35+
36+
class RubyImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
37+
license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt"
38+
repo_url = "git+https://github.com/rubysec/ruby-advisory-db"
39+
importer_name = "Ruby Importer"
40+
pipeline_id = "ruby_importer_v2"
41+
spdx_license_expression = "LicenseRef-scancode-public-domain-disclaimer"
42+
notice = """
43+
If you submit code or data to the ruby-advisory-db that is copyrighted by
44+
yourself, upon submission you hereby agree to release it into the public
45+
domain.
46+
47+
The data imported from the ruby-advisory-db have been filtered to exclude
48+
any non-public domain data from the data copyrighted by the Open
49+
Source Vulnerability Database (http://osvdb.org).
50+
51+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
52+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
53+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
54+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
55+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
56+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
57+
SOFTWARE.
58+
"""
59+
60+
@classmethod
61+
def steps(cls):
62+
return (
63+
cls.clone,
64+
cls.collect_and_store_advisories,
65+
cls.clean_downloads,
66+
)
67+
68+
def clone(self):
69+
self.log(f"Cloning `{self.repo_url}`")
70+
self.vcs_response = fetch_via_vcs(self.repo_url)
71+
72+
def advisories_count(self):
73+
base_path = Path(self.vcs_response.dest_dir)
74+
return sum(1 for _ in base_path.rglob("*.yml"))
75+
76+
def collect_advisories(self) -> Iterable[AdvisoryData]:
77+
base_path = Path(self.vcs_response.dest_dir)
78+
for file_path in base_path.rglob("*.yml"):
79+
if file_path.name.startswith("OSVDB-"):
80+
continue
81+
82+
if "gems" in file_path.parts:
83+
subdir = "gems"
84+
elif "rubies" in file_path.parts:
85+
subdir = "rubies"
86+
else:
87+
continue
88+
89+
raw_data = load_yaml(file_path)
90+
advisory_id = str(file_path.relative_to(base_path).with_suffix(""))
91+
advisory_url = get_advisory_url(
92+
file=file_path,
93+
base_path=base_path,
94+
url="https://github.com/rubysec/ruby-advisory-db/blob/master/",
95+
)
96+
yield parse_ruby_advisory(advisory_id, raw_data, subdir, advisory_url)
97+
98+
def clean_downloads(self):
99+
if self.vcs_response:
100+
self.log(f"Removing cloned repository")
101+
self.vcs_response.delete()
102+
103+
def on_failure(self):
104+
self.clean_downloads()
105+
106+
107+
def parse_ruby_advisory(advisory_id, record, schema_type, advisory_url):
108+
"""
109+
Parse a ruby advisory file and return an AdvisoryData or None.
110+
Each advisory file contains the advisory information in YAML format.
111+
Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas
112+
"""
113+
if schema_type == "gems":
114+
package_name = record.get("gem")
115+
116+
if not package_name:
117+
logger.error("Invalid package name")
118+
return
119+
120+
purl = PackageURL(type="gem", name=package_name)
121+
return AdvisoryData(
122+
advisory_id=advisory_id,
123+
aliases=get_aliases(record),
124+
summary=get_summary(record),
125+
affected_packages=get_affected_packages(record, purl),
126+
references_v2=get_references(record),
127+
severities=get_severities(record),
128+
date_published=get_publish_time(record),
129+
url=advisory_url,
130+
)
131+
132+
elif schema_type == "rubies":
133+
engine = record.get("engine") # engine enum: [jruby, rbx, ruby]
134+
if not engine:
135+
logger.error("Invalid engine name")
136+
return
137+
138+
purl = PackageURL(type="ruby", name=engine)
139+
return AdvisoryData(
140+
advisory_id=advisory_id,
141+
aliases=get_aliases(record),
142+
summary=get_summary(record),
143+
affected_packages=get_affected_packages(record, purl),
144+
severities=get_severities(record),
145+
references=get_references(record),
146+
date_published=get_publish_time(record),
147+
url=advisory_url,
148+
)
149+
150+
151+
def get_affected_packages(record, purl):
152+
"""
153+
Return AffectedPackage objects one for each affected_version_range and invert the safe_version_ranges
154+
( patched_versions , unaffected_versions ) then passing the purl and the inverted safe_version_range
155+
to the AffectedPackage object
156+
"""
157+
affected_packages = []
158+
for unaffected_version in record.get("unaffected_versions", []):
159+
try:
160+
affected_version_range = GemVersionRange.from_native(unaffected_version).invert()
161+
validate_comparators(affected_version_range.constraints)
162+
affected_packages.append(
163+
AffectedPackageV2(
164+
package=purl,
165+
affected_version_range=affected_version_range,
166+
fixed_version_range=None,
167+
)
168+
)
169+
except ValueError as e:
170+
logger.error(
171+
f"Invalid VersionRange Constraints for unaffected_version: {unaffected_version} - error: {e}"
172+
)
173+
174+
for patched_version in record.get("patched_versions", []):
175+
try:
176+
fixed_version_range = GemVersionRange.from_native(patched_version)
177+
validate_comparators(fixed_version_range.constraints)
178+
affected_packages.append(
179+
AffectedPackageV2(
180+
package=purl,
181+
affected_version_range=None,
182+
fixed_version_range=fixed_version_range,
183+
)
184+
)
185+
except ValueError as e:
186+
logger.error(
187+
f"Invalid VersionRange Constraints for patched_version: {patched_version} - error: {e}"
188+
)
189+
190+
return affected_packages
191+
192+
193+
def get_aliases(record) -> [str]:
194+
aliases = []
195+
if record.get("cve"):
196+
aliases.append("CVE-{}".format(record.get("cve")))
197+
if record.get("osvdb"):
198+
aliases.append("OSV-{}".format(record.get("osvdb")))
199+
if record.get("ghsa"):
200+
aliases.append("GHSA-{}".format(record.get("ghsa")))
201+
return aliases
202+
203+
204+
def get_references(record) -> [ReferenceV2]:
205+
references = []
206+
if record.get("url"):
207+
references.append(
208+
ReferenceV2(
209+
url=record.get("url"),
210+
)
211+
)
212+
return references
213+
214+
215+
def get_severities(record):
216+
"""
217+
Extract CVSS severity and return a list of VulnerabilitySeverity objects
218+
"""
219+
220+
severities = []
221+
cvss_v4 = record.get("cvss_v4")
222+
if cvss_v4:
223+
severities.append(
224+
VulnerabilitySeverity(system=CVSSV4, value=cvss_v4),
225+
)
226+
227+
cvss_v3 = record.get("cvss_v3")
228+
if cvss_v3:
229+
severities.append(VulnerabilitySeverity(system=CVSSV3, value=cvss_v3))
230+
231+
cvss_v2 = record.get("cvss_v2")
232+
if cvss_v2:
233+
severities.append(VulnerabilitySeverity(system=CVSSV2, value=cvss_v2))
234+
235+
return severities
236+
237+
238+
def get_publish_time(record):
239+
date = record.get("date")
240+
if not date:
241+
return
242+
return parse(date).replace(tzinfo=UTC)
243+
244+
245+
def get_summary(record):
246+
title = record.get("title") or ""
247+
description = record.get("description") or ""
248+
return build_description(summary=title, description=description)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from pathlib import Path
11+
from unittest.mock import Mock
12+
from unittest.mock import patch
13+
14+
import pytest
15+
16+
from vulnerabilities.pipelines.v2_importers.ruby_importer import RubyImporterPipeline
17+
from vulnerabilities.tests import util_tests
18+
19+
TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "ruby-v2"
20+
21+
TEST_CVE_FILES = [
22+
TEST_DATA / "gems/CVE-2020-5257.yml",
23+
TEST_DATA / "gems/CVE-2024-6531.yml",
24+
TEST_DATA / "rubies/CVE-2011-2686.yml",
25+
TEST_DATA / "rubies/CVE-2022-25857.yml",
26+
]
27+
28+
29+
@pytest.mark.django_db
30+
@pytest.mark.parametrize("yml_file", TEST_CVE_FILES)
31+
def test_ruby_advisories_per_file(yml_file):
32+
pipeline = RubyImporterPipeline()
33+
pipeline.vcs_response = Mock(dest_dir=TEST_DATA)
34+
35+
with patch.object(Path, "rglob", return_value=[yml_file]):
36+
result = [adv.to_dict() for adv in pipeline.collect_advisories()]
37+
38+
expected_file = yml_file.with_name(yml_file.stem + "-expected.json")
39+
util_tests.check_results_against_json(result, expected_file)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"advisory_id": "gems/CVE-2020-5257",
4+
"aliases": [
5+
"CVE-2020-5257",
6+
"GHSA-2p5p-m353-833w"
7+
],
8+
"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.",
9+
"affected_packages": [
10+
{
11+
"package": {
12+
"type": "gem",
13+
"namespace": "",
14+
"name": "administrate",
15+
"version": "",
16+
"qualifiers": "",
17+
"subpath": ""
18+
},
19+
"affected_version_range": null,
20+
"fixed_version_range": "vers:gem/>=0.13.0",
21+
"introduced_by_commit_patches": [],
22+
"fixed_by_commit_patches": []
23+
}
24+
],
25+
"references_v2": [
26+
{
27+
"reference_id": "",
28+
"reference_type": "",
29+
"url": "https://github.com/advisories/GHSA-2p5p-m353-833w"
30+
}
31+
],
32+
"patches": [],
33+
"severities": [
34+
{
35+
"system": "cvssv3",
36+
"value": "7.7",
37+
"scoring_elements": ""
38+
}
39+
],
40+
"date_published": "2020-03-14T00:00:00+00:00",
41+
"weaknesses": [],
42+
"url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/CVE-2020-5257.yml"
43+
}
44+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
gem: administrate
3+
cve: 2020-5257
4+
ghsa: 2p5p-m353-833w
5+
title: Sort order SQL injection via `direction` parameter in administrate
6+
date: 2020-03-14
7+
url: https://github.com/advisories/GHSA-2p5p-m353-833w
8+
description: |
9+
In Administrate (rubygem) before version 0.13.0, when sorting by attributes
10+
on a dashboard, the direction parameter was not validated before being
11+
interpolated into the SQL query.
12+
13+
This could present a SQL injection if the attacker were able to modify the
14+
direction parameter and bypass ActiveRecord SQL protections.
15+
16+
Whilst this does have a high-impact, to exploit this you need access to the
17+
Administrate dashboards, which should generally be behind authentication.
18+
cvss_v3: 7.7
19+
patched_versions:
20+
- ">= 0.13.0"
21+
22+
related:
23+
url:
24+
- https://github.com/thoughtbot/administrate/commit/3ab838b83c5f565fba50e0c6f66fe4517f98eed3

0 commit comments

Comments
 (0)