Skip to content

Commit

Permalink
Add a docstring to get_affected_packages
Browse files Browse the repository at this point in the history
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 <tushar.goel.dav@gmail.com>

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 <ziadhany2016@gmail.com>
  • Loading branch information
ziadhany committed Nov 12, 2023
1 parent d8cdaf4 commit 03e0b80
Show file tree
Hide file tree
Showing 19 changed files with 700 additions and 255 deletions.
2 changes: 2 additions & 0 deletions vulnerabilities/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from vulnerabilities.importers import pysec
from vulnerabilities.importers import redhat
from vulnerabilities.importers import retiredotnet
from vulnerabilities.importers import ruby
from vulnerabilities.importers import suse_scores
from vulnerabilities.importers import ubuntu
from vulnerabilities.importers import ubuntu_usn
Expand Down Expand Up @@ -65,6 +66,7 @@
ubuntu_usn.UbuntuUSNImporter,
fireeye.FireyeImporter,
apache_kafka.ApacheKafkaImporter,
ruby.RubyImporter,
]

IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}
240 changes: 135 additions & 105 deletions vulnerabilities/importers/ruby.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,132 +7,162 @@
# See https://aboutcode.org for more information about nexB OSS projects.
#

import asyncio
from typing import List
from typing import Set
import logging
from pathlib import Path
from typing import Iterable

from dateutil.parser import parse
from packageurl import PackageURL
from pytz import UTC
from univers.version_range import VersionRange
from univers.versions import SemverVersion
from univers.version_range import GemVersionRange

from vulnerabilities.importer import AdvisoryData
from vulnerabilities.importer import AffectedPackage
from vulnerabilities.importer import Importer
from vulnerabilities.importer import Reference
from vulnerabilities.package_managers import RubyVersionAPI
from vulnerabilities.importer import VulnerabilitySeverity
from vulnerabilities.severity_systems import SCORING_SYSTEMS
from vulnerabilities.utils import build_description
from vulnerabilities.utils import load_yaml
from vulnerabilities.utils import nearest_patched_package

logger = logging.getLogger(__name__)

class RubyImporter(Importer):
def __enter__(self):
super(RubyImporter, self).__enter__()

if not getattr(self, "_added_files", None):
self._added_files, self._updated_files = self.file_changes(
recursive=True, file_ext="yml", subdir="./gems"
)

self.pkg_manager_api = RubyVersionAPI()
self.set_api(self.collect_packages())

def set_api(self, packages):
asyncio.run(self.pkg_manager_api.load_api(packages))

def updated_advisories(self) -> Set[AdvisoryData]:
files = self._updated_files.union(self._added_files)
advisories = []
for f in files:
processed_data = self.process_file(f)
if processed_data:
advisories.append(processed_data)
return self.batch_advisories(advisories)

def collect_packages(self):
packages = set()
files = self._updated_files.union(self._added_files)
for f in files:
data = load_yaml(f)
if data.get("gem"):
packages.add(data["gem"])

return packages

def process_file(self, path) -> List[AdvisoryData]:
record = load_yaml(path)
class RubyImporter(Importer):
license_url = "https://github.com/rubysec/ruby-advisory-db/blob/master/LICENSE.txt"
spdx_license_expression = "unknown"
repo_url = "git+https://github.com/rubysec/ruby-advisory-db"

def advisory_data(self) -> Iterable[AdvisoryData]:
try:
self.clone(self.repo_url)
base_path = Path(self.vcs_response.dest_dir)
supported_subdir = ["rubies", "gems"]
for subdir in supported_subdir:
for file_path in base_path.glob(f"{subdir}/**/*.yml"):
if file_path.name.startswith("OSVDB-"):
continue
raw_data = load_yaml(file_path)
yield parse_ruby_advisory(raw_data, subdir)
finally:
if self.vcs_response:
self.vcs_response.delete()


def parse_ruby_advisory(record, schema_type):
"""
Parse a ruby advisory file and return an AdvisoryData or None.
Each advisory file contains the advisory information in YAML format.
Schema: https://github.com/rubysec/ruby-advisory-db/tree/master/spec/schemas
"""
if schema_type == "gems":
package_name = record.get("gem")
if not package_name:
return

if "cve" in record:
cve_id = "CVE-{}".format(record["cve"])
if not package_name:
logger.error("Invalid package name")
else:
return

