Skip to content

Commit 2c774a3

Browse files
committed
Add OvalDataSource, refactor UbuntuDataSource to inherit from OvalDataSource
Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
1 parent 6206544 commit 2c774a3

7 files changed

Lines changed: 132 additions & 88 deletions

File tree

vulnerabilities/data_source.py

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@
3535
from typing import Sequence
3636
from typing import Set
3737
from typing import Tuple
38+
import xml.etree.ElementTree as ET
3839

3940
import pygit2
4041
from packageurl import PackageURL
4142

43+
from vulnerabilities.oval_parser import OvalParser
44+
4245

4346
@dataclasses.dataclass
4447
class Advisory:
@@ -258,7 +261,8 @@ def file_changes(
258261

259262
return {str(p) for p in path.glob(glob) if p.is_file()}, set()
260263

261-
return self._collect_file_changes(subdir=subdir, recursive=recursive, file_ext=file_ext)
264+
return self._collect_file_changes(
265+
subdir=subdir, recursive=recursive, file_ext=file_ext)
262266

263267
def _collect_file_changes(
264268
self,
@@ -270,7 +274,8 @@ def _collect_file_changes(
270274
previous_commit = None
271275
added_files, updated_files = set(), set()
272276

273-
for commit in self._repo.walk(self._repo.head.target, pygit2.GIT_SORT_TIME):
277+
for commit in self._repo.walk(
278+
self._repo.head.target, pygit2.GIT_SORT_TIME):
274279
commit_time = commit.commit_time + commit.commit_time_offset # convert to UTC
275280

276281
if commit_time < self.cutoff_timestamp:
@@ -281,13 +286,16 @@ def _collect_file_changes(
281286
continue
282287

283288
for d in commit.tree.diff_to_tree(previous_commit.tree).deltas:
284-
if not _include_file(d.new_file.path, subdir, recursive, file_ext) or d.is_binary:
289+
if not _include_file(
290+
d.new_file.path, subdir, recursive, file_ext) or d.is_binary:
285291
continue
286292

287-
abspath = os.path.join(self.config.working_directory, d.new_file.path)
293+
abspath = os.path.join(
294+
self.config.working_directory, d.new_file.path)
288295
# TODO
289296
# Just filtering on the two status values for "added" and "modified" is too
290-
# simplistic. This does not cover file renames, copies & deletions.
297+
# simplistic. This does not cover file renames, copies &
298+
# deletions.
291299
if d.status == pygit2.GIT_DELTA_ADDED:
292300
added_files.add(abspath)
293301
elif d.status == pygit2.GIT_DELTA_MODIFIED:
@@ -382,3 +390,103 @@ def _include_file(
382390
match = match and path.endswith(f'.{file_ext}')
383391

384392
return match
393+
394+
395+
class OvalDataSource(DataSource):
396+
397+
@staticmethod
398+
def create_purl(pkg_name: str, pkg_version: str, pkg_data: Mapping):
399+
"""
400+
Note: pkg_data must include 'type' of package
401+
"""
402+
return PackageURL(name=pkg_name, version=pkg_version, **pkg_data)
403+
404+
@staticmethod
405+
def _collect_pkgs(parsed_oval_data: Mapping) -> Set:
406+
"""
407+
Helper method, used for loading the API. It expects data from
408+
OvalParser.get_data() .
409+
"""
410+
all_pkgs = set()
411+
for definition_data in parsed_oval_data:
412+
for test_data in definition_data['test_data']:
413+
for package in test_data['package_list']:
414+
all_pkgs.add(package)
415+
416+
return all_pkgs
417+
418+
def _fetch() -> Tuple[Mapping, Iterable[ET.ElementTree]]:
419+
"""
420+
This method contains logic to fetch OVAL files and yield them into
421+
a tuple of file's metadata and it's ET.ElementTree.
422+
Subclasses must implement this method.
423+
"""
424+
raise NotImplementedError
425+
426+
def added_advisories(self) -> List[Advisory]:
427+
advisories = []
428+
for metadata, oval_file in self._fetch():
429+
advisories.extend(self.get_data_from_xml_doc(oval_file, metadata))
430+
return advisories
431+
432+
def set_api(self, all_pkgs: Iterable[str]):
433+
"""
434+
This method loads the self.pkg_manager_api with the specified packages. It fetches
435+
and caches the data about these packages exposes them through
436+
self.pkg_manager_api.get(<package_name>)
437+
"""
438+
raise NotImplementedError
439+
440+
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[Advisory]:
441+
"""
442+
The orchestration method of the OvalDataSource. Breaks an OVAL xml
443+
ElementTree into a list of Advisory.
444+
"""
445+
all_adv = []
446+
oval_doc = OvalParser(self.translations, xml_doc)
447+
raw_data = oval_doc.get_data()
448+
all_pkgs = self._collect_pkgs(raw_data)
449+
self.set_api(all_pkgs)
450+
for definition_data in raw_data: # definition_data -> Advisory
451+
vuln_id = definition_data['vuln_id']
452+
description = definition_data['description']
453+
affected_purls = set()
454+
safe_purls = set()
455+
urls = definition_data['reference_urls']
456+
for test_data in definition_data['test_data']:
457+
for package in test_data['package_list']:
458+
pkg_name = package
459+
aff_ver_range = test_data['version_ranges']
460+
all_versions = self.pkg_manager_api.get(package)
461+
# This filter is to filter out long versions.
462+
# 50 is limit because that's what db permits atm
463+
all_versions = set(
464+
filter(
465+
lambda x: len(x) < 50,
466+
all_versions))
467+
if not all_versions:
468+
continue
469+
affected_versions = set(
470+
filter(
471+
lambda x: x in aff_ver_range,
472+
all_versions))
473+
safe_versions = all_versions - affected_versions
474+
475+
for version in affected_versions:
476+
pkg_url = self.create_purl(
477+
pkg_name=pkg_name, pkg_version=version, pkg_data=pkg_metadata)
478+
affected_purls.add(pkg_url)
479+
480+
for version in safe_versions:
481+
pkg_url = self.create_purl(
482+
pkg_name=pkg_name, pkg_version=version, pkg_data=pkg_metadata)
483+
safe_purls.add(pkg_url)
484+
485+
all_adv.append(
486+
Advisory(
487+
summary=description,
488+
impacted_package_urls=affected_purls,
489+
resolved_package_urls=safe_purls,
490+
cve_id=vuln_id,
491+
reference_urls=urls))
492+
return all_adv

vulnerabilities/importers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

23+
2324
from vulnerabilities.importers.alpine_linux import AlpineDataSource
2425
from vulnerabilities.importers.archlinux import ArchlinuxDataSource
2526
from vulnerabilities.importers.debian import DebianDataSource

vulnerabilities/importers/ubuntu.py

Lines changed: 14 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,21 @@
3131
import xml.etree.ElementTree as ET
3232

3333

34-
from aiohttp import ClientSession
34+
from aiohttp import ClientSession, ClientTimeout
3535
from aiohttp.client_exceptions import ClientResponseError
3636
import requests
3737
from packageurl import PackageURL
3838

3939

40-
from vulnerabilities.data_source import DataSource, DataSourceConfiguration, Advisory
41-
from vulnerabilities.importers import oval_parser
40+
from vulnerabilities.data_source import OvalDataSource, DataSourceConfiguration, Advisory
4241

4342

4443
@dataclasses.dataclass
4544
class UbuntuConfiguration(DataSourceConfiguration):
4645
releases: list
4746

4847

49-
class UbuntuDataSource(DataSource):
48+
class UbuntuDataSource(OvalDataSource):
5049

5150
CONFIG_CLASS = UbuntuConfiguration
5251

@@ -56,86 +55,21 @@ def __init__(self, *args, **kwargs):
5655
# set by default in the OvalParser, but we don't yet know
5756
# whether all OVAL providers use the same format
5857
self.translations = {'less than': '<'}
59-
self._versions = VersionAPI()
58+
self.pkg_manager_api = VersionAPI()
6059

6160
def _fetch(self):
6261
base_url = 'https://people.canonical.com/~ubuntu-security/oval/'
6362
file_name = 'com.ubuntu.{}.cve.oval.xml.bz2'
6463
releases = self.config.releases
6564
for release in releases:
65+
print("getting ", release)
6666
resp = requests.get(base_url + file_name.format(release))
6767
extracted = bz2.decompress(resp.content)
68-
yield ET.ElementTree(ET.fromstring(extracted.decode('utf-8')))
69-
70-
def added_advisories(self) -> List[Advisory]:
71-
advisories = []
72-
for oval_file in self._fetch():
73-
advisories.extend(self.get_data_from_xml_doc(oval_file))
74-
return advisories
75-
76-
@staticmethod
77-
def _collect_pkgs(parsed_oval_data) -> Set:
78-
all_pkgs = set()
79-
for definition_data in parsed_oval_data:
80-
for test_data in definition_data['test_data']:
81-
for package in test_data['package_list']:
82-
all_pkgs.add(package)
83-
84-
return all_pkgs
85-
86-
def get_data_from_xml_doc(self, xml_doc) -> List[Advisory]:
87-
all_adv = []
88-
oval_doc = oval_parser.OvalParser(self.translations, xml_doc)
89-
raw_data = oval_doc.get_data()
90-
all_pkgs = self._collect_pkgs(raw_data)
91-
92-
asyncio.run(self._versions.load_api(all_pkgs))
93-
94-
for definition_data in raw_data: # definition_data -> Advisory
95-
vuln_id = definition_data['vuln_id']
96-
description = definition_data['description']
97-
affected_purls = set()
98-
safe_purls = set()
99-
urls = definition_data['reference_urls']
100-
for test_data in definition_data['test_data']:
101-
for package in test_data['package_list']:
102-
pkg_name = package
103-
aff_ver_range = test_data['version_ranges']
104-
all_versions = self._versions.get(package)
105-
# This filter is to filter out long versions.
106-
# 50 is limit because that's what db permits atm
107-
all_versions = set(
108-
filter(
109-
lambda x: len(x) < 50,
110-
all_versions))
111-
if not all_versions:
112-
continue
113-
affected_versions = set(
114-
filter(
115-
lambda x: x in aff_ver_range,
116-
all_versions))
117-
safe_versions = all_versions - affected_versions
118-
119-
for version in affected_versions:
120-
# should we add a qualifier like 'distro:ubuntu'?
121-
pkg_url = PackageURL(
122-
name=pkg_name, type='deb', version=version)
123-
affected_purls.add(pkg_url)
124-
125-
for version in safe_versions:
126-
# should we add a qualifier like 'distro:ubuntu'?
127-
pkg_url = PackageURL(
128-
name=pkg_name, type='deb', version=version)
129-
safe_purls.add(pkg_url)
130-
131-
all_adv.append(
132-
Advisory(
133-
summary=description,
134-
impacted_package_urls=affected_purls,
135-
resolved_package_urls=safe_purls,
136-
cve_id=vuln_id,
137-
reference_urls=urls))
138-
return all_adv
68+
print("done ")
69+
yield ({'type': 'deb'}, ET.ElementTree(ET.fromstring(extracted.decode('utf-8'))))
70+
71+
def set_api(self, packages):
72+
asyncio.run(self.pkg_manager_api.load_api(packages))
13973

14074

14175
class VersionAPI:
@@ -146,7 +80,9 @@ def get(self, package_name: str) -> Set[str]:
14680
return self.cache[package_name]
14781

14882
async def load_api(self, pkg_set):
149-
async with ClientSession() as session:
83+
# This is debatable
84+
timeout = ClientTimeout(total=None)
85+
async with ClientSession(raise_for_status=True, timeout=timeout) as session:
15086
await asyncio.gather(*[self.set_api(pkg, session)
15187
for pkg in pkg_set if pkg not in self.cache])
15288

@@ -156,9 +92,8 @@ async def set_api(self, pkg, session):
15692
'source_name={}&exact_match=true'.format(pkg))
15793
try:
15894
all_versions = set()
159-
while(True):
95+
while True:
16096
response = await session.request(method='GET', url=url)
161-
response.raise_for_status()
16297
resp_json = await response.json()
16398
if resp_json['entries'] == []:
16499
self.cache[pkg] = {}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
from dephell_specifier import RangeSpecifier
3232

33-
from vulnerabilities.importers.lib_oval import (
33+
from vulnerabilities.lib_oval import (
3434
OvalDefinition, OvalDocument, OvalTest, OvalObject, OvalState)
3535

3636

vulnerabilities/tests/test_suse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dephell_specifier import RangeSpecifier
66

77

8-
from vulnerabilities.importers.oval_parser import OvalParser
8+
from vulnerabilities.oval_parser import OvalParser
99

1010

1111
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

vulnerabilities/tests/test_ubuntu.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from dephell_specifier import RangeSpecifier
99
from packageurl import PackageURL
1010

11-
from vulnerabilities.importers.oval_parser import OvalParser
11+
from vulnerabilities.oval_parser import OvalParser
1212
from vulnerabilities.importers.ubuntu import UbuntuDataSource
1313
from vulnerabilities.data_source import Advisory
1414

@@ -265,6 +265,6 @@ def test_get_data_from_xml_doc(self, mock_write):
265265
cve_id='CVE-2016-8703')}
266266

267267
xml_doc = ET.parse(os.path.join(TEST_DATA, "ubuntu_oval_data.xml"))
268-
data = set(ubuntu_data_src.get_data_from_xml_doc(xml_doc))
268+
data = set(ubuntu_data_src.get_data_from_xml_doc(xml_doc,{"type":"deb"}))
269269

270270
assert expected_data == data

0 commit comments

Comments
 (0)