Skip to content

Commit 03e0b80

Browse files
committed
Add a docstring to get_affected_packages
Add a unite test for get_affected_packages function Remove unused variables Fix sorted affected_package_merge Add ruby importer and improver Fix style test Fix test Rewrite affected_packages Ruby initial config Reference: #796 Clean imported data after import process Signed-off-by: Tushar Goel <[email protected]> Fix sorted affected_package_merge Refactor Ruby importer and improver 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 d8cdaf4 commit 03e0b80

19 files changed

+700
-255
lines changed

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
@@ -65,6 +66,7 @@
6566
ubuntu_usn.UbuntuUSNImporter,
6667
fireeye.FireyeImporter,
6768
apache_kafka.ApacheKafkaImporter,
69+
ruby.RubyImporter,
6870
]
6971

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

vulnerabilities/importers/ruby.py

Lines changed: 135 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -7,132 +7,162 @@
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
1515
from packageurl import PackageURL
1616
from pytz import UTC
17-
from univers.version_range import VersionRange
18-
from univers.versions import SemverVersion
17+
from univers.version_range import GemVersionRange
1918

2019
from vulnerabilities.importer import AdvisoryData
20+
from vulnerabilities.importer import AffectedPackage
2121
from vulnerabilities.importer import Importer
2222
from vulnerabilities.importer import Reference
23-
from vulnerabilities.package_managers import RubyVersionAPI
23+
from vulnerabilities.importer import VulnerabilitySeverity
24+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
25+
from vulnerabilities.utils import build_description
2426
from vulnerabilities.utils import load_yaml
25-
from vulnerabilities.utils import nearest_patched_package
2627

28+
logger = logging.getLogger(__name__)
2729

28-
class RubyImporter(Importer):
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-
)
3630

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)
31+
class RubyImporter(Importer):
32+
license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt"
33+
spdx_license_expression = "unknown"
34+
repo_url = "git+https://github.com/rubysec/ruby-advisory-db"
35+
36+
def advisory_data(self) -> Iterable[AdvisoryData]:
37+
try:
38+
self.clone(self.repo_url)
39+
base_path = Path(self.vcs_response.dest_dir)
40+
supported_subdir = ["rubies", "gems"]
41+
for subdir in supported_subdir:
42+
for file_path in base_path.glob(f"{subdir}/**/*.yml"):
43+
if file_path.name.startswith("OSVDB-"):
44+
continue
45+
raw_data = load_yaml(file_path)
46+
yield parse_ruby_advisory(raw_data, subdir)
47+
finally:
48+
if self.vcs_response:
49+
self.vcs_response.delete()
50+
51+
52+
def parse_ruby_advisory(record, schema_type):
53+
"""
54+
Parse a ruby advisory file and return an AdvisoryData or None.
55+
Each advisory file contains the advisory information in YAML format.
56+
Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas
57+
"""
58+
if schema_type == "gems":
6459
package_name = record.get("gem")
65-
if not package_name:
66-
return
6760

