Skip to content

Commit eeca595

Browse files
committed
Add ruby importer and improver
Fix style test Fix test Rewrite affected_packages Ruby initial config Reference: #796 Signed-off-by: ziadhany <ziadhany2016@gmail.com>
1 parent 2646d7e commit eeca595

15 files changed

Lines changed: 396 additions & 264 deletions

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from vulnerabilities.importers import pysec
3232
from vulnerabilities.importers import redhat
3333
from vulnerabilities.importers import retiredotnet
34+
from vulnerabilities.importers import ruby
3435
from vulnerabilities.importers import suse_scores
3536
from vulnerabilities.importers import ubuntu
3637
from vulnerabilities.importers import ubuntu_usn
@@ -64,6 +65,7 @@
6465
xen.XenImporter,
6566
ubuntu_usn.UbuntuUSNImporter,
6667
fireeye.FireyeImporter,
68+
ruby.RubyImporter,
6769
apache_kafka.ApacheKafkaImporter,
6870
]
6971

vulnerabilities/importers/ruby.py

Lines changed: 167 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,132 +7,192 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import asyncio
11-
from typing import List
12-
from typing import Set
10+
import logging
11+
from pathlib import Path
12+
from typing import Iterable
1313

1414
from dateutil.parser import parse
15+
from django.db.models import QuerySet
1516
from packageurl import PackageURL
1617
from pytz import UTC
17-
from univers.version_range import VersionRange
18-
from univers.versions import SemverVersion
18+
from univers.version_range import GemVersionRange
19+
from univers.versions import RubygemsVersion
1920

2021
from vulnerabilities.importer import AdvisoryData
22+
from vulnerabilities.importer import AffectedPackage
2123
from vulnerabilities.importer import GitImporter
2224
from vulnerabilities.importer import Reference
25+
from vulnerabilities.importer import VulnerabilitySeverity
26+
from vulnerabilities.improver import Improver
27+
from vulnerabilities.improver import Inference
28+
from vulnerabilities.improvers.valid_versions import ValidVersionImprover
29+
from vulnerabilities.models import Advisory
2330
from vulnerabilities.package_managers import RubyVersionAPI
31+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
32+
from vulnerabilities.utils import build_description
33+
from vulnerabilities.utils import evolve_purl
2434
from vulnerabilities.utils import load_yaml
25-
from vulnerabilities.utils import nearest_patched_package
2635

36+
logger = logging.getLogger(__name__)
2737

28-
class RubyImporter(GitImporter):
29-
def __enter__(self):
30-
super(RubyImporter, self).__enter__()
31-
32-
if not getattr(self, "_added_files", None):
33-
self._added_files, self._updated_files = self.file_changes(
34-
recursive=True, file_ext="yml", subdir="./gems"
35-
)
3638

37-
self.pkg_manager_api = RubyVersionAPI()
38-
self.set_api(self.collect_packages())
39-
40-
def set_api(self, packages):
41-
asyncio.run(self.pkg_manager_api.load_api(packages))
42-
43-
def updated_advisories(self) -> Set[AdvisoryData]:
44-
files = self._updated_files.union(self._added_files)
45-
advisories = []
46-
for f in files:
47-
processed_data = self.process_file(f)
48-
if processed_data:
49-
advisories.append(processed_data)
50-
return self.batch_advisories(advisories)
51-
52-
def collect_packages(self):
53-
packages = set()
54-
files = self._updated_files.union(self._added_files)
55-
for f in files:
56-
data = load_yaml(f)
57-
if data.get("gem"):
58-
packages.add(data["gem"])
59-
60-
return packages
61-
62-
def process_file(self, path) -> List[AdvisoryData]:
63-
record = load_yaml(path)
39+
class RubyImporter(GitImporter):
40+
license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt"
41+
spdx_license_expression = "unknown"
42+
43+
def __init__(self):
44+
super().__init__(repo_url="git+https://github.com/rubysec/ruby-advisory-db")
45+
46+
def advisory_data(self) -> Iterable[AdvisoryData]:
47+
self.clone()
48+
base_path = Path(self.vcs_response.dest_dir)
49+
supported_subdir = ["rubies", "gems"]
50+
for subdir in supported_subdir:
51+
for file_path in base_path.glob(f"{subdir}/**/*.yml"):
52+
if file_path.name.startswith("OSVDB-"):
53+
continue
54+
raw_data = load_yaml(file_path)
55+
yield parse_ruby_advisory(raw_data, subdir)
56+
57+
58+
def parse_ruby_advisory(record, schema_type):
59+
"""
60+
Parse a ruby advisory file and return an AdvisoryData or None.
61+
Each advisory file contains the advisory information in YAML format.
62+
Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas
63+
"""
64+
if schema_type == "gems":
6465
package_name = record.get("gem")
65-
if not package_name:
66-
return
67-
68-
if "cve" in record:
69-
cve_id = "CVE-{}".format(record["cve"])
70-
else:
71-
return
72-
73-
publish_time = parse(record["date"]).replace(tzinfo=UTC)
74-
safe_version_ranges = record.get("patched_versions", [])
75-
# this case happens when the advisory contain only 'patched_versions' field
76-
# and it has value None(i.e it is empty :( ).
77-
if not safe_version_ranges:
78-
safe_version_ranges = []
79-
safe_version_ranges += record.get("unaffected_versions", [])
80-
safe_version_ranges = [i for i in safe_version_ranges if i]
81-
82-
if not getattr(self, "pkg_manager_api", None):
83-
self.pkg_manager_api = RubyVersionAPI()
84-
all_vers = self.pkg_manager_api.get(package_name, until=publish_time).valid_versions
85-
safe_versions, affected_versions = self.categorize_versions(all_vers, safe_version_ranges)
86-
87-
impacted_purls = [
88-
PackageURL(
89-
name=package_name,
90-
type="gem",
91-
version=version,
92-
)
93-
for version in affected_versions
94-
]
95-
96-
resolved_purls = [
97-
PackageURL(
98-
name=package_name,
99-
type="gem",
100-
version=version,
101-
)
102-
for version in safe_versions
103-
]
66+
library = record.get("library")
67+
framework = record.get("framework")
68+
platform = record.get("platform")
69+
purl = PackageURL(type="gem", name=package_name)
10470

