Skip to content

Commit c6091c9

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 <[email protected]>
1 parent 8134a08 commit c6091c9

16 files changed

+396
-264
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from vulnerabilities.importers import pysec
3131
from vulnerabilities.importers import redhat
3232
from vulnerabilities.importers import retiredotnet
33+
from vulnerabilities.importers import ruby
3334
from vulnerabilities.importers import suse_scores
3435
from vulnerabilities.importers import ubuntu
3536
from vulnerabilities.importers import ubuntu_usn
@@ -63,6 +64,7 @@
6364
xen.XenImporter,
6465
ubuntu_usn.UbuntuUSNImporter,
6566
fireeye.FireyeImporter,
67+
ruby.RubyImporter,
6668
]
6769

6870
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}

vulnerabilities/importers/ruby.py

Lines changed: 166 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,132 +7,191 @@
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.models import Advisory
2329
from vulnerabilities.package_managers import RubyVersionAPI
30+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
31+
from vulnerabilities.utils import build_description
32+
from vulnerabilities.utils import evolve_purl
2433
from vulnerabilities.utils import load_yaml
25-
from vulnerabilities.utils import nearest_patched_package
2634

35+
logger = logging.getLogger(__name__)
2736

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-
)
3637

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)
38+
class RubyImporter(GitImporter):
39+
license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt"
40+
spdx_license_expression = "unknown"
41+
42+
def __init__(self):
43+
super().__init__(repo_url="git+https://github.com/rubysec/ruby-advisory-db")
44+
45+
def advisory_data(self) -> Iterable[AdvisoryData]:
46+
self.clone()
47+
base_path = Path(self.vcs_response.dest_dir)
48+
supported_subdir = ["rubies", "gems"]
49+
for subdir in supported_subdir:
50+
for file_path in base_path.glob(f"{subdir}/**/*.yml"):
51+
if file_path.name.startswith("OSVDB-"):
52+
continue
53+
raw_data = load_yaml(file_path)
54+
yield parse_ruby_advisory(raw_data, subdir)
55+
56+
57+
def parse_ruby_advisory(record, schema_type):
58+
"""
59+
Parse a ruby advisory file and return an AdvisoryData or None.
60+
Each advisory file contains the advisory information in YAML format.
61+
Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas
62+
"""
63+
if schema_type == "gems":
6464
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-
]
65+
library = record.get("library")
66+
framework = record.get("framework")
67+
platform = record.get("platform")
68+
purl = PackageURL(type="gem", name=package_name)
10469

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

78+
elif schema_type == "rubies":
79+
engine = record.get("engine") # engine enum: [jruby, rbx, ruby]
80+
purl = PackageURL(type="ruby", name=engine)
10981
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,
82+
aliases=get_aliases(record),
83+
summary=get_summary(record),
84+
affected_packages=get_affected_packages(record, purl),
85+
references=get_references(record),
86+
date_published=get_publish_time(record),
11487
)
11588

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

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

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

vulnerabilities/improvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
oval.DebianOvalBasicImprover,
2222
oval.UbuntuOvalBasicImprover,
2323
importers.apache_httpd.ApacheHTTPDImprover,
24+
importers.ruby.RubyImprover,
2425
]
2526

2627
IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY}

vulnerabilities/tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def no_rmtree(monkeypatch):
2828
"test_apache_kafka.py",
2929
"test_models.py",
3030
"test_package_managers.py",
31-
"test_ruby.py",
3231
"test_rust.py",
3332
"test_suse_backports.py",
3433
"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)