Skip to content

Commit 13e58a5

Browse files
committed
Migrate oval importer and improver
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent d7627ef commit 13e58a5

File tree

11 files changed

+334
-474
lines changed

11 files changed

+334
-474
lines changed

vulnerabilities/importer.py

Lines changed: 39 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@
3535
from typing import Set
3636
from typing import Tuple
3737

38+
import pytz
3839
from binaryornot.helpers import is_binary_string
40+
from dateutil import parser as dateparser
3941
from git import DiffIndex
4042
from git import Repo
4143
from license_expression import Licensing
4244
from packageurl import PackageURL
45+
from univers.version_range import RANGE_CLASS_BY_SCHEMES
4346
from univers.version_range import VersionRange
4447
from univers.versions import Version
4548

@@ -521,13 +524,13 @@ class OvalImporter(Importer):
521524
"""
522525

523526
@staticmethod
524-
def create_purl(pkg_name: str, pkg_version: str, pkg_data: Mapping) -> PackageURL:
527+
def create_purl(pkg_name: str, pkg_data: Mapping) -> PackageURL:
525528
"""
526529
Helper method for creating different purls for subclasses without them reimplementing
527530
get_data_from_xml_doc method
528531
Note: pkg_data must include 'type' of package
529532
"""
530-
return PackageURL(name=pkg_name, version=pkg_version, **pkg_data)
533+
return PackageURL(name=pkg_name, **pkg_data)
531534

532535
@staticmethod
533536
def _collect_pkgs(parsed_oval_data: Mapping) -> Set:
@@ -561,28 +564,17 @@ def advisory_data(self) -> List[AdvisoryData]:
561564
for metadata, oval_file in self._fetch():
562565
try:
563566
oval_data = self.get_data_from_xml_doc(oval_file, metadata)
564-
yield oval_data
567+
yield from oval_data
565568
except Exception:
566569
logger.error(
567570
f"Failed to get updated_advisories: {oval_file!r} "
568571
f"with {metadata!r}:\n" + traceback.format_exc()
569572
)
570573
continue
571574

572-
def set_api(self, all_pkgs: Iterable[str]):
573-
"""
574-
This method loads the self.pkg_manager_api with the specified packages.
575-
It fetches and caches all the versions of these packages and exposes
576-
them through self.pkg_manager_api.get(<package_name>). Example
577-
578-
>> self.set_api(['electron'])
579-
Assume 'electron' has only versions 1.0.0 and 1.2.0
580-
>> assert self.pkg_manager_api.get('electron') == {'1.0.0','1.2.0'}
581-
582-
"""
583-
raise NotImplementedError
584-
585-
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[AdvisoryData]:
575+
def get_data_from_xml_doc(
576+
self, xml_doc: ET.ElementTree, pkg_metadata={}
577+
) -> Iterable[AdvisoryData]:
586578
"""
587579
The orchestration method of the OvalDataSource. This method breaks an
588580
OVAL xml ElementTree into a list of `Advisory`.
@@ -593,12 +585,10 @@ def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> Lis
593585
Example value of pkg_metadata:
594586
{"type":"deb","qualifiers":{"distro":"buster"} }
595587
"""
596-
597-
all_adv = []
598-
oval_doc = OvalParser(self.translations, xml_doc)
599-
raw_data = oval_doc.get_data()
600-
all_pkgs = self._collect_pkgs(raw_data)
601-
self.set_api(all_pkgs)
588+
oval_parsed_data = OvalParser(self.translations, xml_doc)
589+
raw_data = oval_parsed_data.get_data()
590+
oval_doc = oval_parsed_data.oval_document
591+
timestamp = oval_doc.getGenerator().getTimestamp()
602592

603593
# convert definition_data to Advisory objects
604594
for definition_data in raw_data:
@@ -610,49 +600,31 @@ def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> Lis
610600
affected_packages = []
611601
for test_data in definition_data["test_data"]:
612602
for package_name in test_data["package_list"]:
613-
if package_name and len(package_name) >= 50:
614-
continue
615-
616-
affected_version_range = test_data["version_ranges"] or set()
617-
version_class = version_class_by_package_type[pkg_metadata["type"]]
618-
version_scheme = version_class.scheme
619-
620-
affected_version_range = VersionRange.from_scheme_version_spec_string(
621-
version_scheme, affected_version_range
622-
)
623-
all_versions = self.pkg_manager_api.get(package_name).valid_versions
624-
625-
# FIXME: what is this 50 DB limit? that's too small for versions
626-
# FIXME: we should not drop data this way
627-
# This filter is for filtering out long versions.
628-
# 50 is limit because that's what db permits atm.
629-
all_versions = [version for version in all_versions if len(version) < 50]
630-
if not all_versions:
631-
continue
632-
633-
affected_purls = []
634-
safe_purls = []
635-
for version in all_versions:
636-
purl = self.create_purl(
637-
pkg_name=package_name,
638-
pkg_version=version,
639-
pkg_data=pkg_metadata,
603+
affected_version_range = test_data["version_ranges"]
604+
vrc = RANGE_CLASS_BY_SCHEMES[pkg_metadata["type"]]
605+
if affected_version_range:
606+
try:
607+
affected_version_range = vrc.from_native(affected_version_range)
608+
except Exception as e:
609+
logger.error(
610+
f"Failed to parse version range {affected_version_range!r} "
611+
f"for package {package_name!r}:\n{e}"
612+
)
613+
continue
614+
if package_name:
615+
affected_packages.append(
616+
AffectedPackage(
617+
package=self.create_purl(package_name, pkg_metadata),
618+
affected_version_range=affected_version_range,
619+
)
640620
)
641-
if version_class(version) in affected_version_range:
642-
affected_purls.append(purl)
643-
else:
644-
safe_purls.append(purl)
645-
646-
affected_packages.extend(
647-
nearest_patched_package(affected_purls, safe_purls),
648-
)
649-
650-
all_adv.append(
651-
AdvisoryData(
652-
summary=description,
653-
affected_packages=affected_packages,
654-
vulnerability_id=vuln_id,
655-
references=references,
656-
)
621+
date_published = dateparser.parse(timestamp)
622+
if not date_published.tzinfo:
623+
date_published = date_published.replace(tzinfo=pytz.UTC)
624+
yield AdvisoryData(
625+
aliases=[vuln_id],
626+
summary=description,
627+
affected_packages=affected_packages,
628+
references=sorted(references),
629+
date_published=date_published,
657630
)
658-
return all_adv

vulnerabilities/importers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
from vulnerabilities.importers import alpine_linux
2323
from vulnerabilities.importers import debian
24+
from vulnerabilities.importers import debian_oval
2425
from vulnerabilities.importers import github
2526
from vulnerabilities.importers import gitlab
2627
from vulnerabilities.importers import nginx
2728
from vulnerabilities.importers import nvd
2829
from vulnerabilities.importers import openssl
2930
from vulnerabilities.importers import pysec
3031
from vulnerabilities.importers import redhat
32+
from vulnerabilities.importers import ubuntu
3133

3234
IMPORTERS_REGISTRY = [
3335
nginx.NginxImporter,
@@ -39,6 +41,8 @@
3941
pysec.PyPIImporter,
4042
debian.DebianImporter,
4143
gitlab.GitLabAPIImporter,
44+
debian_oval.DebianOvalImporter,
45+
ubuntu.UbuntuImporter,
4246
]
4347

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

vulnerabilities/importers/debian_oval.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,29 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

2323

24-
import asyncio
2524
import xml.etree.ElementTree as ET
2625

2726
import requests
2827

2928
from vulnerabilities.importer import OvalImporter
30-
from vulnerabilities.package_managers import DebianVersionAPI
31-
from vulnerabilities.utils import create_etag
3229

3330

3431
class DebianOvalImporter(OvalImporter):
32+
spdx_license_expression = "LicenseRef-scancode-unknown"
33+
3534
def __init__(self, *args, **kwargs):
3635
super().__init__(*args, **kwargs)
3736
# we could avoid setting translations, and have it
3837
# set by default in the OvalParser, but we don't yet know
3938
# whether all OVAL providers use the same format
4039
self.translations = {"less than": "<"}
41-
self.pkg_manager_api = DebianVersionAPI()
4240

4341
def _fetch(self):
44-
releases = self.config.releases
42+
releases = ["wheezy", "stretch", "jessie", "buster", "bullseye"]
4543
for release in releases:
4644
file_url = f"https://www.debian.org/security/oval/oval-definitions-{release}.xml"
47-
if not create_etag(data_src=self, url=file_url, etag_key="ETag"):
48-
continue
49-
5045
resp = requests.get(file_url).content
5146
yield (
5247
{"type": "deb", "namespace": "debian", "qualifiers": {"distro": release}},
5348
ET.ElementTree(ET.fromstring(resp.decode("utf-8"))),
5449
)
55-
return []
56-
57-
def set_api(self, packages):
58-
asyncio.run(self.pkg_manager_api.load_api(packages))

vulnerabilities/importers/ubuntu.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,32 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

2323

24-
import asyncio
2524
import bz2
2625
import logging
2726
import xml.etree.ElementTree as ET
2827

2928
import requests
3029

3130
from vulnerabilities.importer import OvalImporter
32-
from vulnerabilities.package_managers import LaunchpadVersionAPI
3331

3432
logger = logging.getLogger(__name__)
3533

3634

3735
class UbuntuImporter(OvalImporter):
36+
spdx_license_expression = "GPL"
37+
license_url = "https://ubuntu.com/legal/terms"
38+
3839
def __init__(self, *args, **kwargs):
3940
super().__init__(*args, **kwargs)
4041
# we could avoid setting translations, and have it
4142
# set by default in the OvalParser, but we don't yet know
4243
# whether all OVAL providers use the same format
4344
self.translations = {"less than": "<"}
44-
self.pkg_manager_api = LaunchpadVersionAPI()
4545

4646
def _fetch(self):
4747
base_url = "https://people.canonical.com/~ubuntu-security/oval"
48-
releases = self.config.releases
49-
for i, release in enumerate(releases, 1):
48+
releases = ["bionic", "trusty", "focal", "eoan", "xenial"]
49+
for release in releases:
5050
file_url = f"{base_url}/com.ubuntu.{release}.cve.oval.xml.bz2" # nopep8
5151
logger.info(f"Fetching Ubuntu Oval: {file_url}")
5252
response = requests.get(file_url)
@@ -61,8 +61,3 @@ def _fetch(self):
6161
{"type": "deb", "namespace": "ubuntu"},
6262
ET.ElementTree(ET.fromstring(extracted.decode("utf-8"))),
6363
)
64-
65-
logger.info(f"Fetched {i} Ubuntu Oval releases from {base_url}")
66-
67-
def set_api(self, packages):
68-
asyncio.run(self.pkg_manager_api.load_api(packages))

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323

2424
from vulnerabilities import importers
2525
from vulnerabilities.improvers import default
26+
from vulnerabilities.improvers import oval
2627

2728
IMPROVERS_REGISTRY = [
2829
default.DefaultImprover,
2930
importers.nginx.NginxBasicImprover,
3031
importers.github.GitHubBasicImprover,
3132
importers.debian.DebianBasicImprover,
3233
importers.gitlab.GitLabBasicImprover,
34+
oval.OvalBasicImprover,
3335
]
3436

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

0 commit comments

Comments
 (0)