Skip to content

Commit 4eeb3ec

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

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:
@@ -256,7 +259,8 @@ def file_changes(
256259

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

259-
return self._collect_file_changes(subdir=subdir, recursive=recursive, file_ext=file_ext)
262+
return self._collect_file_changes(
263+
subdir=subdir, recursive=recursive, file_ext=file_ext)
260264

261265
def _collect_file_changes(
262266
self,
@@ -268,7 +272,8 @@ def _collect_file_changes(
268272
previous_commit = None
269273
added_files, updated_files = set(), set()
270274

271-
for commit in self._repo.walk(self._repo.head.target, pygit2.GIT_SORT_TIME):
275+
for commit in self._repo.walk(
276+
self._repo.head.target, pygit2.GIT_SORT_TIME):
272277
commit_time = commit.commit_time + commit.commit_time_offset # convert to UTC
273278

274279
if commit_time < self.cutoff_timestamp:
@@ -279,13 +284,16 @@ def _collect_file_changes(
279284
continue
280285

281286
for d in commit.tree.diff_to_tree(previous_commit.tree).deltas:
282-
if not _include_file(d.new_file.path, subdir, recursive, file_ext) or d.is_binary:
287+
if not _include_file(
288+
d.new_file.path, subdir, recursive, file_ext) or d.is_binary:
283289
continue
284290

285-
abspath = os.path.join(self.config.working_directory, d.new_file.path)
291+
abspath = os.path.join(
292+
self.config.working_directory, d.new_file.path)
286293
# TODO
287294
# Just filtering on the two status values for "added" and "modified" is too
288-
# simplistic. This does not cover file renames, copies & deletions.
295+
# simplistic. This does not cover file renames, copies &
296+
# deletions.
289297
if d.status == pygit2.GIT_DELTA_ADDED:
290298
added_files.add(abspath)
291299
elif d.status == pygit2.GIT_DELTA_MODIFIED:
@@ -380,3 +388,103 @@ def _include_file(
380388
match = match and path.endswith(f'.{file_ext}')
381389

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