Skip to content

Commit 36f43d1

Browse files
committed
Harden Ubuntu OVAL importer
This importer no longer worked with failures to process some version ranges as RangeSpecifier(). These change imprivae error handling and crude logging using print statements for now. Also apply minor code formatting. Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent 38d4d1a commit 36f43d1

3 files changed

Lines changed: 142 additions & 76 deletions

File tree

vulnerabilities/data_source.py

Lines changed: 98 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ class DataSource(ContextManager):
106106
def __init__(
107107
self,
108108
batch_size: int,
109-
last_run_date: Optional[datetime] = None,
110-
cutoff_date: Optional[datetime] = None,
111-
config: Optional[Mapping[str, Any]] = None,
109+
last_run_date: Optional[datetime]=None,
110+
cutoff_date: Optional[datetime]=None,
111+
config: Optional[Mapping[str, Any]]=None,
112112
):
113113
"""
114114
Create a DataSource instance.
@@ -240,9 +240,9 @@ def __exit__(self, exc_type, exc_val, exc_tb):
240240

241241
def file_changes(
242242
self,
243-
subdir: str = None,
244-
recursive: bool = False,
245-
file_ext: Optional[str] = None,
243+
subdir: str=None,
244+
recursive: bool=False,
245+
file_ext: Optional[str]=None,
246246
) -> Tuple[Set[str], Set[str]]:
247247
"""
248248
Returns all added and modified files since last_run_date or cutoff_date (whichever is more
@@ -382,9 +382,9 @@ def _update_from_remote(self, remote, branch) -> None:
382382

383383
def _include_file(
384384
path: str,
385-
subdir: Optional[str] = None,
386-
recursive: bool = False,
387-
file_ext: Optional[str] = None,
385+
subdir: Optional[str]=None,
386+
recursive: bool=False,
387+
file_ext: Optional[str]=None,
388388
) -> bool:
389389
match = True
390390

@@ -408,6 +408,7 @@ class OvalDataSource(DataSource):
408408
All data sources which collect data from OVAL files must inherit from this
409409
`OvalDataSource` class. Subclasses must implement the methods `_fetch` and `set_api`.
410410
"""
411+
411412
@staticmethod
412413
def create_purl(pkg_name: str, pkg_version: str, pkg_data: Mapping) -> PackageURL:
413414
"""
@@ -433,29 +434,48 @@ def _collect_pkgs(parsed_oval_data: Mapping) -> Set:
433434

434435
def _fetch(self) -> Tuple[Mapping, Iterable[ET.ElementTree]]:
435436
"""
436-
This method contains logic to fetch OVAL files and yield them into
437-
a tuple of file's metadata and it's ET.ElementTree.
437+
Return a two-tuple of ({mapping of Package URL data}, it's ET.ElementTree)
438438
Subclasses must implement this method.
439439
440-
Note: Mapping MUST INCLUDE "type" key. Example values of Mapping
441-
{"type":"deb","qualifiers":{"distro":"buster"} }
440+
Note: Package URL data MUST INCLUDE a Package URL "type" key so
441+
implement _fetch() accordingly.
442+
For example::
442443
444+
{"type":"deb","qualifiers":{"distro":"buster"} }
443445
"""
446+
# TODO: enforce that we receive the proper data here
444447
raise NotImplementedError
445448

446449
def updated_advisories(self) -> List[Advisory]:
447-
"""
448-
Note: metadata MUST INCLUDE "type" key, implement _fetch accordingly.
449-
"""
450-
for metadata, oval_file in self._fetch():
450+
for purl_data, oval_etree in self._fetch():
451+
if 'type' not in purl_data:
452+
ets = (oval_etree and ET.tostring(oval_etree)) or 'NO DATA'
453+
msg = (
454+
"Failed to get updated_advisories for Ubuntu: purl_data is "
455+
f"missing a package type {purl_data!r}\n"
456+
f"with OVAL XML:\n"
457+
f"{ets}\n"
458+
f"... continuing..."
459+
)
460+
print(msg)
461+
logger.error(msg)
462+
continue
463+
451464
try:
452-
oval_data = self.get_data_from_xml_doc(oval_file, metadata)
465+
oval_data = self.get_data_from_xml_doc(oval_etree, purl_data)
453466
yield oval_data
454467
except Exception:
455-
logger.error(
456-
f"Failed to get updated_advisories: {oval_file!r} "
457-
"with {metadata!r}:\n" + traceback.format_exc()
468+
ets = (oval_etree and ET.tostring(oval_etree)) or 'NO DATA'
469+
tb = traceback.format_exc()
470+
msg = (
471+
f"Failed to get updated_advisories for Ubuntu:"
472+
f"with {purl_data!r}\n"
473+
f"and with OVAL XML:\n"
474+
f"{ets}\n{tb}\n"
475+
f"... continuing..."
458476
)
477+
print(msg)
478+
logger.error(msg)
459479
continue
460480

461481
def set_api(self, all_pkgs: Iterable[str]):
@@ -473,57 +493,89 @@ def set_api(self, all_pkgs: Iterable[str]):
473493

474494
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[Advisory]:
475495
"""
476-
The orchestration method of the OvalDataSource. This method breaks an OVAL xml
477-
ElementTree into a list of `Advisory`.
496+
The orchestration method of the OvalDataSource. This method breaks an
497+
OVAL xml ElementTree into a list of `Advisory`.
478498
479-
Note: pkg_metadata MUST INCLUDE "type" key. Example value of pkg_metadata,
499+
Note: pkg_metadata is a mapping of Package URL data that MUST INCLUDE
500+
"type" key.
501+
502+
Example value of pkg_metadata:
480503
{"type":"deb","qualifiers":{"distro":"buster"} }
481504
"""
505+
if 'type' not in pkg_metadata:
506+
ets = xml_doc and ET.tostring(xml_doc) or 'NO DATA'
507+
msg = (
508+
"Failed to get_data_from_xml_doc: pkg_metadata is "
509+
f"missing a package type {pkg_metadata!r}\n"
510+
f"with OVAL XML:\n"
511+
f"{ets}"
512+
)
513+
print(msg)
514+
logger.error(msg)
515+
raise Exception(msg)
516+
482517
all_adv = []
483518
oval_doc = OvalParser(self.translations, xml_doc)
484-
raw_data = oval_doc.get_data()
519+
try:
520+
raw_data = oval_doc.get_data()
521+
except Exception:
522+
ets = xml_doc and ET.tostring(xml_doc) or 'NO DATA'
523+
tb = traceback.format_exc()
524+
msg = (
525+
f"Failed to get_data_from_xml_doc:"
526+
f"with {pkg_metadata!r}\n"
527+
f"and with OVAL XML:\n"
528+
f"{ets}\n{tb}"
529+
)
530+
print(msg)
531+
logger.error(msg)
532+
raise Exception(msg)
533+
485534
all_pkgs = self._collect_pkgs(raw_data)
486535
self.set_api(all_pkgs)
487-
for definition_data in raw_data: # definition_data -> Advisory
488536

489-
# These fields are definition level, i.e common for all
490-
# elements connected/linked to an OvalDefinition
537+
# convert definition_data to Advisory objects
538+
for definition_data in raw_data:
539+
# These fields are definition level, i.e common for all elements
540+
# connected/linked to an OvalDefinition
491541
vuln_id = definition_data['vuln_id']
492542
description = definition_data['description']
493543
affected_purls = set()
494544
safe_purls = set()
495-
references = [Reference(url=url)
496-
for url in definition_data['reference_urls']]
497-
545+
references = [Reference(url=url) for url in definition_data['reference_urls']]
498546
for test_data in definition_data['test_data']:
499547
for package in test_data['package_list']:
500548
pkg_name = package
501549
if package and len(pkg_name) >= 50:
502550
continue
503-
aff_ver_range = test_data['version_ranges']
551+
aff_ver_range = test_data['version_ranges'] or set()
504552
all_versions = self.pkg_manager_api.get(package)
553+
554+
# FIXME: what is this 50 DB limit? that's too small for versions
555+
# FIXME: we should not drop data this way
505556
# This filter is for filtering out long versions.
506557
# 50 is limit because that's what db permits atm.
507-
all_versions = set(
508-
filter(
509-
lambda x: len(x) < 50,
510-
all_versions))
558+
all_versions = set(filter(lambda x: len(x) < 50, all_versions))
511559
if not all_versions:
512560
continue
513-
affected_versions = set(
514-
filter(
515-
lambda x: x in aff_ver_range,
516-
all_versions))
561+
562+
affected_versions = set(filter(lambda x: x in aff_ver_range, all_versions))
517563
safe_versions = all_versions - affected_versions
518564

519565
for version in affected_versions:
520566
pkg_url = self.create_purl(
521-
pkg_name=pkg_name, pkg_version=version, pkg_data=pkg_metadata)
567+
pkg_name=pkg_name,
568+
pkg_version=version,
569+
pkg_data=pkg_metadata,
570+
)
522571
affected_purls.add(pkg_url)
523572

524573
for version in safe_versions:
525574
pkg_url = self.create_purl(
526-
pkg_name=pkg_name, pkg_version=version, pkg_data=pkg_metadata)
575+
pkg_name=pkg_name,
576+
pkg_version=version,
577+
pkg_data=pkg_metadata,
578+
)
527579
safe_purls.add(pkg_url)
528580

