Skip to content

Commit 8eaa86a

Browse files
authored
Merge pull request #740 from TG1999/migrate/debian_oval
Migrate debian-oval and ubuntu importer
2 parents d96e20f + 6d7905c commit 8eaa86a

18 files changed

+557
-534
lines changed

vulnerabilities/importer.py

Lines changed: 53 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222
from typing import Set
2323
from typing import Tuple
2424

25+
import pytz
26+
from dateutil import parser as dateparser
2527
from fetchcode.vcs import fetch_via_vcs
2628
from license_expression import Licensing
2729
from packageurl import PackageURL
30+
from univers.version_range import RANGE_CLASS_BY_SCHEMES
2831
from univers.version_range import VersionRange
2932
from univers.versions import Version
3033

34+
from vulnerabilities import severity_systems
3135
from vulnerabilities.oval_parser import OvalParser
3236
from vulnerabilities.severity_systems import SCORING_SYSTEMS
3337
from vulnerabilities.severity_systems import ScoringSystem
@@ -350,13 +354,13 @@ class OvalImporter(Importer):
350354
"""
351355

352356
@staticmethod
353-
def create_purl(pkg_name: str, pkg_version: str, pkg_data: Mapping) -> PackageURL:
357+
def create_purl(pkg_name: str, pkg_data: Mapping) -> PackageURL:
354358
"""
355359
Helper method for creating different purls for subclasses without them reimplementing
356360
get_data_from_xml_doc method
357361
Note: pkg_data must include 'type' of package
358362
"""
359-
return PackageURL(name=pkg_name, version=pkg_version, **pkg_data)
363+
return PackageURL(name=pkg_name, **pkg_data)
360364

361365
@staticmethod
362366
def _collect_pkgs(parsed_oval_data: Mapping) -> Set:
@@ -390,28 +394,17 @@ def advisory_data(self) -> List[AdvisoryData]:
390394
for metadata, oval_file in self._fetch():
391395
try:
392396
oval_data = self.get_data_from_xml_doc(oval_file, metadata)
393-
yield oval_data
397+
yield from oval_data
394398
except Exception:
395399
logger.error(
396400
f"Failed to get updated_advisories: {oval_file!r} "
397401
f"with {metadata!r}:\n" + traceback.format_exc()
398402
)
399403
continue
400404

401-
def set_api(self, all_pkgs: Iterable[str]):
402-
"""
403-
This method loads the self.pkg_manager_api with the specified packages.
404-
It fetches and caches all the versions of these packages and exposes
405-
them through self.pkg_manager_api.get(<package_name>). Example
406-
407-
>> self.set_api(['electron'])
408-
Assume 'electron' has only versions 1.0.0 and 1.2.0
409-
>> assert self.pkg_manager_api.get('electron') == {'1.0.0','1.2.0'}
410-
411-
"""
412-
raise NotImplementedError
413-
414-
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[AdvisoryData]:
405+
def get_data_from_xml_doc(
406+
self, xml_doc: ET.ElementTree, pkg_metadata={}
407+
) -> Iterable[AdvisoryData]:
415408
"""
416409
The orchestration method of the OvalDataSource. This method breaks an
417410
OVAL xml ElementTree into a list of `Advisory`.
@@ -422,66 +415,58 @@ def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> Lis
422415
Example value of pkg_metadata:
423416
{"type":"deb","qualifiers":{"distro":"buster"} }
424417
"""
425-
426-
all_adv = []
427-
oval_doc = OvalParser(self.translations, xml_doc)
428-
raw_data = oval_doc.get_data()
429-
all_pkgs = self._collect_pkgs(raw_data)
430-
self.set_api(all_pkgs)
418+
oval_parsed_data = OvalParser(self.translations, xml_doc)
419+
raw_data = oval_parsed_data.get_data()
420+
oval_doc = oval_parsed_data.oval_document
421+
timestamp = oval_doc.getGenerator().getTimestamp()
431422

432423
# convert definition_data to Advisory objects
433424
for definition_data in raw_data:
434425
# These fields are definition level, i.e common for all elements
435426
# connected/linked to an OvalDefinition
436427
vuln_id = definition_data["vuln_id"]
437428
description = definition_data["description"]
438-
references = [Reference(url=url) for url in definition_data["reference_urls"]]
429+
severities = (
430+
[
431+
VulnerabilitySeverity(
432+
system=severity_systems.GENERIC, value=definition_data.get("severity")
433+
)
434+
]
435+
if definition_data.get("severity")
436+
else []
437+
)
438+
references = [
439+
Reference(url=url, severities=severities)
440+
for url in definition_data["reference_urls"]
441+
]
439442
affected_packages = []
440443
for test_data in definition_data["test_data"]:
441444
for package_name in test_data["package_list"]:
442-
if package_name and len(package_name) >= 50:
443-
continue
444-
445-
affected_version_range = test_data["version_ranges"] or set()
446-
version_class = version_class_by_package_type[pkg_metadata["type"]]
447-
version_scheme = version_class.scheme
448-
449-
affected_version_range = VersionRange.from_scheme_version_spec_string(
450-
version_scheme, affected_version_range
451-
)
452-
all_versions = self.pkg_manager_api.get(package_name).valid_versions
453-
454-
# FIXME: what is this 50 DB limit? that's too small for versions
455-
# FIXME: we should not drop data this way
456-
# This filter is for filtering out long versions.
457-
# 50 is limit because that's what db permits atm.
458-
all_versions = [version for version in all_versions if len(version) < 50]
459-
if not all_versions:
460-
continue
461-
462-
affected_purls = []
463-
safe_purls = []
464-
for version in all_versions:
465-
purl = self.create_purl(
466-
pkg_name=package_name,
467-
pkg_version=version,
468-
pkg_data=pkg_metadata,
445+
affected_version_range = test_data["version_ranges"]
446+
vrc = RANGE_CLASS_BY_SCHEMES[pkg_metadata["type"]]
447+
if affected_version_range:
448+
try:
449+
affected_version_range = vrc.from_native(affected_version_range)
450+
except Exception as e:
451+
logger.error(
452+
f"Failed to parse version range {affected_version_range!r} "
453+
f"for package {package_name!r}:\n{e}"
454+
)
455+
continue
456+
if package_name:
457+
affected_packages.append(
458+
AffectedPackage(
459+
package=self.create_purl(package_name, pkg_metadata),
460+
affected_version_range=affected_version_range,
461+
)
469462
)
470-
if version_class(version) in affected_version_range:
471-
affected_purls.append(purl)
472-
else:
473-
safe_purls.append(purl)
474-
475-
affected_packages.extend(
476-
nearest_patched_package(affected_purls, safe_purls),
477-
)
478-
479-
all_adv.append(
480-
AdvisoryData(
481-
summary=description,
482-
affected_packages=affected_packages,
483-
vulnerability_id=vuln_id,
484-
references=references,
485-
)
463+
date_published = dateparser.parse(timestamp)
464+
if not date_published.tzinfo:
465+
date_published = date_published.replace(tzinfo=pytz.UTC)
466+
yield AdvisoryData(
467+
aliases=[vuln_id],
468+
summary=description,
469+
affected_packages=affected_packages,
470+
references=sorted(references),
471+
date_published=date_published,
486472
)
487-
return all_adv

vulnerabilities/importers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from vulnerabilities.importers import alpine_linux
1111
from vulnerabilities.importers import archlinux
1212
from vulnerabilities.importers import debian
13+
from vulnerabilities.importers import debian_oval
1314
from vulnerabilities.importers import github
1415
from vulnerabilities.importers import gitlab
1516
from vulnerabilities.importers import nginx
@@ -18,6 +19,7 @@
1819
from vulnerabilities.importers import pypa
1920
from vulnerabilities.importers import pysec
2021
from vulnerabilities.importers import redhat
22+
from vulnerabilities.importers import ubuntu
2123

2224
IMPORTERS_REGISTRY = [
2325
nginx.NginxImporter,
@@ -31,6 +33,8 @@
3133
gitlab.GitLabAPIImporter,
3234
pypa.PyPaImporter,
3335
archlinux.ArchlinuxImporter,
36+
ubuntu.UbuntuImporter,
37+
debian_oval.DebianOvalImporter,
3438
]
3539

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

vulnerabilities/importers/debian.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
class DebianImporter(Importer):
4141

42-
spdx_license_expression = "MIT"
42+
spdx_license_expression = "LicenseRef-scancode-other-permissive"
4343
license_url = "https://www.debian.org/license"
4444
notice = """
4545
From: Tushar Goel <[email protected]>

