Skip to content

Commit 7d50ecd

Browse files
committed
Initial Ruby importer migration to Advisory V2
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 5da8c44 commit 7d50ecd

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-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: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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_range import GemVersionRange
19+
20+
from vulnerabilities.importer import AdvisoryData, AffectedPackageV2
21+
from vulnerabilities.importer import ReferenceV2
22+
from vulnerabilities.importer import VulnerabilitySeverity
23+
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
24+
from vulnerabilities.severity_systems import CVSSV2
25+
from vulnerabilities.severity_systems import CVSSV3
26+
from vulnerabilities.severity_systems import CVSSV4
27+
from vulnerabilities.utils import build_description
28+
from vulnerabilities.utils import get_advisory_url
29+
from vulnerabilities.utils import load_yaml
30+
31+
logger = logging.getLogger(__name__)
32+
33+
34+
class RubyImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
35+
license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt"
36+
repo_url = "git+https://github.com/rubysec/ruby-advisory-db"
37+
importer_name = "Ruby Importer"
38+
pipeline_id = "ruby_importer_v2"
39+
spdx_license_expression = "LicenseRef-scancode-public-domain-disclaimer"
40+
notice = """
41+
If you submit code or data to the ruby-advisory-db that is copyrighted by
42+
yourself, upon submission you hereby agree to release it into the public
43+
domain.
44+
45+
The data imported from the ruby-advisory-db have been filtered to exclude
46+
any non-public domain data from the data copyrighted by the Open
47+
Source Vulnerability Database (http://osvdb.org).
48+
49+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
51+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
52+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
53+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
54+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
55+
SOFTWARE.
56+
"""
57+
58+
@classmethod
59+
def steps(cls):
60+
return (
61+
cls.clone,
62+
cls.collect_and_store_advisories,
63+
cls.clean_downloads,
64+
)
65+
66+
def clone(self):
67+
self.log(f"Cloning `{self.repo_url}`")
68+
self.vcs_response = fetch_via_vcs(self.repo_url)
69+
70+
def advisories_count(self):
71+
return 10
72+
73+
def collect_advisories(self) -> Iterable[AdvisoryData]:
74+
base_path = Path(self.vcs_response.dest_dir)
75+
supported_subdir = ["rubies", "gems"]
76+
for subdir in supported_subdir:
77+
for file_path in base_path.glob(f"{subdir}/**/*.yml"):
78+
if file_path.name.startswith("OSVDB-"):
79+
continue
80+
81+
raw_data = load_yaml(file_path)
82+
advisory_id = file_path.stem
83+
advisory_url = get_advisory_url(
84+
file=file_path,
85+
base_path=base_path,
86+
url="https://github.com/rubysec/ruby-advisory-db/blob/master/",
87+
)
88+
yield parse_ruby_advisory(advisory_id, raw_data, subdir, advisory_url)
89+
90+
def clean_downloads(self):
91+
if self.vcs_response:
92+
self.log(f"Removing cloned repository")
93+
self.vcs_response.delete()
94+
95+
def on_failure(self):
96+
self.clean_downloads()
97+
98+
99+
def parse_ruby_advisory(advisory_id, record, schema_type, advisory_url):
100+
"""
101+
Parse a ruby advisory file and return an AdvisoryData or None.
102+
Each advisory file contains the advisory information in YAML format.
103+
Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas
104+
"""
105+
if schema_type == "gems":
106+
package_name = record.get("gem")
107+
108+
if not package_name:
109+
logger.error("Invalid package name")
110+
else:
111+
purl = PackageURL(type="gem", name=package_name)
112+
113+
return AdvisoryData(
114+
advisory_id=advisory_id,
115+
aliases=get_aliases(record),
116+
summary=get_summary(record),
117+
affected_packages=get_affected_packages(record, purl),
118+
references=get_references(record),
119+
severities=get_severities(record),
120+
date_published=get_publish_time(record),
121+
url=advisory_url,
122+
)
123+
124+
elif schema_type == "rubies":
125+
engine = record.get("engine") # engine enum: [jruby, rbx, ruby]
126+
if not engine:
127+
logger.error("Invalid engine name")
128+
else:
129+
purl = PackageURL(type="ruby", name=engine)
130+
return AdvisoryData(
131+
advisory_id=advisory_id,
132+
aliases=get_aliases(record),
133+
summary=get_summary(record),
134+
affected_packages=get_affected_packages(record, purl),
135+
severities=get_severities(record),
136+
references=get_references(record),
137+
date_published=get_publish_time(record),
138+
url=advisory_url,
139+
)
140+
141+
142+
def get_affected_packages(record, purl):
143+
"""
144+
Return AffectedPackage objects one for each affected_version_range and invert the safe_version_ranges
145+
( patched_versions , unaffected_versions ) then passing the purl and the inverted safe_version_range
146+
to the AffectedPackage object
147+
"""
148+
safe_version_ranges = record.get("patched_versions", [])
149+
# this case happens when the advisory contain only 'patched_versions' field
150+
# and it has value None(i.e it is empty :( ).
151+
if not safe_version_ranges:
152+
safe_version_ranges = []
153+
safe_version_ranges += record.get("unaffected_versions", [])
154+
safe_version_ranges = [i for i in safe_version_ranges if i]
155+
156+
affected_packages = []
157+
affected_version_ranges = [
158+
GemVersionRange.from_native(elem).invert() for elem in safe_version_ranges
159+
]
160+
161+
for affected_version_range in affected_version_ranges:
162+
affected_packages.append(
163+
AffectedPackageV2(
164+
package=purl,
165+
affected_version_range=affected_version_range,
166+
fixed_version_range=None
167+
)
168+
)
169+
return affected_packages
170+
171+
172+
def get_aliases(record) -> [str]:
173+
aliases = []
174+
if record.get("cve"):
175+
aliases.append("CVE-{}".format(record.get("cve")))
176+
if record.get("osvdb"):
177+
aliases.append("OSV-{}".format(record.get("osvdb")))
178+
if record.get("ghsa"):
179+
aliases.append("GHSA-{}".format(record.get("ghsa")))
180+
return aliases
181+
182+
183+
def get_references(record) -> [ReferenceV2]:
184+
references = []
185+
if record.get("url"):
186+
references.append(
187+
ReferenceV2(
188+
url=record.get("url"),
189+
)
190+
)
191+
return references
192+
193+
194+
def get_severities(record):
195+
"""
196+
Extract CVSS severity and return a list of VulnerabilitySeverity objects
197+
"""
198+
199+
severities = []
200+
cvss_v4 = record.get("cvss_v4")
201+
if cvss_v4:
202+
severities.append(
203+
VulnerabilitySeverity(system=CVSSV4, value=cvss_v4),
204+
)
205+
206+
cvss_v3 = record.get("cvss_v3")
207+
if cvss_v3:
208+
severities.append(VulnerabilitySeverity(system=CVSSV3, value=cvss_v4))
209+
210+
cvss_v2 = record.get("cvss_v2")
211+
if cvss_v2:
212+
severities.append(VulnerabilitySeverity(system=CVSSV2, value=cvss_v2))
213+
214+
return severities
215+
216+
217+
def get_publish_time(record):
218+
date = record.get("date")
219+
if not date:
220+
return
221+
return parse(date).replace(tzinfo=UTC)
222+
223+
224+
def get_summary(record):
225+
title = record.get("title") or ""
226+
description = record.get("description") or ""
227+
return build_description(summary=title, description=description)

0 commit comments

Comments
 (0)