529581
all_adv.append(
@@ -532,5 +584,8 @@ def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> Lis
532584
impacted_package_urls=affected_purls,
533585
resolved_package_urls=safe_purls,
534586
cve_id=vuln_id,
535-
vuln_references=references))
587+
vuln_references=references,
588+
))
589+
590+
print(f"Processed {len(all_adv)} Advisory from Oval data for {pkg_metadata}")
536591
return all_adv

vulnerabilities/importers/ubuntu.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,25 @@ def __init__(self, *args, **kwargs):
5959

6060
def _fetch(self):
6161
releases = self.config.releases
62-
for release in releases:
62+
for i, release in enumerate(releases, 1):
6363
file_url = f"https://people.canonical.com/~ubuntu-security/oval/com.ubuntu.{release}.cve.oval.xml.bz2" # nopep8
64-
if not create_etag(data_src=self, url=file_url, etag_key="ETag"):
64+
# if not create_etag(data_src=self, url=file_url, etag_key="ETag"):
65+
# print(f"Ubuntu Oval Etag not changed, not re-fetching: {file_url}")
66+
# continue
67+
68+
print(f"Fetching Ubuntu Oval: {file_url}")
69+
response = requests.get(file_url)
70+
if response.status_code != requests.codes.ok:
71+
print(f"Failed to fetch Ubuntu Oval: HTTP {response.status_code} : {file_url}")
6572
continue
66-
resp = requests.get(file_url)
67-
extracted = bz2.decompress(resp.content)
73+
74+
extracted = bz2.decompress(response.content)
6875
yield (
6976
{"type": "deb", "namespace": "ubuntu"},
7077
ET.ElementTree(ET.fromstring(extracted.decode("utf-8"))),
7178
)
72-
# In case every file is latest, _fetch won't yield anything(due to checking for new etags),
73-
# this would return None to added_advisories
74-
# which will cause error, hence this
75-
# function return an empty list
76-
return []
79+
80+
print(f"Fetched {i} Ubuntu Oval releases https://people.canonical.com/~ubuntu-security/oval/")
7781

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