vulnerabilities/importers/debian_oval.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,65 @@
88
#
99

1010

11-
import asyncio
1211
import xml.etree.ElementTree as ET
1312

1413
import requests
1514

1615
from vulnerabilities.importer import OvalImporter
17-
from vulnerabilities.package_managers import DebianVersionAPI
18-
from vulnerabilities.utils import create_etag
1916

2017

2118
class DebianOvalImporter(OvalImporter):
19+
20+
spdx_license_expression = "LicenseRef-scancode-other-permissive"
21+
license_url = "https://www.debian.org/license"
22+
notice = """
23+
From: Tushar Goel <[email protected]>
24+
Date: Thu, May 12, 2022 at 11:42 PM +00:00
25+
Subject: Usage of Debian Security Data in VulnerableCode
26+
27+
Hey,
28+
We would like to integrate the debian security data in vulnerablecode
29+
[1][2] which is a FOSS db of FOSS vulnerability data. We were not able
30+
to know under which license the debian security data comes. We would
31+
be grateful to have your acknowledgement over usage of the debian
32+
security data in vulnerablecode and have some kind of licensing
33+
declaration from your side.
34+
[1] - https://github.com/nexB/vulnerablecode
35+
[2] - https://github.com/nexB/vulnerablecode/pull/723
36+
Regards,
37+
From: Moritz Mühlenhoff <[email protected]>
38+
Date: Wed, May 17, 2022, 19:12 PM +00:00
39+
Subject: Re: Usage of Debian Security Data in VulnerableCode
40+
To: Tushar Goel <[email protected]>
41+
42+
Am Thu, May 12, 2022 at 05:12:48PM +0530 schrieb Tushar Goel:
43+
> Hey,
44+
>
45+
> We would like to integrate the debian security data in vulnerablecode
46+
> [1][2] which is a FOSS db of FOSS vulnerability data. We were not able
47+
> to know under which license the debian security data comes. We would
48+
> be grateful to have your acknowledgement over usage of the debian
49+
> security data in vulnerablecode and have some kind of licensing
50+
> declaration from your side.
51+
We don't have a specific license, but you have our endorsemen to
52+
reuse the data by all means :-)
53+
Cheers,
54+
Moritz
55+
"""
56+
2257
def __init__(self, *args, **kwargs):
2358
super().__init__(*args, **kwargs)
2459
# we could avoid setting translations, and have it
2560
# set by default in the OvalParser, but we don't yet know
2661
# whether all OVAL providers use the same format
2762
self.translations = {"less than": "<"}
28-
self.pkg_manager_api = DebianVersionAPI()
2963