publish_time = parse(record["date"]).replace(tzinfo=UTC)
safe_version_ranges = record.get("patched_versions", [])
# this case happens when the advisory contain only 'patched_versions' field
# and it has value None(i.e it is empty :( ).
if not safe_version_ranges:
safe_version_ranges = []
safe_version_ranges += record.get("unaffected_versions", [])
safe_version_ranges = [i for i in safe_version_ranges if i]

if not getattr(self, "pkg_manager_api", None):
self.pkg_manager_api = RubyVersionAPI()
all_vers = self.pkg_manager_api.get(package_name, until=publish_time).valid_versions
safe_versions, affected_versions = self.categorize_versions(all_vers, safe_version_ranges)

impacted_purls = [
PackageURL(
name=package_name,
type="gem",
version=version,
purl = PackageURL(type="gem", name=package_name)

return AdvisoryData(
aliases=get_aliases(record),
summary=get_summary(record),
affected_packages=get_affected_packages(record, purl),
references=get_references(record),
date_published=get_publish_time(record),
)
for version in affected_versions
]

resolved_purls = [
PackageURL(
name=package_name,
type="gem",
version=version,

elif schema_type == "rubies":
engine = record.get("engine") # engine enum: [jruby, rbx, ruby]
if not engine:
logger.error("Invalid engine name")
else:
purl = PackageURL(type="ruby", name=engine)
return AdvisoryData(
aliases=get_aliases(record),
summary=get_summary(record),
affected_packages=get_affected_packages(record, purl),
references=get_references(record),
date_published=get_publish_time(record),
)
for version in safe_versions
]

references = []
if record.get("url"):
references.append(Reference(url=record.get("url")))

return AdvisoryData(
summary=record.get("description", ""),
affected_packages=nearest_patched_package(impacted_purls, resolved_purls),
references=references,
vulnerability_id=cve_id,
def get_affected_packages(record, purl):
"""
Return AffectedPackage objects one for each affected_version_range and invert the safe_version_ranges
( patched_versions , unaffected_versions ) then passing the purl and the inverted safe_version_range
to the AffectedPackage object
"""
safe_version_ranges = record.get("patched_versions", [])
# this case happens when the advisory contain only 'patched_versions' field
# and it has value None(i.e it is empty :( ).
if not safe_version_ranges:
safe_version_ranges = []
safe_version_ranges += record.get("unaffected_versions", [])
safe_version_ranges = [i for i in safe_version_ranges if i]

affected_packages = []
affected_version_ranges = [
GemVersionRange.from_native(elem).invert() for elem in safe_version_ranges
]

for affected_version_range in affected_version_ranges:
affected_packages.append(
AffectedPackage(
package=purl,
affected_version_range=affected_version_range,
)
)
return affected_packages


def get_aliases(record) -> [str]:
aliases = []
if record.get("cve"):
aliases.append("CVE-{}".format(record.get("cve")))
if record.get("osvdb"):
aliases.append("OSV-{}".format(record.get("osvdb")))
if record.get("ghsa"):
aliases.append("GHSA-{}".format(record.get("ghsa")))
return aliases

@staticmethod
def categorize_versions(all_versions, unaffected_version_ranges):

for id, elem in enumerate(unaffected_version_ranges):
unaffected_version_ranges[id] = VersionRange.from_scheme_version_spec_string(
"semver", elem
def get_references(record) -> [Reference]:
references = []
cvss_v2 = record.get("cvss_v2")
cvss_v3 = record.get("cvss_v3")

if record.get("url"):
if not (cvss_v2 or cvss_v3):
references.append(Reference(url=record.get("url")))
if cvss_v2:
references.append(
Reference(
url=record.get("url"),
severities=[
VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv2"], value=cvss_v2)
],
)
)
if cvss_v3:
references.append(
Reference(
url=record.get("url"),
severities=[
VulnerabilitySeverity(system=SCORING_SYSTEMS["cvssv3"], value=cvss_v3)
],
)
)
return references


def get_publish_time(record):
date = record.get("date")
if not date:
return
return parse(date).replace(tzinfo=UTC)


safe_versions = []
vulnerable_versions = []
for i in all_versions:
vobj = SemverVersion(i)
is_vulnerable = False
for ver_rng in unaffected_version_ranges:
if vobj in ver_rng:
safe_versions.append(i)
is_vulnerable = True
break

if not is_vulnerable:
vulnerable_versions.append(i)

return safe_versions, vulnerable_versions
def get_summary(record):
title = record.get("title") or ""
description = record.get("description") or ""
return build_description(summary=title, description=description)
1 change: 1 addition & 0 deletions vulnerabilities/improvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
valid_versions.IstioImprover,
valid_versions.DebianOvalImprover,
valid_versions.UbuntuOvalImprover,
valid_versions.RubyImprover,
]

IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY}
8 changes: 8 additions & 0 deletions vulnerabilities/improvers/valid_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from django.db.models.query import QuerySet
from packageurl import PackageURL
from univers.versions import NginxVersion
from univers.versions import RubygemsVersion

from vulnerabilities.importer import AdvisoryData
from vulnerabilities.importer import AffectedPackage
Expand All @@ -35,6 +36,7 @@
from vulnerabilities.importers.istio import IstioImporter
from vulnerabilities.importers.nginx import NginxImporter
from vulnerabilities.importers.npm import NpmImporter
from vulnerabilities.importers.ruby import RubyImporter
from vulnerabilities.importers.ubuntu import UbuntuImporter
from vulnerabilities.improver import MAX_CONFIDENCE
from vulnerabilities.improver import Improver
Expand All @@ -43,6 +45,7 @@
from vulnerabilities.package_managers import GitHubTagsAPI
from vulnerabilities.package_managers import GoproxyVersionAPI
from vulnerabilities.package_managers import PackageVersion
from vulnerabilities.package_managers import RubyVersionAPI
from vulnerabilities.package_managers import VersionAPI
from vulnerabilities.package_managers import get_api_package_name
from vulnerabilities.package_managers import get_version_fetcher
Expand Down Expand Up @@ -477,3 +480,8 @@ class DebianOvalImprover(ValidVersionImprover):
class UbuntuOvalImprover(ValidVersionImprover):
importer = UbuntuImporter
ignorable_versions = []


class RubyImprover(ValidVersionImprover):
importer = RubyImporter
ignorable_versions = []
1 change: 0 additions & 1 deletion vulnerabilities/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def no_rmtree(monkeypatch):
collect_ignore = [
"test_models.py",
"test_package_managers.py",
"test_ruby.py",
"test_rust.py",
"test_suse_backports.py",
"test_suse.py",
Expand Down
47 changes: 47 additions & 0 deletions vulnerabilities/tests/test_data/ruby/CVE-2007-5770-expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"aliases": [
"CVE-2007-5770"
],
"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.",
"affected_packages": [
{
"package": {
"type": "ruby",
"namespace": null,
"name": "ruby",
"version": null,
"qualifiers": null,
"subpath": null
},
"affected_version_range": "vers:gem/<1.8.6.230|>=1.8.7",
"fixed_version": null
},
{
"package": {
"type": "ruby",
"namespace": null,
"name": "ruby",
"version": null,
"qualifiers": null,
"subpath": null
},
"affected_version_range": "vers:gem/<1.8.7",
"fixed_version": null
}
],
"references": [
{
"reference_id": "",
"url": "http://www.cvedetails.com/cve/CVE-2007-5770/",
"severities": [
{
"system": "cvssv2",
"value": "4.3",
"scoring_elements": ""
}
]
}
],
"date_published": "2007-10-08T00:00:00+00:00",
"weaknesses": []
}
17 changes: 17 additions & 0 deletions vulnerabilities/tests/test_data/ruby/CVE-2007-5770.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
engine: ruby
cve: 2007-5770
url: http://www.cvedetails.com/cve/CVE-2007-5770/
title: Ruby Net::HTTPS library does not validate server certificate CN
date: 2007-10-08
description: |
The (1) Net::ftptls, (2) Net::telnets, (3) Net::imap, (4) Net::pop, and (5)
Net::smtp libraries in Ruby 1.8.5 and 1.8.6 do not verify that the
commonName (CN) field in a server certificate matches the domain name in a
request sent over SSL, which makes it easier for remote attackers to
intercept SSL transmissions via a man-in-the-middle attack or spoofed web
site, different components than CVE-2007-5162.
cvss_v2: 4.3
patched_versions:
- ~> 1.8.6.230
- '>= 1.8.7'
Loading

0 comments on commit 03e0b80

Please sign in to comment.