vulnerabilities/oval_parser.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030

3131
from dephell_specifier import RangeSpecifier
3232

33-
from vulnerabilities.lib_oval import (
34-
OvalDefinition, OvalDocument, OvalTest, OvalObject, OvalState)
33+
from vulnerabilities.lib_oval import OvalDefinition
34+
from vulnerabilities.lib_oval import OvalDocument
35+
from vulnerabilities.lib_oval import OvalObject
36+
from vulnerabilities.lib_oval import OvalState
37+
from vulnerabilities.lib_oval import OvalTest
38+
import traceback
3539

3640

3741
class OvalParser:
@@ -45,8 +49,7 @@ def __init__(self, translations: Dict, oval_document: ET.ElementTree):
4549

4650
def get_data(self) -> List[Dict]:
4751
"""
48-
This is the orchestration method, it returns a list of dictionaries,
49-
where each dictionary represents data from an OvalDefinition
52+
Return a list of OvalDefinition mappings.
5053
"""
5154
oval_data = []
5255
for definition in self.all_definitions:
@@ -55,27 +58,22 @@ def get_data(self) -> List[Dict]:
5558
if not matching_tests:
5659
continue
5760
definition_data = {'test_data': []}
58-
definition_data['description'] = definition.getMetadata(
59-
).getDescription() # this could use some data cleaning
61+
# TODO:this could use some data cleaning
62+
definition_data['description'] = definition.getMetadata().getDescription() or ''
6063

61-
if not definition_data['description']:
62-
definition_data['description'] = ''
64+
definition_data['vuln_id'] = self.get_vuln_id_from_definition(definition)
65+
definition_data['reference_urls'] = self.get_urls_from_definition(definition)
6366

64-
definition_data['vuln_id'] = self.get_vuln_id_from_definition(
65-
definition)
66-
definition_data['reference_urls'] = self.get_urls_from_definition(
67-
definition
68-
)
6967
for test in matching_tests:
7068
test_obj, test_state = self.get_object_state_of_test(test)
7169
if not test_obj or not test_state:
7270
continue
7371
test_data = {'package_list': []}
74-
test_data['package_list'].extend(
75-
self.get_pkgs_from_obj(test_obj))
76-
test_data['version_ranges'] = self.get_versionsrngs_from_state(
77-
test_state)
72+
test_data['package_list'].extend(self.get_pkgs_from_obj(test_obj))
73+
version_ranges = self.get_versionsrngs_from_state(test_state)
74+
test_data['version_ranges'] = version_ranges
7875
definition_data['test_data'].append(test_data)
76+
7977
oval_data.append(definition_data)
8078

8179
return oval_data
@@ -138,18 +136,27 @@ def get_pkgs_from_obj(self, obj: OvalObject) -> List[str]:
138136

139137
return pkg_list
140138

139+
# TODO: this method needs a better name
141140
def get_versionsrngs_from_state(self, state: OvalState) -> Optional[RangeSpecifier]:
142141
"""
143-
returns all related version ranges within a state
142+
Return a version range(s)? from a state
144143
"""
145144
for var in state.element:
146-
if var.get('operation'):
147-
if var.get('operation') not in self.translations:
148-
continue
149-
operand = self.translations[var.get('operation')]
150-
version = var.text
151-
version_range = operand + version
145+
operation = var.get('operation')
146+
if not operation:
147+
continue
148+
operand = self.translations.get(operation) or ''
149+
if not operand:
150+
continue
151+
version = var.text or ''
152+
if not version:
153+
continue
154+
version_range = operand + version
155+
try:
152156
return RangeSpecifier(version_range)
157+
except Exception:
158+
# FIXME: we should not continue
159+
print(f"Failed to process invalid version_range in OvalState: {version_range}...continuing")
153160

154161
@staticmethod
155162
def get_urls_from_definition(definition: OvalDefinition) -> Set[str]:

0 commit comments

Comments
 (0)