3064
def _fetch(self):
31-
releases = self.config.releases
65+
releases = ["wheezy", "stretch", "jessie", "buster", "bullseye"]
3266
for release in releases:
3367
file_url = f"https://www.debian.org/security/oval/oval-definitions-{release}.xml"
34-
if not create_etag(data_src=self, url=file_url, etag_key="ETag"):
35-
continue
36-
3768
resp = requests.get(file_url).content
3869
yield (
3970
{"type": "deb", "namespace": "debian", "qualifiers": {"distro": release}},
4071
ET.ElementTree(ET.fromstring(resp.decode("utf-8"))),
4172
)
42-
return []
43-
44-
def set_api(self, packages):
45-
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
@@ -7,32 +7,32 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10-
import asyncio
1110
import bz2
1211
import logging
1312
import xml.etree.ElementTree as ET
1413

1514
import requests
1615

1716
from vulnerabilities.importer import OvalImporter
18-
from vulnerabilities.package_managers import LaunchpadVersionAPI
1917

2018
logger = logging.getLogger(__name__)
2119

2220

2321
class UbuntuImporter(OvalImporter):
22+
spdx_license_expression = "GPL"
23+
license_url = "https://ubuntu.com/legal/terms"
24+
2425
def __init__(self, *args, **kwargs):
2526
super().__init__(*args, **kwargs)
2627
# we could avoid setting translations, and have it
2728
# set by default in the OvalParser, but we don't yet know
2829
# whether all OVAL providers use the same format
2930
self.translations = {"less than": "<"}
30-
self.pkg_manager_api = LaunchpadVersionAPI()
3131

3232
def _fetch(self):
3333
base_url = "https://people.canonical.com/~ubuntu-security/oval"
34-
releases = self.config.releases
35-
for i, release in enumerate(releases, 1):
34+
releases = ["bionic", "trusty", "focal", "eoan", "xenial"]
35+
for release in releases:
3636
file_url = f"{base_url}/com.ubuntu.{release}.cve.oval.xml.bz2" # nopep8
3737
logger.info(f"Fetching Ubuntu Oval: {file_url}")
3838
response = requests.get(file_url)
@@ -47,8 +47,3 @@ def _fetch(self):
4747
{"type": "deb", "namespace": "ubuntu"},
4848
ET.ElementTree(ET.fromstring(extracted.decode("utf-8"))),
4949
)
50-
51-
logger.info(f"Fetched {i} Ubuntu Oval releases from {base_url}")
52-
53-
def set_api(self, packages):
54-
asyncio.run(self.pkg_manager_api.load_api(packages))

vulnerabilities/improvers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99

1010
from vulnerabilities import importers
1111
from vulnerabilities.improvers import default
12+
from vulnerabilities.improvers import oval
1213

1314
IMPROVERS_REGISTRY = [
1415
default.DefaultImprover,
1516
importers.nginx.NginxBasicImprover,
1617
importers.github.GitHubBasicImprover,
1718
importers.debian.DebianBasicImprover,
1819
importers.gitlab.GitLabBasicImprover,
20+
oval.DebianOvalBasicImprover,
21+
oval.UbuntuOvalBasicImprover,
1922
]
2023

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

0 commit comments

Comments
 (0)