Skip to content

Commit cf38c6e

Browse files
pombredannesbs2001
authored andcommitted
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 6b6360d commit cf38c6e

4 files changed

Lines changed: 169 additions & 103 deletions

File tree

vulnerabilities/data_source.py

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -446,29 +446,48 @@ def _collect_pkgs(parsed_oval_data: Mapping) -> Set:
446446

447447
def _fetch(self) -> Tuple[Mapping, Iterable[ET.ElementTree]]:
448448
"""
449-
This method contains logic to fetch OVAL files and yield them into
450-
a tuple of file's metadata and it's ET.ElementTree.
449+
Return a two-tuple of ({mapping of Package URL data}, it's ET.ElementTree)
451450
Subclasses must implement this method.
452451
453-
Note: Mapping MUST INCLUDE "type" key. Example values of Mapping
454-
{"type":"deb","qualifiers":{"distro":"buster"} }
452+
Note: Package URL data MUST INCLUDE a Package URL "type" key so
453+
implement _fetch() accordingly.
454+
For example::
455455
456+
{"type":"deb","qualifiers":{"distro":"buster"} }
456457
"""
458+
# TODO: enforce that we receive the proper data here
457459
raise NotImplementedError
458460

459461
def updated_advisories(self) -> List[Advisory]:
460-
"""
461-
Note: metadata MUST INCLUDE "type" key, implement _fetch accordingly.
462-
"""
463-
for metadata, oval_file in self._fetch():
462+
for purl_data, oval_etree in self._fetch():
463+
if 'type' not in purl_data:
464+
ets = (oval_etree and ET.tostring(oval_etree)) or 'NO DATA'
465+
msg = (
466+
"Failed to get updated_advisories for Ubuntu: purl_data is "
467+
f"missing a package type {purl_data!r}\n"
468+
f"with OVAL XML:\n"
469+
f"{ets}\n"
470+
f"... continuing..."
471+
)
472+
print(msg)
473+
logger.error(msg)
474+
continue
475+
464476
try:
465-
oval_data = self.get_data_from_xml_doc(oval_file, metadata)
477+
oval_data = self.get_data_from_xml_doc(oval_etree, purl_data)
466478
yield oval_data
467479
except Exception:
468-
logger.error(
469-
f"Failed to get updated_advisories: {oval_file!r} "
470-
"with {metadata!r}:\n" + traceback.format_exc()
480+
ets = (oval_etree and ET.tostring(oval_etree)) or 'NO DATA'
481+
tb = traceback.format_exc()
482+
msg = (
483+
f"Failed to get updated_advisories for Ubuntu:"
484+
f"with {purl_data!r}\n"
485+
f"and with OVAL XML:\n"
486+
f"{ets}\n{tb}\n"
487+
f"... continuing..."
471488
)
489+
print(msg)
490+
logger.error(msg)
472491
continue
473492

474493
def set_api(self, all_pkgs: Iterable[str]):
@@ -486,34 +505,66 @@ def set_api(self, all_pkgs: Iterable[str]):
486505

487506
def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> List[Advisory]:
488507
"""
489-
The orchestration method of the OvalDataSource. This method breaks an OVAL xml
490-
ElementTree into a list of `Advisory`.
508+
The orchestration method of the OvalDataSource. This method breaks an
509+
OVAL xml ElementTree into a list of `Advisory`.
491510
492-
Note: pkg_metadata MUST INCLUDE "type" key. Example value of pkg_metadata,
511+
Note: pkg_metadata is a mapping of Package URL data that MUST INCLUDE
512+
"type" key.
513+
514+
Example value of pkg_metadata:
493515
{"type":"deb","qualifiers":{"distro":"buster"} }
494516
"""
517+
if 'type' not in pkg_metadata:
518+
ets = xml_doc and ET.tostring(xml_doc) or 'NO DATA'
519+
msg = (
520+
"Failed to get_data_from_xml_doc: pkg_metadata is "
521+
f"missing a package type {pkg_metadata!r}\n"
522+
f"with OVAL XML:\n"
523+
f"{ets}"
524+
)
525+
print(msg)
526+
logger.error(msg)
527+
raise Exception(msg)
528+
495529
all_adv = []
496530
oval_doc = OvalParser(self.translations, xml_doc)
497-
raw_data = oval_doc.get_data()
531+
try:
532+
raw_data = oval_doc.get_data()
533+
except Exception:
534+
ets = xml_doc and ET.tostring(xml_doc) or 'NO DATA'
535+
tb = traceback.format_exc()
536+
msg = (
537+
f"Failed to get_data_from_xml_doc:"
538+
f"with {pkg_metadata!r}\n"
539+
f"and with OVAL XML:\n"
540+
f"{ets}\n{tb}"
541+
)
542+
print(msg)
543+
logger.error(msg)
544+
raise Exception(msg)
545+
498546
all_pkgs = self._collect_pkgs(raw_data)
499547
self.set_api(all_pkgs)
500-
for definition_data in raw_data: # definition_data -> Advisory
501548

502-
# These fields are definition level, i.e common for all
503-
# elements connected/linked to an OvalDefinition
504-
vuln_id = definition_data["vuln_id"]
505-
description = definition_data["description"]
549+
# convert definition_data to Advisory objects
550+
for definition_data in raw_data:
551+
# These fields are definition level, i.e common for all elements
552+
# connected/linked to an OvalDefinition
553+
vuln_id = definition_data['vuln_id']
554+
description = definition_data['description']
506555
affected_purls = set()
507556
safe_purls = set()
508-
references = [Reference(url=url) for url in definition_data["reference_urls"]]
509-
510-
for test_data in definition_data["test_data"]:
511-
for package in test_data["package_list"]:
557+
references = [Reference(url=url) for url in definition_data['reference_urls']]
558+
for test_data in definition_data['test_data']:
559+
for package in test_data['package_list']:
512560
pkg_name = package
513561
if package and len(pkg_name) >= 50:
514562
continue
515-
aff_ver_range = test_data["version_ranges"]
563+
aff_ver_range = test_data['version_ranges'] or set()
516564
all_versions = self.pkg_manager_api.get(package)
565+
566+
# FIXME: what is this 50 DB limit? that's too small for versions
567+
# FIXME: we should not drop data this way
517568
# This filter is for filtering out long versions.
518569
# 50 is limit because that's what db permits atm.
519570
all_versions = set(filter(lambda x: len(x) < 50, all_versions))
@@ -524,13 +575,17 @@ def get_data_from_xml_doc(self, xml_doc: ET.ElementTree, pkg_metadata={}) -> Lis
524575

525576
for version in affected_versions:
526577
pkg_url = self.create_purl(
527-
pkg_name=pkg_name, pkg_version=version, pkg_data=pkg_metadata
578+
pkg_name=pkg_name,
579+
pkg_version=version,
580+
pkg_data=pkg_metadata,
528581
)
529582
affected_purls.add(pkg_url)
530583

531584
for version in safe_versions:
532585
pkg_url = self.create_purl(
533-
pkg_name=pkg_name, pkg_version=version, pkg_data=pkg_metadata
586+
pkg_name=pkg_name,
587+
pkg_version=version,
588+
pkg_data=pkg_metadata,
534589
)
535590
safe_purls.add(pkg_url)
536591

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/") # nopep8
7781

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

vulnerabilities/lib_oval.py

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,71 +27,71 @@
2727
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
2828
# POSSIBILITY OF SUCH DAMAGE.
2929

30-
"""Library to simplify working with the OVAL XML structure
30+
# Library to simplify working with the OVAL XML structure
3131

3232

33-
Authors: Gunnar Engelbach <Gunnar.Engelbach@ThreatGuard.com>
33+
# Authors: Gunnar Engelbach <Gunnar.Engelbach@ThreatGuard.com>
3434

3535

3636

37-
Available classes:
38-
- OvalDocument: operations at the OVAL document level, such as reading in an existing OVAL document from
39-
file, creating a new one, finding or adding OVAL elements
40-
- OvalElement: the base class for OVAL elements. Implements a few common methods inherited by the
41-
subclasses for definition, test, state, object, and variable
42-
- OvalDefinition: a type of OVAL element with certain attributes available. Additional classes used by the OvalDefinition class:
43-
- OvalMetadata: the metadata associated with a definition, which includes the definition title and description. Metadata also contains:
44-
- OvalAffected: The family and platforms affected by this definition
45-
- OvalRepositoryInformation: Additional information added by the OVAL repository
46-
- OvalTest: for working with OVAL test elements
47-
- OvalObject: for working with OVAL object elements
48-
- OvalState: for working with OVAL state elements
49-
- OvalVariable: for working with OVAL variable elements
37+
# Available classes:
38+
# - OvalDocument: operations at the OVAL document level, such as reading in an existing OVAL document from
39+
# file, creating a new one, finding or adding OVAL elements
40+
# - OvalElement: the base class for OVAL elements. Implements a few common methods inherited by the
41+
# subclasses for definition, test, state, object, and variable
42+
# - OvalDefinition: a type of OVAL element with certain attributes available. Additional classes used by the OvalDefinition class:
43+
# - OvalMetadata: the metadata associated with a definition, which includes the definition title and description. Metadata also contains:
44+
# - OvalAffected: The family and platforms affected by this definition
45+
# - OvalRepositoryInformation: Additional information added by the OVAL repository
46+
# - OvalTest: for working with OVAL test elements
47+
# - OvalObject: for working with OVAL object elements
48+
# - OvalState: for working with OVAL state elements
49+
# - OvalVariable: for working with OVAL variable elements
5050

5151

5252

53-
Available exceptions:
54-
- None at this time
53+
# Available exceptions:
54+
# - None at this time
5555

5656

57-
:Usage:
57+
# :Usage:
5858

59-
1. Create an OvalDocument:
59+
# 1. Create an OvalDocument:
6060

61-
>>> tree = ElementTree()
62-
>>> tree.parse("OvalTest.xml")
63-
>>> document = OvalDocument(tree)
61+
# >>> tree = ElementTree()
62+
# >>> tree.parse("OvalTest.xml")
63+
# >>> document = OvalDocument(tree)
6464

65-
2. Find an oval element within the loaded document:
65+
# 2. Find an oval element within the loaded document:
6666

67-
>>> element = document.getElementByID("oval:org.mitre.oval:def:22382")
68-
>>> if element is not None:
69-
>>> ....
67+
# >>> element = document.getElementByID("oval:org.mitre.oval:def:22382")
68+
# >>> if element is not None:
69+
# >>> ....
7070

71-
3. Read an XML file with a single OVAL Definition (error checking omitted for brevity):
71+
# 3. Read an XML file with a single OVAL Definition (error checking omitted for brevity):
7272

73-
>>> tree = ElementTree()
74-
>>> tree.parse('test-definition.xml')
75-
>>> root = tree.getroot()
76-
>>> definition = lib_oval.OvalDefinition(root)
73+
# >>> tree = ElementTree()
74+
# >>> tree.parse('test-definition.xml')
75+
# >>> root = tree.getroot()
76+
# >>> definition = lib_oval.OvalDefinition(root)
7777

78-
4. Change information in the definition from #3 and write the changes
78+
# 4. Change information in the definition from #3 and write the changes
7979

80-
>>> meta = definition.getMetadata()
81-
>>> repo = meta.getOvalRepositoryInformation()
82-
>>> repo.setMinimumSchemaVersion("5.9")
83-
>>> tree.write("outfilename.xml", UTF-8", True)
80+
# >>> meta = definition.getMetadata()
81+
# >>> repo = meta.getOvalRepositoryInformation()
82+
# >>> repo.setMinimumSchemaVersion("5.9")
83+
# >>> tree.write("outfilename.xml", UTF-8", True)
8484

8585

8686

8787

8888

89-
TODO:
90-
- Add exceptions that give more detail about why a value of None is sometimes returned
91-
- Expand use of find() to allow for the possibility that the XML document is not using namespaces
92-
- Lots of pydoc to be added
93-
- Redo getter/setter for OvalRepository status elements.
94-
"""
89+
# TODO:
90+
# - Add exceptions that give more detail about why a value of None is sometimes returned
91+
# - Expand use of find() to allow for the possibility that the XML document is not using namespaces
92+
# - Lots of pydoc to be added
93+
# - Redo getter/setter for OvalRepository status elements.
94+
9595

9696
import os, xml.etree
9797
from xml.etree import ElementTree

0 commit comments

Comments
 (0)