105-
references = []
106-
if record.get("url"):
107-
references.append(Reference(url=record.get("url")))
71+
return AdvisoryData(
72+
aliases=get_aliases(record),
73+
summary=get_summary(record),
74+
affected_packages=get_affected_packages(record, purl),
75+
references=get_references(record),
76+
date_published=get_publish_time(record),
77+
)
10878

79+
elif schema_type == "rubies":
80+
engine = record.get("engine") # engine enum: [jruby, rbx, ruby]
81+
purl = PackageURL(type="ruby", name=engine)
10982
return AdvisoryData(
110-
summary=record.get("description", ""),
111-
affected_packages=nearest_patched_package(impacted_purls, resolved_purls),
112-
references=references,
113-
vulnerability_id=cve_id,
83+
aliases=get_aliases(record),
84+
summary=get_summary(record),
85+
affected_packages=get_affected_packages(record, purl),
86+
references=get_references(record),
87+
date_published=get_publish_time(record),
11488
)
11589

116-
@staticmethod
117-
def categorize_versions(all_versions, unaffected_version_ranges):
11890

119-
for id, elem in enumerate(unaffected_version_ranges):
120-
unaffected_version_ranges[id] = VersionRange.from_scheme_version_spec_string(
121-
"semver", elem
91+
def get_affected_packages(record, purl):
92+
safe_version_ranges = record.get("patched_versions", [])
93+
# this case happens when the advisory contain only 'patched_versions' field
94+
# and it has value None(i.e it is empty :( ).
95+
if not safe_version_ranges:
96+
safe_version_ranges = []
97+
safe_version_ranges += record.get("unaffected_versions", [])
98+
safe_version_ranges = [i for i in safe_version_ranges if i]
99+
100+
affected_packages = []
101+
affected_version_ranges = [
102+
GemVersionRange.from_native(elem).invert() for elem in safe_version_ranges
103+
]
104+
105+
for affected_version_range in affected_version_ranges:
106+
affected_packages.append(
107+
AffectedPackage(
108+
package=purl,
109+
affected_version_range=affected_version_range,
122110
)
111+
)
112+
return affected_packages
113+
114+
115+
def get_aliases(record) -> [str]:
116+
aliases = []
117+
if record.get("cve"):
118+
aliases.append("CVE-{}".format(record.get("cve")))
119+
if record.get("osvdb"):
120+
aliases.append("OSV-{}".format(record.get("osvdb")))
121+
if record.get("ghsa"):
122+
aliases.append("GHSA-{}".format(record.get("ghsa")))
123+
return aliases
124+
123125

124-
safe_versions = []
125-
vulnerable_versions = []
126-
for i in all_versions:
127-
vobj = SemverVersion(i)
128-
is_vulnerable = False
129-
for ver_rng in unaffected_version_ranges:
130-
if vobj in ver_rng:
131-
safe_versions.append(i)
132-
is_vulnerable = True
133-
break
134-
135-
if not is_vulnerable:
136-
vulnerable_versions.append(i)
137-
138-
return safe_versions, vulnerable_versions
126+
def get_references(record) -> [Reference]:
127+
references = []
128+
cvss_v2 = record.get("cvss_v2")
129+
cvss_v3 = record.get("cvss_v3")
130+
131+
if record.get("url"):
132+
if not (cvss_v2 or cvss_v3):
133+
references.append(Reference(url=record.get("url")))
134+
if cvss_v2:
135+
references.append(
136+
Reference(
137+
url=record.get("url"),
138+
severities=[
139+
VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv2"], value=cvss_v2)
140+
],
141+
)
142+
)
143+
if cvss_v3:
144+
references.append(
145+
Reference(
146+
url=record.get("url"),
147+
severities=[
148+
VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv3"], value=cvss_v3)
149+
],
150+
)
151+
)
152+
return references
153+
154+
155+
def get_publish_time(record):
156+
return parse(record["date"]).replace(tzinfo=UTC)
157+
158+
159+
def get_summary(record):
160+
title = record.get("title")
161+
description = record.get("description", "")
162+
return build_description(summary=title, description=description)
163+
164+
165+
class RubyImprover(Improver):
166+
pkg_manager_api = RubyVersionAPI()
167+
168+
@property
169+
def interesting_advisories(self) -> QuerySet:
170+
return Advisory.objects.filter(created_by=RubyImporter.qualified_name)
171+
172+
def get_inferences(self, advisory_data) -> Iterable[Inference]:
173+
for affected_package in advisory_data.affected_packages:
174+
purl = affected_package.package
175+
pkg_name = purl.name
176+
all_vers_pkgs = self.pkg_manager_api.fetch(pkg_name)
177+
178+
safe_versions = []
179+
affected_purls = []
180+
for pkg_version in all_vers_pkgs:
181+
vobj = RubygemsVersion(pkg_version.value)
182+
try:
183+
if vobj in affected_package.affected_version_range:
184+
new_purl = evolve_purl(purl=purl, version=str(pkg_version.value))
185+
affected_purls.append(new_purl)
186+
else:
187+
safe_versions.append(pkg_version.value)
188+
except Exception as e:
189+
logger.error(f"{e}")
190+
191+
for fixed_version in safe_versions:
192+
fixed_purl = evolve_purl(purl=purl, version=str(fixed_version))
193+
yield Inference.from_advisory_data(
194+
advisory_data,
195+
confidence=90,
196+
affected_purls=affected_purls,
197+
fixed_purl=fixed_purl,
198+
)

vulnerabilities/tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def no_rmtree(monkeypatch):
2727
collect_ignore = [
2828
"test_models.py",
2929
"test_package_managers.py",
30-
"test_ruby.py",
3130
"test_rust.py",
3231
"test_suse_backports.py",
3332
"test_suse.py",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"aliases": [
3+
"CVE-2007-5770"
4+
],
5+
"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.",
6+
"affected_packages": [
7+
{
8+
"package": {
9+
"type": "ruby",
10+
"namespace": null,
11+
"name": "ruby",
12+
"version": null,
13+
"qualifiers": null,
14+
"subpath": null
15+
},
16+
"affected_version_range": "vers:gem/<1.8.6.230|>=1.8.7",
17+
"fixed_version": null
18+
},
19+
{
20+
"package": {
21+
"type": "ruby",
22+
"namespace": null,
23+
"name": "ruby",
24+
"version": null,
25+
"qualifiers": null,
26+
"subpath": null
27+
},
28+
"affected_version_range": "vers:gem/<1.8.7",
29+
"fixed_version": null
30+
}
31+
],
32+
"references": [
33+
{
34+
"reference_id": "",
35+
"url": "http://www.cvedetails.com/cve/CVE-2007-5770/",
36+
"severities": [
37+
{
38+
"system": "cvssv2",
39+
"value": "4.3",
40+
"scoring_elements": ""
41+
}
42+
]
43+
}
44+
],
45+
"date_published": "2007-10-08T00:00:00+00:00",
46+
"weaknesses": []
47+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
engine: ruby
3+
cve: 2007-5770
4+
url: http://www.cvedetails.com/cve/CVE-2007-5770/
5+
title: Ruby Net::HTTPS library does not validate server certificate CN
6+
date: 2007-10-08
7+
description: |
8+
The (1) Net::ftptls, (2) Net::telnets, (3) Net::imap, (4) Net::pop, and (5)
9+
Net::smtp libraries in Ruby 1.8.5 and 1.8.6 do not verify that the
10+
commonName (CN) field in a server certificate matches the domain name in a
11+
request sent over SSL, which makes it easier for remote attackers to
12+
intercept SSL transmissions via a man-in-the-middle attack or spoofed web
13+
site, different components than CVE-2007-5162.
14+
cvss_v2: 4.3
15+
patched_versions:
16+
- ~> 1.8.6.230
17+
- '>= 1.8.7'

0 commit comments

Comments
 (0)