68-
if "cve" in record:
69-
cve_id = "CVE-{}".format(record["cve"])
61+
if not package_name:
62+
logger.error("Invalid package name")
7063
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,
64+
purl = PackageURL(type="gem", name=package_name)
65+
66+
return AdvisoryData(
67+
aliases=get_aliases(record),
68+
summary=get_summary(record),
69+
affected_packages=get_affected_packages(record, purl),
70+
references=get_references(record),
71+
date_published=get_publish_time(record),
9272
)
93-
for version in affected_versions
94-
]
95-
96-
resolved_purls = [
97-
PackageURL(
98-
name=package_name,
99-
type="gem",
100-
version=version,
73+
74+
elif schema_type == "rubies":
75+
engine = record.get("engine") # engine enum: [jruby, rbx, ruby]
76+
if not engine:
77+
logger.error("Invalid engine name")
78+
else:
79+
purl = PackageURL(type="ruby", name=engine)
80+
return AdvisoryData(
81+
aliases=get_aliases(record),
82+
summary=get_summary(record),
83+
affected_packages=get_affected_packages(record, purl),
84+
references=get_references(record),
85+
date_published=get_publish_time(record),
10186
)
102-
for version in safe_versions
103-
]
10487

105-
references = []
106-
if record.get("url"):
107-
references.append(Reference(url=record.get("url")))
10888

109-
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,
89+
def get_affected_packages(record, purl):
90+
"""
91+
Return AffectedPackage objects one for each affected_version_range and invert the safe_version_ranges
92+
( patched_versions , unaffected_versions ) then passing the purl and the inverted safe_version_range
93+
to the AffectedPackage object
94+
"""
95+
safe_version_ranges = record.get("patched_versions", [])
96+
# this case happens when the advisory contain only 'patched_versions' field
97+
# and it has value None(i.e it is empty :( ).
98+
if not safe_version_ranges:
99+
safe_version_ranges = []
100+
safe_version_ranges += record.get("unaffected_versions", [])
101+
safe_version_ranges = [i for i in safe_version_ranges if i]
102+
103+
affected_packages = []
104+
affected_version_ranges = [
105+
GemVersionRange.from_native(elem).invert() for elem in safe_version_ranges
106+
]
107+
108+
for affected_version_range in affected_version_ranges:
109+
affected_packages.append(
110+
AffectedPackage(
111+
package=purl,
112+
affected_version_range=affected_version_range,
113+
)
114114
)
115+
return affected_packages
116+
117+
118+
def get_aliases(record) -> [str]:
119+
aliases = []
120+
if record.get("cve"):
121+
aliases.append("CVE-{}".format(record.get("cve")))
122+
if record.get("osvdb"):
123+
aliases.append("OSV-{}".format(record.get("osvdb")))
124+
if record.get("ghsa"):
125+
aliases.append("GHSA-{}".format(record.get("ghsa")))
126+
return aliases
115127

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

119-
for id, elem in enumerate(unaffected_version_ranges):
120-
unaffected_version_ranges[id] = VersionRange.from_scheme_version_spec_string(
121-
"semver", elem
129+
def get_references(record) -> [Reference]:
130+
references = []
131+
cvss_v2 = record.get("cvss_v2")
132+
cvss_v3 = record.get("cvss_v3")
133+
134+
if record.get("url"):
135+
if not (cvss_v2 or cvss_v3):
136+
references.append(Reference(url=record.get("url")))
137+
if cvss_v2:
138+
references.append(
139+
Reference(
140+
url=record.get("url"),
141+
severities=[
142+
VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv2"], value=cvss_v2)
143+
],
144+
)
122145
)
146+
if cvss_v3:
147+
references.append(
148+
Reference(
149+
url=record.get("url"),
150+
severities=[
151+
VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv3"], value=cvss_v3)
152+
],
153+
)
154+
)
155+
return references
156+
157+
158+
def get_publish_time(record):
159+
date = record.get("date")
160+
if not date:
161+
return
162+
return parse(date).replace(tzinfo=UTC)
163+
123164

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
165+
def get_summary(record):
166+
title = record.get("title") or ""
167+
description = record.get("description") or ""
168+
return build_description(summary=title, description=description)

vulnerabilities/improvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
valid_versions.IstioImprover,
2323
valid_versions.DebianOvalImprover,
2424
valid_versions.UbuntuOvalImprover,
25+
valid_versions.RubyImprover,
2526
]
2627

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

vulnerabilities/improvers/valid_versions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from django.db.models.query import QuerySet
2020
from packageurl import PackageURL
2121
from univers.versions import NginxVersion
22+
from univers.versions import RubygemsVersion
2223

2324
from vulnerabilities.importer import AdvisoryData
2425
from vulnerabilities.importer import AffectedPackage
@@ -35,6 +36,7 @@
3536
from vulnerabilities.importers.istio import IstioImporter
3637
from vulnerabilities.importers.nginx import NginxImporter
3738
from vulnerabilities.importers.npm import NpmImporter
39+
from vulnerabilities.importers.ruby import RubyImporter
3840
from vulnerabilities.importers.ubuntu import UbuntuImporter
3941
from vulnerabilities.improver import MAX_CONFIDENCE
4042
from vulnerabilities.improver import Improver
@@ -43,6 +45,7 @@
4345
from vulnerabilities.package_managers import GitHubTagsAPI
4446
from vulnerabilities.package_managers import GoproxyVersionAPI
4547
from vulnerabilities.package_managers import PackageVersion
48+
from vulnerabilities.package_managers import RubyVersionAPI
4649
from vulnerabilities.package_managers import VersionAPI
4750
from vulnerabilities.package_managers import get_api_package_name
4851
from vulnerabilities.package_managers import get_version_fetcher
@@ -477,3 +480,8 @@ class DebianOvalImprover(ValidVersionImprover):
477480
class UbuntuOvalImprover(ValidVersionImprover):
478481
importer = UbuntuImporter
479482
ignorable_versions = []
483+
484+
485+
class RubyImprover(ValidVersionImprover):
486+
importer = RubyImporter
487+
ignorable_versions = []

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)