Merge ~litios/ubuntu-cve-tracker:oval-for-pkgs into ubuntu-cve-tracker:master

Proposed by David Fernandez Gonzalez
Status: Merged
Merged at revision: b866b1c07fa73084a209f128cf8add7d4538ee35
Proposed branch: ~litios/ubuntu-cve-tracker:oval-for-pkgs
Merge into: ubuntu-cve-tracker:master
Diff against target: 1580 lines (+1126/-145)
3 files modified
scripts/generate-oval (+75/-131)
scripts/oval_lib.py (+1046/-14)
scripts/source_map.py (+5/-0)
Reviewer Review Type Date Requested Status
Eduardo Barretto Approve
Review via email: mp+440235@code.launchpad.net

Description of the change

A little bit of context:

* I created several metadata classes (Package, CVE, CVEPkgRelEntry) to hold the information and make it easy to generate multiple formats. Everything gets loaded and all of them are linked among themselves. This way, generating one format or the other will be a matter of what is used as the definition. Right now, this is not used for CVE or USN OVAL, that is working as always.

* OvalGenerator is the abstract class. Specific generators will inherit from here. Some functions have already been created there and I think more can be extracted from the Pkg generator (the ones related to generating the OVAL XML elements, it should work the same regardless of the OVAL type). Right now, this is not used for CVE or USN OVAL, that is working as always.

* xml.etree.cElementTree has been used as the XML library. It allows attributes, which is needed for OVAL and it's one of the main XML Python libraries. Easy to use and it seems to work fine.

* All logic is on the oval_lib file. The idea is for generate_oval to create the OVAL generator with the desired options and ask him to generate the OVAL file.

To post a comment you must log in.
Revision history for this message
Eduardo Barretto (ebarretto) wrote :

Thanks for returning the PackageCache to generate-oval!
For others to understand, we discussed that we don't want this in oval_lib as we are expecting to at one point not have this cache generation inside OVAL generation, and for it to be its own script running separate, much like we do not generate USN database or LSN database during OVAL generation, but instead we just fetch the latest databases.

Known missing things in this PR:
1. In the <advisory> we don't have any reference to CVEs/USNs, this will come later in a next PR where we will add a <cve> field with CVSS score, and that will be add for all OVAL types.

2. For now we are using source_map to get package description, at one point we want to have this inside the package cache file.

Overall it looks good to me, I haven't had the time to test the code locally, but I did review the Package based OVAL you generated and we are going in the write direction. I believe we should add some tests in $UCT/test/, as we currently only have tests for USN OVAL. We should also do it for CVE OVAL.

I did notice a small difference on indentation when comparing your sample data and our CVE and USN data, we should work on getting all in the same style, so we can provide a more consistent diff. We can discuss more about it in another PR.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/scripts/generate-oval b/scripts/generate-oval
2index d5d4c11..f703733 100755
3--- a/scripts/generate-oval
4+++ b/scripts/generate-oval
5@@ -26,20 +26,20 @@
6 from __future__ import print_function, unicode_literals
7
8 import argparse
9-import functools
10 import glob
11 import json
12 import os
13 import re
14 import sys
15-import tempfile
16 #from launchpadlib.launchpad import Launchpad
17
18 import apt_pkg
19 from cve_lib import (kernel_srcs, product_series, load_cve, PRODUCT_UBUNTU, all_releases, eol_releases, devel_release, release_parent, release_name, release_ppa, release_progenitor, needs_oval)
20 from kernel_lib import meta_kernels
21 import oval_lib
22+import functools
23 import lpl_common
24+import tempfile
25
26 # cope with apt_pkg api changes.
27 if 'init_system' in dir(apt_pkg):
28@@ -61,6 +61,9 @@ packages_to_ignore = ("-dev", "-doc", "-dbg", "-dbgsym", "-udeb", "-locale-")
29
30 debug_level = 0
31
32+package_cache = None
33+
34+cve_cache = {}
35
36 def main():
37 """ parse command line options and iterate through files to be processed
38@@ -95,12 +98,14 @@ def main():
39 help="report debugging information")
40 parser.add_argument('--usn-oval', action='store_true',
41 help='generates oval from the USN database')
42+ parser.add_argument('--pkg-oval', action='store_true',
43+ help='generates oval from the Package database')
44 parser.add_argument('--usn-db-dir', default='./', type=str,
45 help='location of USN database.json to process '
46 '(default is ./)')
47 parser.add_argument('--usn-number', default=None, type=str,
48 help='if passed specifics a USN for the oval_usn generator')
49- parser.add_argument('--usn-oval-release', default=None, type=str,
50+ parser.add_argument('--oval-release', default=None, type=str,
51 help='specifies a release to generate the oval usn')
52 parser.add_argument('--packages', nargs='+', action='store', default=None,
53 help='generates oval for specific packages. Only for '
54@@ -134,139 +139,33 @@ def main():
55
56 if args.usn_oval:
57 if args.oci:
58- generate_oval_usn(args.output_dir, args.usn_number, args.usn_oval_release,
59+ generate_oval_usn(args.output_dir, args.usn_number, args.oval_release,
60 args.cve_prefix_dir, args.usn_db_dir, ociprefix, ocioutdir)
61 else:
62- generate_oval_usn(args.output_dir, args.usn_number, args.usn_oval_release,
63+ generate_oval_usn(args.output_dir, args.usn_number, args.oval_release,
64 args.cve_prefix_dir, args.usn_db_dir)
65
66 return
67
68- ovals = dict()
69- for i in supported_releases:
70- # we can have nested parent releases
71- parent = release_progenitor(i)
72- index = '{0}_dpkg'.format(i)
73- ovals[index] = oval_lib.OvalGenerator(i, release_name(i), parent, warn, outdir, prefix='', oval_format='dpkg')
74- ovals[index].add_release_applicability_definition()
75- if args.oci:
76- index = '{0}_oci'.format(i)
77- ovals[index] = oval_lib.OvalGenerator(i, release_name(i), parent, warn, ocioutdir, prefix=ociprefix, oval_format='oci')
78- ovals[index].add_release_applicability_definition()
79-
80- # set up cachefile
81+ cve_cache = {}
82 cache = PackageCache(args.pkg_cache, args.force_cache_reload)
83
84- # loop through all CVE data files
85- files = []
86- for pathname in pathnames:
87- files = files + glob.glob(os.path.join(args.cve_prefix_dir, pathname))
88- files.sort()
89-
90- pkg_filter = None
91- if args.packages:
92- pkg_filter = args.packages
93-
94- files_count = len(files)
95- for i_file, filepath in enumerate(files):
96- cve_data = parse_cve_file(filepath, cache, pkg_filter)
97-
98- # skip CVEs without packages for supported releases
99- if not cve_data['packages']:
100- if not args.no_progress:
101- progress_bar(i_file + 1, files_count)
102- continue
103-
104- for i in ovals:
105- ovals[i].generate_cve_definition(cve_data)
106-
107- if not args.no_progress:
108- progress_bar(i_file + 1, files_count)
109-
110- for i in ovals:
111- ovals[i].write_to_file()
112-
113- cache.write_cache()
114-
115+ if args.pkg_oval:
116+ releases = [args.oval_release] if args.oval_release else supported_releases
117+ for release in releases:
118+ if args.oci:
119+ generate_oval_package(release, outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames, ociprefix, ocioutdir)
120+ else:
121+ generate_oval_package(release, outdir, args.cve_prefix_dir, cache, cve_cache, args.oci, args.no_progress, args.packages, pathnames)
122+ return
123
124-def parse_package_status(release, package, status_text, filepath, cache):
125- """ parse ubuntu package status string format:
126- <status code> (<version/notes>)
127- outputs dictionary: {
128- 'status' : '<not-applicable | unknown | vulnerable | fixed>',
129- 'note' : '<description of the status>',
130- 'fix-version' : '<version with issue fixed, if applicable>',
131- 'bin-pkgs' : []
132- } """
133-
134- # break out status code and detail
135- status_sections = status_text.strip().split(' ', 1)
136- code = status_sections[0].strip().lower()
137- detail = status_sections[1].strip('()') if len(status_sections) > 1 else None
138-
139- status = {}
140- note_end = " (note: '{0}').".format(detail) if detail else '.'
141- if code != 'dne':
142- if detail and detail[0].isdigit() and code in ['released', 'not-affected']:
143- status['bin-pkgs'] = cache.get_binarypkgs(package, release, version=detail)
144- else:
145- status['bin-pkgs'] = cache.get_binarypkgs(package, release)
146-
147- if code == 'dne':
148- status['status'] = 'not-applicable'
149- status['note'] = \
150- " package does not exist in {0}{1}".format(release, note_end)
151- elif code == 'ignored':
152- status['status'] = 'vulnerable'
153- status['note'] = ": while related to the CVE in some way, a decision has been made to ignore this issue{0}".format(note_end)
154- elif code == 'not-affected':
155- # check if there is a release version and if so, test for
156- # package existence with that version
157- if detail and detail[0].isdigit():
158- status['status'] = 'fixed'
159- status['note'] = " package in {0}, is related to the CVE in some way and has been fixed{1}".format(release, note_end)
160- status['fix-version'] = detail
161- else:
162- status['status'] = 'not-vulnerable'
163- status['note'] = " package in {0}, while related to the CVE in some way, is not affected{1}".format(release, note_end)
164- elif code == 'needed':
165- status['status'] = 'vulnerable'
166- status['note'] = \
167- " package in {0} is affected and needs fixing{1}".format(release, note_end)
168- elif code == 'pending':
169- # pending means that packages have been prepared and are in
170- # -proposed or in a ppa somewhere, and should have a version
171- # attached. If there is a version, test for package existence
172- # with that version, otherwise mark as vulnerable
173- if detail and detail[0].isdigit():
174- status['status'] = 'fixed'
175- status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(release, note_end)
176- status['fix-version'] = detail
177- else:
178- status['status'] = 'vulnerable'
179- status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(release, note_end)
180- elif code == 'deferred':
181- status['status'] = 'vulnerable'
182- status['note'] = " package in {0} is affected, but a decision has been made to defer addressing it{1}".format(release, note_end)
183- elif code in ['released']:
184- # if there isn't a release version, then just mark
185- # as vulnerable to test for package existence
186- if not detail:
187- status['status'] = 'vulnerable'
188- status['note'] = " package in {0} was vulnerable and has been fixed, but no release version available for it{1}".format(release, note_end)
189- else:
190- status['status'] = 'fixed'
191- status['note'] = " package in {0} was vulnerable but has been fixed{1}".format(release, note_end)
192- status['fix-version'] = detail
193- elif code == 'needs-triage':
194- status['status'] = 'vulnerable'
195- status['note'] = " package in {0} is affected and may need fixing{1}".format(release, note_end)
196+ if args.oci:
197+ generate_oval_cve(args.output_dir, args.cve_prefix_dir, cache, args.oci,
198+ args.no_progress, args.packages, pathnames, ociprefix, ocioutdir)
199 else:
200- warn('Unsupported status "{0}" in {1}_{2} in "{3}". Setting to "unknown".'.format(code, release, package, filepath))
201- status['status'] = 'unknown'
202- status['note'] = " package in {0} has a vulnerability that is not known (status: '{1}'). It is pending evaluation{2}".format(release, code, note_end)
203+ generate_oval_cve(args.output_dir, args.cve_prefix_dir, cache, args.oci,
204+ args.no_progress, args.packages, pathnames)
205
206- return status
207
208
209 # given a status generated by parse_package_status(), duplicate it for a
210@@ -354,10 +253,7 @@ def parse_cve_file(filepath, cache, pkg_filter=None):
211 if rel not in supported_releases:
212 continue
213 state, details = data['pkgs'][pkg][rel]
214- status_line = state
215- if len(details) > 0:
216- status_line += ' (' + details + ')'
217- packages[pkg]['Releases'][rel] = parse_package_status(rel, pkg, status_line, filepath, cache)
218+ packages[pkg]['Releases'][rel] = oval_lib.CVEPkgRelEntry.parse_package_status(rel, pkg, state, details, filepath, cache)
219
220 # add supplemental packages; usually kernels only need this special case.
221 for package in [name for name in packages if name in kernel_srcs]:
222@@ -458,7 +354,6 @@ def prepend_usn_to_id(usn_database, usn_id):
223 if re.search(r'^[0-9]+-[0-9]$', usn_id):
224 usn_database[usn_id]['id'] = 'USN-' + usn_id
225
226-
227 # Class to contain the binary package cache
228 class PackageCache():
229
230@@ -466,6 +361,7 @@ class PackageCache():
231 cache_updates = 0
232 releases = dict()
233 unpublished_sources = dict()
234+ packages_to_ignore = ("-dev", "-doc", "-dbg", "-dbgsym", "-udeb", "-locale-")
235
236 def __init__(self, cachefile='data_file.json', force_reload=False):
237 self.cachefile = cachefile
238@@ -597,7 +493,7 @@ class PackageCache():
239 if pname.startswith('linux') and not (i.binary_package_name).startswith('linux-image-'):
240 continue
241 # skip ignored packages, with exception of golang*-dev pkgs
242- if (i.binary_package_name).startswith(('golang-go')) or not any(s in i.binary_package_name for s in packages_to_ignore):
243+ if (i.binary_package_name).startswith(('golang-go')) or not any(s in i.binary_package_name for s in self.packages_to_ignore):
244 binlist.append(i.binary_package_name)
245
246 # save current pkgcache to local cache
247@@ -684,6 +580,54 @@ def generate_oval_usn(outdir, usn, usn_release, cve_dir, usn_db_dir, ociprefix=N
248
249 return True
250
251+def generate_oval_package(release, outdir, cve_prefix_dir, pkg_cache, cve_cache, oci, no_progress, packages, pathnames, ociprefix=None, ocioutdir=None):
252+ print(f'[*] Generating OVAL for packages in release {release}')
253+ ov = oval_lib.OvalGeneratorPkg(release, release_name(release), pathnames,packages, not no_progress,pkg_cache=pkg_cache, cve_cache=cve_cache, oval_format='oci' if oci else 'dpkg', outdir=outdir, cve_prefix_dir=cve_prefix_dir, prefix=ociprefix)
254+ ov.generate_oval()
255+ pkg_cache.write_cache()
256+ print(f'[X] Done generating OVAL for packages in release {release}')
257
258+def generate_oval_cve(outdir, cve_prefix_dir, cache, oci, no_progress, packages, pathnames, ociprefix=None, ocioutdir=None):
259+ ovals = dict()
260+ for i in supported_releases:
261+ # we can have nested parent releases
262+ parent = release_progenitor(i)
263+ index = '{0}_dpkg'.format(i)
264+ ovals[index] = oval_lib.OvalGeneratorCVE(i, release_name(i), parent, warn, outdir, prefix='', oval_format='dpkg')
265+ ovals[index].add_release_applicability_definition()
266+ if oci:
267+ index = '{0}_oci'.format(i)
268+ ovals[index] = oval_lib.OvalGeneratorCVE(i, release_name(i), parent, warn, ocioutdir, prefix=ociprefix, oval_format='oci')
269+ ovals[index].add_release_applicability_definition()
270+
271+ # loop through all CVE data files
272+ files = []
273+ for pathname in pathnames:
274+ files = files + glob.glob(os.path.join(cve_prefix_dir, pathname))
275+ files.sort()
276+
277+ pkg_filter = None
278+ if packages:
279+ pkg_filter = packages
280+
281+ files_count = len(files)
282+ for i_file, filepath in enumerate(files):
283+ cve_data = parse_cve_file(filepath, cache, pkg_filter)
284+ # skip CVEs without packages for supported releases
285+ if not cve_data['packages']:
286+ if not no_progress:
287+ progress_bar(i_file + 1, files_count)
288+ continue
289+
290+ for i in ovals:
291+ ovals[i].generate_cve_definition(cve_data)
292+
293+ if not no_progress:
294+ progress_bar(i_file + 1, files_count)
295+
296+ for i in ovals:
297+ ovals[i].write_to_file()
298+
299+ cache.write_cache()
300 if __name__ == '__main__':
301 main()
302diff --git a/scripts/oval_lib.py b/scripts/oval_lib.py
303index f17f57d..9ced3d3 100644
304--- a/scripts/oval_lib.py
305+++ b/scripts/oval_lib.py
306@@ -27,11 +27,18 @@ import shutil
307 import sys
308 import tempfile
309 import collections
310+import glob
311+import xml.etree.cElementTree as etree
312
313-from cve_lib import load_cve, get_subproject_description, release_name, release_stamp
314+from source_map import load
315+import cve_lib
316
317 from xml.sax.saxutils import escape
318
319+sources = {}
320+source_map_binaries = {}
321+debug_level = 0
322+
323 def recursive_rm(dirPath):
324 '''recursively remove directory'''
325 names = os.listdir(dirPath)
326@@ -107,8 +114,1015 @@ def process_kernel_binaries(binaries, oval_format):
327
328 return None
329
330+def debug(message):
331+ """ print a debuging message """
332+ if debug_level > 0:
333+ sys.stdout.write('\rDEBUG: {0}\n'.format(message))
334
335 class OvalGenerator:
336+ supported_oval_elements = ('definition', 'test', 'object', 'state', 'variable')
337+ generator_version = '1.1'
338+ oval_schema_version = '5.11.1'
339+ def __init__(self, release, release_name, parent = None, warn_method=False, outdir='./', prefix='', oval_format='dpkg') -> None:
340+ self.release = release
341+ # e.g. codename for trusty/esm should be trusty
342+ self.release_codename = parent if parent else self.release.replace('/', '_')
343+ self.release_name = release_name
344+ #self.warn = warn_method or self.warn
345+ self.tmpdir = tempfile.mkdtemp(prefix='oval_lib-')
346+ self.output_dir = outdir
347+ self.oval_format = oval_format
348+ self.output_filepath = \
349+ '{0}com.ubuntu.{1}.cve.oval.xml'.format(prefix, self.release.replace('/', '_'))
350+ self.ns = 'oval:com.ubuntu.{0}'.format(self.release_codename)
351+ self.id = 100
352+ self.host_def_id = self.id
353+ self.release_applicability_definition_id = '{0}:def:{1}0'.format(self.ns, self.id)
354+
355+ def _add_structure(self, root) -> None:
356+ structure = {}
357+ for element in self.supported_oval_elements:
358+ structure_element = element + 's'
359+ etree.SubElement(root, structure_element)
360+
361+ return structure
362+
363+ def _get_root_element(self, type) -> etree.Element:
364+ oval_timestamp = datetime.now(tz=timezone.utc).strftime(
365+ '%Y-%m-%dT%H:%M:%S')
366+
367+ root_element = etree.Element("oval_definitions", attrib= {
368+ "xmlns":"http://oval.mitre.org/XMLSchema/oval-definitions-5",
369+ "xmlns:ind-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#independent",
370+ "xmlns:oval":"http://oval.mitre.org/XMLSchema/oval-common-5",
371+ "xmlns:unix-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#unix",
372+ "xmlns:linux-def":"http://oval.mitre.org/XMLSchema/oval-definitions-5#linux",
373+ "xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance" ,
374+ "xsi:schemaLocation":"http://oval.mitre.org/XMLSchema/oval-common-5 oval-common-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5 oval-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#independent independent-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#unix unix-definitions-schema.xsd http://oval.mitre.org/XMLSchema/oval-definitions-5#macos linux-definitions-schema.xsd"
375+ })
376+
377+ generator = etree.SubElement(root_element, "generator")
378+ product_name = etree.SubElement(generator, "oval:product_name")
379+ product_version = etree.SubElement(generator, "oval:product_version")
380+ schema_version = etree.SubElement(generator, "oval:schema_version")
381+ timestamp = etree.SubElement(generator, "oval:timestamp")
382+
383+ product_name.text = f"Canonical {type} OVAL Generator"
384+ product_version.text = self.generator_version
385+ schema_version.text = self.oval_schema_version
386+ timestamp.text = oval_timestamp
387+
388+ xml_tree = etree.ElementTree(root_element)
389+ return xml_tree, root_element
390+
391+ def _add_release_checks(self, root_element) -> None:
392+ rel_definition = self._create_release_definition()
393+ rel_family_test, rel_test = self._create_release_test()
394+ rel_family_obj, rel_obj = self._create_release_object()
395+ rel_family_state, rel_state = self._create_release_state()
396+
397+ definitions = root_element.find("definitions")
398+ tests = root_element.find("tests")
399+ objects = root_element.find("objects")
400+ states = root_element.find("states")
401+
402+ definitions.append(rel_definition)
403+ tests.append(rel_family_test)
404+ tests.append(rel_test)
405+ objects.append(rel_family_obj)
406+ objects.append(rel_obj)
407+ states.append(rel_family_state)
408+ states.append(rel_state)
409+
410+
411+ def _create_release_definition(self) -> etree.Element:
412+ if self.oval_format == 'dpkg':
413+ definition = etree.Element("definition")
414+ definition.set("class", "inventory")
415+ definition.set("id", f'{self.ns}:def:{self.id}')
416+ definition.set("version", "1")
417+
418+ # Metadata tag
419+ metadata = etree.Element("metadata")
420+ title = etree.SubElement(metadata, "title")
421+ etree.SubElement(metadata, "description")
422+ title.text = f"Check that {self.release_name} ({self.release_codename}) is installed."
423+
424+ # Criteria tag
425+ criteria = etree.Element("criteria")
426+ criterion_unix = etree.SubElement(criteria, "criterion")
427+ criterion_rel = etree.SubElement(criteria, "criterion")
428+
429+
430+ criterion_unix.set("test_ref", f'{self.ns}:tst:{self.id}')
431+ criterion_unix.set("comment", "The host is part of the unix family.")
432+
433+ criterion_rel.set("test_ref", f'{self.ns}:tst:{self.id+1}')
434+ criterion_rel.set("comment", f"The host is running Ubuntu {self.release_codename}")
435+
436+
437+ definition.append(metadata)
438+ definition.append(criteria)
439+ else:
440+ definition = etree.Element()
441+
442+ return definition
443+
444+ def _create_release_test(self) -> tuple[etree.Element, etree.Element]:
445+ if self.oval_format == 'dpkg':
446+ family_test = etree.Element("ind-def:family_test", attrib={
447+ "id": f'{self.ns}:tst:{self.id}',
448+ "check":"at least one",
449+ "check_existence":"at_least_one_exists",
450+ "version":"1",
451+ "comment":"Is the host part of the unix family?"
452+ })
453+
454+ family_test_obj = etree.SubElement(family_test, "ind-def:object")
455+ family_test_state = etree.SubElement(family_test, "ind-def:state")
456+ family_test_obj.set("object_ref", f'{self.ns}:obj:{self.id}')
457+ family_test_state.set("state_ref", f'{self.ns}:ste:{self.id}')
458+
459+ textfilecontent54_test = etree.Element("ind-def:textfilecontent54_test", attrib={
460+ "id": f'{self.ns}:tst:{self.id+1}',
461+ "check":"at least one",
462+ "check_existence":"at_least_one_exists",
463+ "version":"1",
464+ "comment":f"Is the host running Ubuntu {self.release_codename}?"
465+ })
466+
467+ textfc54_test_obj = etree.SubElement(textfilecontent54_test, "ind-def:object")
468+ textfc54_test_state = etree.SubElement(textfilecontent54_test, "ind-def:state")
469+ textfc54_test_obj.set("object_ref", f'{self.ns}:obj:{self.id+1}')
470+ textfc54_test_state.set("state_ref", f'{self.ns}:ste:{self.id+1}')
471+
472+ else:
473+ family_test = etree.Element()
474+ textfilecontent54_test = etree.Element()
475+
476+ return family_test, textfilecontent54_test
477+
478+ def _create_release_object(self) -> tuple[etree.Element, etree.Element]:
479+ if self.oval_format == 'dpkg':
480+ family_object = etree.Element("ind-def:family_object",
481+ attrib={
482+ 'id' : f"{self.ns}:obj:{self.id}",
483+ 'version': "1",
484+ "comment": "The singleton family object."
485+ })
486+
487+ object = etree.Element("ind-def:textfilecontent54_object",
488+ attrib={
489+ 'id' : f"{self.ns}:obj:{self.id+1}",
490+ 'version': "1",
491+ "comment": f"The singleton {self.release_codename} object."
492+ })
493+ filepath = etree.SubElement(object, "ind-def:filepath")
494+ pattern = etree.SubElement(object, "ind-def:pattern",attrib={"operation": "pattern match"})
495+ instance = etree.SubElement(object, "ind-def:instance",attrib={"datatype": "int"})
496+
497+ filepath.text = "/etc/lsb-release"
498+ pattern.text = "^[\s\S]*DISTRIB_CODENAME=([a-z]+)$"
499+ instance.text = "1"
500+ else:
501+ family_object = etree.Element("")
502+ object = etree.Element("")
503+
504+ return family_object, object
505+
506+ def _create_release_state(self) -> tuple[etree.Element, etree.Element]:
507+ if self.oval_format == 'dpkg':
508+
509+ family_state= etree.Element("ind-def:family_state",
510+ attrib={
511+ 'id' : f"{self.ns}:ste:{self.id}",
512+ 'version': "1",
513+ "comment": "The singleton family state."
514+ })
515+
516+ state = etree.Element("ind-def:textfilecontent54_state",
517+ attrib={
518+ 'id' : f"{self.ns}:ste:{self.id+1}",
519+ 'version': "1",
520+ "comment": f"The singleton {self.release_codename} state."
521+ })
522+
523+ family = etree.SubElement(family_state, "ind-def:family")
524+ subexpression = etree.SubElement(state, "ind-def:subexpression")
525+
526+ family.text = "unix"
527+ subexpression.text = cve_lib.product_series(self.release)[1]
528+ else:
529+ family_state = etree.Element()
530+ state = etree.Element()
531+
532+ return family_state, state
533+
534+class CVEPkgRelEntry:
535+ def __init__(self, pkg, cve, status, note) -> None:
536+ self.pkg = pkg
537+ self.cve = cve
538+ self.orig_status = status
539+ self.orig_note = note
540+ cve_info = CVEPkgRelEntry.parse_package_status(pkg.rel, pkg.name, status, note, cve.number, None)
541+
542+ self.note = cve_info['note']
543+ self.status = cve_info['status']
544+ self.fixed_version = cve_info['fix-version'] if self.status == 'fixed' else None
545+
546+ @staticmethod
547+ def parse_package_status(release, package, status_text, note, filepath, cache):
548+ """ parse ubuntu package status string format:
549+ <status code> (<version/notes>)
550+ outputs dictionary: {
551+ 'status' : '<not-applicable | unknown | vulnerable | fixed>',
552+ 'note' : '<description of the status>',
553+ 'fix-version' : '<version with issue fixed, if applicable>',
554+ 'bin-pkgs' : []
555+ } """
556+
557+ # TODO fix for CVE Generator
558+
559+ # break out status code and detail
560+ code = status_text.lower()
561+ detail = note.strip('()') if note else None
562+ status = {}
563+
564+ if cache and code != 'dne':
565+ if detail and detail[0].isdigit() and code in ['released', 'not-affected']:
566+ status['bin-pkgs'] = cache.get_binarypkgs(package, release, version=detail)
567+ else:
568+ status['bin-pkgs'] = cache.get_binarypkgs(package, release)
569+
570+ note_end = " (note: '{0}').".format(detail) if detail else '.'
571+ if code == 'dne':
572+ status['status'] = 'not-applicable'
573+ status['note'] = \
574+ " package does not exist in {0}{1}".format(release, note_end)
575+ elif code == 'ignored':
576+ status['status'] = 'vulnerable'
577+ status['note'] = ": while related to the CVE in some way, a decision has been made to ignore this issue{0}".format(note_end)
578+ elif code == 'not-affected':
579+ # check if there is a release version and if so, test for
580+ # package existence with that version
581+ if detail and detail[0].isdigit():
582+ status['status'] = 'fixed'
583+ status['note'] = " package in {0}, is related to the CVE in some way and has been fixed{1}".format(release, note_end)
584+ status['fix-version'] = detail
585+ else:
586+ status['status'] = 'not-vulnerable'
587+ status['note'] = " package in {0}, while related to the CVE in some way, is not affected{1}".format(release, note_end)
588+ elif code == 'needed':
589+ status['status'] = 'vulnerable'
590+ status['note'] = \
591+ " package in {0} is affected and needs fixing{1}".format(release, note_end)
592+ elif code == 'pending':
593+ # pending means that packages have been prepared and are in
594+ # -proposed or in a ppa somewhere, and should have a version
595+ # attached. If there is a version, test for package existence
596+ # with that version, otherwise mark as vulnerable
597+ if detail and detail[0].isdigit():
598+ status['status'] = 'fixed'
599+ status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(release, note_end)
600+ status['fix-version'] = detail
601+ else:
602+ status['status'] = 'vulnerable'
603+ status['note'] = " package in {0} is affected. An update containing the fix has been completed and is pending publication{1}".format(release, note_end)
604+ elif code == 'deferred':
605+ status['status'] = 'vulnerable'
606+ status['note'] = " package in {0} is affected, but a decision has been made to defer addressing it{1}".format(release, note_end)
607+ elif code in ['released']:
608+ # if there isn't a release version, then just mark
609+ # as vulnerable to test for package existence
610+ if not detail:
611+ status['status'] = 'vulnerable'
612+ status['note'] = " package in {0} was vulnerable and has been fixed, but no release version available for it{1}".format(release, note_end)
613+ else:
614+ status['status'] = 'fixed'
615+ status['note'] = " package in {0} was vulnerable but has been fixed{1}".format(release, note_end)
616+ status['fix-version'] = detail
617+ elif code == 'needs-triage':
618+ status['status'] = 'vulnerable'
619+ status['note'] = " package in {0} is affected and may need fixing{1}".format(release, note_end)
620+ else:
621+ # TODO LOGGIN
622+ print('Unsupported status "{0}" in {1}_{2} in "{3}". Setting to "unknown".'.format(code, release, package, filepath))
623+ status['status'] = 'unknown'
624+ status['note'] = " package in {0} has a vulnerability that is not known (status: '{1}'). It is pending evaluation{2}".format(release, code, note_end)
625+
626+ return status
627+
628+ def __str__(self) -> str:
629+ return f'{str(self.pkg)}:{self.status} {self.fixed_version}'
630+
631+class CVE:
632+ def __init__(self, number, info, pkgs=[]) -> None:
633+ self.number = number
634+ self.description = info['Description']
635+ self.pkg_rel_entries = {}
636+ self.pkgs = pkgs
637+
638+ def add_pkg(self, pkg_object, state, note):
639+ cve_pkg_entry = CVEPkgRelEntry(pkg_object, self, state, note)
640+ self.pkg_rel_entries[Package.get_unique_id(pkg_object.name, pkg_object.rel)] = cve_pkg_entry
641+ self.pkgs.append(pkg_object)
642+
643+ def __str__(self) -> str:
644+ return self.number
645+
646+ def __repr__(self):
647+ return self.__str__()
648+
649+class Package:
650+ def __init__(self, pkgname, rel, binaries, version):
651+ self.name = pkgname
652+ self.rel = rel
653+ self.description = cve_lib.lookup_package_override_description(pkgname)
654+
655+ if not self.description:
656+ if 'description' in sources[rel][pkgname]:
657+ self.description = sources[rel][pkgname]['description']
658+ elif pkgname in source_map_binaries[rel] and \
659+ 'description' in source_map_binaries[rel][pkgname]:
660+ self.description = source_map_binaries[rel][pkgname]['description']
661+ else:
662+ # Get first description found
663+ if 'binaries' in sources[self.rel][self.name]:
664+ for binary in sources[self.rel][self.name]['binaries']:
665+ if binary in source_map_binaries[self.rel] and 'description' in source_map_binaries[self.rel][binary]:
666+ self.description = source_map_binaries[self.rel][binary]["description"]
667+ break
668+
669+ self.section = sources[rel][pkgname]['section']
670+ self.version = version
671+ self.binaries = binaries if binaries else []
672+ self.cves = []
673+
674+ @staticmethod
675+ def get_unique_id(name, rel):
676+ return f'{name}/{rel}'
677+
678+ def add_cve(self, cve) -> None:
679+ self.cves.append(cve)
680+
681+ def __str__(self) -> str:
682+ return f"{self.name}/{self.rel}"
683+
684+ def __repr__(self):
685+ return self.__str__()
686+
687+class OvalGeneratorPkg(OvalGenerator):
688+ def __init__(self, release, release_name, cve_paths, packages, progress, pkg_cache, cve_cache=None, cve_prefix_dir=None, parent=None, warn_method=False, outdir='./', prefix='', oval_format='dpkg') -> None:
689+ super().__init__(release, release_name, parent, warn_method, outdir, prefix, oval_format)
690+ ###
691+ # ID schema: 2204|00001|0001
692+ # * The first four digits are the ubuntu release number
693+ # * The next 5 digits is # just a package counter, we increase it for each definition
694+ # * The last 4 digits is a counter for the criterion
695+ ###
696+ release_code = int(release_name.split(' ')[1].replace('.', '')) if release not in cve_lib.external_releases else 1111
697+ self.definition_id = release_code * 10 ** 10
698+ self.definition_step = 1 * 10 ** 5
699+ self.criterion_step = 10
700+ self.progress = progress
701+ self.cve_cache = cve_cache
702+ self.pkg_cache = pkg_cache
703+ self.cve_paths = cve_paths
704+ self.packages = self._load_pkgs(cve_prefix_dir, packages)
705+
706+ def _generate_advisory(self, package: Package) -> etree.Element:
707+ advisory = etree.Element("advisory")
708+ rights = etree.SubElement(advisory, "rights")
709+ component = etree.SubElement(advisory, "component")
710+ version = etree.SubElement(advisory, "current_version")
711+
712+ rights.text = f"Copyright (C) {datetime.now().year} Canonical Ltd."
713+ component.text = package.section
714+ version.text = package.version
715+
716+ return advisory
717+
718+ def _generate_metadata(self, package: Package) -> etree.Element:
719+ metadata = etree.Element("metadata")
720+ title = etree.SubElement(metadata, "title")
721+ reference = self._generate_reference(package)
722+ advisory = self._generate_advisory(package)
723+ metadata.append(reference)
724+ description = etree.SubElement(metadata, "description")
725+ affected = etree.SubElement(metadata, "affected", attrib = {"family": "unix"})
726+ platform = etree.SubElement(affected, "platform")
727+ metadata.append(advisory)
728+
729+ platform.text = self.release_name
730+ title.text = package.name
731+ description.text = package.description
732+
733+ return metadata
734+
735+ def _generate_criteria(self) -> etree.Element:
736+ criteria = etree.Element("criteria")
737+ if self.oval_format == 'dpkg':
738+ extend_definition = etree.SubElement(criteria, "extend_definition")
739+
740+ extend_definition.set("definition_ref", f"{self.ns}:def:{self.host_def_id}")
741+ extend_definition.set("comment", f"{self.release_name} is installed.")
742+ extend_definition.set("applicability_check", "true")
743+
744+ return criteria
745+
746+ # Element generators
747+ def _generate_reference(self, package) -> etree.Element:
748+ reference = etree.Element("reference", attrib={
749+ "source": "Package",
750+ "ref_id": package.name,
751+ "ref_url": f'https://launchpad.net/ubuntu/+source/{package.name}'
752+ })
753+
754+ return reference
755+
756+ def _generate_definition_object(self, package) -> None:
757+ id = f"{self.ns}:def:{self.definition_id}"
758+ definition = etree.Element("definition")
759+ definition.set("class", "vulnerability")
760+ definition.set("id", id)
761+ definition.set("version", "1")
762+
763+ metadata = self._generate_metadata(package)
764+ criteria = self._generate_criteria()
765+ definition.append(metadata)
766+ definition.append(criteria)
767+ return definition
768+
769+ def _generate_var_object(self, comment, id, binaries) -> etree.Element:
770+ var = etree.Element("constant_variable",
771+ attrib={
772+ 'id' : f"{self.ns}:var:{id}",
773+ 'version': "1",
774+ "datatype": "string",
775+ "comment": comment
776+ })
777+
778+ for binary in binaries:
779+ item = etree.SubElement(var, "value")
780+ item.text = binary
781+
782+ return var
783+
784+ def _generate_object_object(self, comment, id, var_id) -> etree.Element:
785+ if self.oval_format == 'dpkg':
786+ object = etree.Element("linux-def:dpkginfo_object",
787+ attrib={
788+ 'id' : f"{self.ns}:obj:{id}",
789+ 'version': "1",
790+ "comment": comment
791+ })
792+
793+ etree.SubElement(object, "linux-def:name", attrib={
794+ "var_ref": f"{self.ns}:var:{var_id}",
795+ "var_check": "at least one"
796+ })
797+ elif self.oval_format == 'oci':
798+ object = etree.Element("ind-def:textfilecontent54_object",
799+ attrib={
800+ 'id' : f"{self.ns}:obj:{id}",
801+ 'version': "1",
802+ "comment": comment
803+ })
804+ path = etree.SubElement(object, 'ind-def:path')
805+ filename = etree.SubElement(object, 'ind-def:filename')
806+ etree.SubElement(object, "ind-def:pattern", attrib={
807+ "operation": "pattern match",
808+ "datatype": "string",
809+ "var_ref": f"{self.ns}:var:{var_id}",
810+ "var_check": "at least one"
811+ })
812+ instance = etree.SubElement(object, 'ind-def:instance', attrib={
813+ "operation": "greater than or equal",
814+ "datatype": "int"
815+ })
816+ path.text = '.'
817+ filename.text = 'manifest'
818+ instance.text = '1'
819+ return object
820+
821+ def _generate_test_element(self, comment, id, create_state, type, obj_id = None, state_id=None) -> etree.Element:
822+ if type == 'pkg':
823+ if self.oval_format == 'dpkg':
824+ tag = 'dpkginfo_test'
825+ pre_tag = 'linux-def'
826+ elif self.oval_format == 'oci':
827+ tag = 'textfilecontent54_test'
828+ pre_tag = 'ind-def'
829+ else:
830+ ValueError()
831+ elif type == 'kernel':
832+ tag = 'variable_test'
833+ pre_tag = 'ind-def'
834+
835+ test = etree.Element(f'{pre_tag}:{tag}', attrib={
836+ "id": f"{self.ns}:tst:{id}",
837+ "version": "1",
838+ "check_existence": "at_least_one_exists",
839+ "check": "at least one",
840+ "comment": comment
841+ })
842+ textfc54_test_obj = etree.SubElement(test, f"{pre_tag}:object")
843+ textfc54_test_obj.set("object_ref", f'{self.ns}:obj:{obj_id if obj_id else id}')
844+
845+ if create_state:
846+ textfc54_test_state = etree.SubElement(test, f"{pre_tag}:state")
847+ textfc54_test_state.set("state_ref", f'{self.ns}:ste:{state_id if state_id else id}')
848+
849+ return test
850+
851+ def _generate_state_object(self, comment, id, version) -> None:
852+ if self.oval_format == 'dpkg':
853+ object = etree.Element("linux-def:dpkginfo_state",
854+ attrib={
855+ 'id' : f"{self.ns}:ste:{id}",
856+ 'version': "1",
857+ "comment": comment
858+ })
859+
860+ version_check = etree.SubElement(object, "linux-def:evr", attrib={
861+ "datatype": "debian_evr_string",
862+ "operation": "less than"
863+ })
864+
865+ version_check.text = f"0:{version}"
866+ elif self.oval_format == 'oci':
867+ object = etree.Element("ind-def:textfilecontent54_state",
868+ attrib={
869+ 'id' : f"{self.ns}:ste:{id}",
870+ 'version': "1",
871+ "comment": comment
872+ })
873+
874+ version_check = etree.SubElement(object, "ind-def:subexpression", attrib={
875+ "datatype": "debian_evr_string",
876+ "operation": "less than"
877+ })
878+
879+ version_check.text = f"0:{version}"
880+ else:
881+ ValueError(f"Format not {self.oval_format} not supported")
882+
883+ return object
884+
885+ def _generate_criterion_element(self, comment, id) -> etree.Element:
886+ criterion = etree.Element("criterion", attrib={
887+ "test_ref": f"{self.ns}:tst:{id}",
888+ "comment": comment
889+ })
890+
891+ return criterion
892+
893+ # Running kernel element generators
894+ def _add_running_kernel_checks(self, root_element):
895+ objects = root_element.find("objects")
896+ variables = root_element.find("variables")
897+ states = root_element.find("states")
898+
899+ variable_local_kernel_check = self._generate_local_variable_kernel(self.definition_id, "Kernel version in evr format", self.definition_id)
900+ obj_running_kernel = self._generate_uname_object_element(self.definition_id)
901+ state_kernel_version = self._generate_state_kernel_element("Kernel check", self.definition_id, self.definition_id)
902+
903+ objects.append(obj_running_kernel)
904+ variables.append(variable_local_kernel_check)
905+ states.append(state_kernel_version)
906+
907+ def _generate_local_variable_kernel(self, id, comment, uname_obj_id):
908+ var = etree.Element("local_variable",
909+ attrib={
910+ 'id' : f"{self.ns}:var:{id}",
911+ 'version': "1",
912+ "datatype": "debian_evr_string",
913+ "comment": comment
914+ })
915+ concat = etree.SubElement(var, "concat")
916+ component = etree.SubElement(concat, "literal_component")
917+ regex = etree.SubElement(concat, "regex_capture", attrib={
918+ "pattern": "^([\d|\.]+-\d+)[-|\w]+$"
919+ })
920+
921+ etree.SubElement(regex, "object_component", attrib={
922+ "object_ref": f"{self.ns}:obj:{uname_obj_id}",
923+ "item_field": "os_release"
924+ })
925+
926+ component.text = "0:"
927+
928+ return var
929+
930+ def _generate_uname_object_element(self, id):
931+ object = etree.Element("unix-def:uname_object",
932+ attrib={
933+ 'id' : f"{self.ns}:obj:{id}",
934+ 'version': "1",
935+ "comment": "The uname object."
936+ })
937+
938+ return object
939+
940+ def _generate_uname_state_element(self, id, regex, comment):
941+ object = etree.Element("unix-def:uname_state",
942+ attrib={
943+ 'id' : f"{self.ns}:ste:{id}",
944+ 'version': "1",
945+ "comment": comment
946+ })
947+
948+ version_check = etree.SubElement(object, "unix-def:os_release", attrib={
949+ "operation": "pattern match"
950+ })
951+
952+ version_check.text = regex
953+
954+ return object
955+
956+ def _generate_variable_kernel_version(self, comment, id, version):
957+ var = etree.Element("constant_variable",
958+ attrib={
959+ 'id' : f"{self.ns}:var:{id}",
960+ 'version': "1",
961+ "datatype": "debian_evr_string",
962+ "comment": comment
963+ })
964+
965+ item = etree.SubElement(var, "value")
966+ item.text = f"0:{version.rsplit('.', 1)[0]}"
967+
968+ return var
969+
970+ def _generate_test_element_running_kernel(self, id, comment, obj_id):
971+ test = etree.Element("unix-def:uname_test", attrib={
972+ "id": f"{self.ns}:tst:{id}",
973+ "version": "1",
974+ "check": "at least one",
975+ "comment": comment
976+ })
977+
978+ textfc54_test_obj = etree.SubElement(test, "unix-def:object")
979+ textfc54_test_obj.set("object_ref", f'{self.ns}:obj:{obj_id}')
980+
981+ textfc54_test_state = etree.SubElement(test, "unix-def:state")
982+ textfc54_test_state.set("state_ref", f'{self.ns}:ste:{id}')
983+
984+ return test
985+
986+ # Kernel elements generators
987+ def _generate_criteria_kernel(self, operator) -> etree.Element:
988+ return etree.Element("criteria", attrib={
989+ "operator": operator
990+ })
991+
992+ def _generate_kernel_version_object_element(self, id, var_id) -> etree.Element:
993+ object = etree.Element("ind-def:variable_object",
994+ attrib={
995+ 'id' : f"{self.ns}:obj:{id}",
996+ 'version': "1",
997+ })
998+
999+ var_ref = etree.SubElement(object, 'ind-def:var_ref')
1000+ var_ref.text = f"{self.ns}:var:{var_id}"
1001+
1002+ return object
1003+
1004+ def _generate_state_kernel_element(self, comment, id, var_id) -> None:
1005+ state = etree.Element("ind-def:variable_state",
1006+ attrib={
1007+ 'id' : f"{self.ns}:ste:{id}",
1008+ 'version': "1",
1009+ "comment": comment
1010+ })
1011+
1012+ etree.SubElement(state, "ind-def:value", attrib={
1013+ "datatype": "debian_evr_string",
1014+ "operation": "greater than",
1015+ "var_check": "at least one",
1016+ "var_ref": f"{self.ns}:var:{var_id}"
1017+ })
1018+
1019+ return state
1020+
1021+ def _generate_kernel_package_elements(self, package: Package, root_element, running_kernel_check_id) -> etree.Element:
1022+ tests = root_element.find("tests")
1023+ states = root_element.find("states")
1024+
1025+ comment_running_kernel = f'Is kernel {package.name} running?'
1026+ regex = process_kernel_binaries(package.binaries, self.oval_format)
1027+
1028+ criterion_running_kernel = self._generate_criterion_element(comment_running_kernel, self.definition_id)
1029+ test_running_kernel = self._generate_test_element_running_kernel(self.definition_id, comment_running_kernel, running_kernel_check_id)
1030+ state_running_kernel = self._generate_uname_state_element(self.definition_id, regex, f"Regex match for kernel {package.name}")
1031+
1032+ self.definition_id += self.criterion_step
1033+
1034+ tests.append(test_running_kernel)
1035+ states.append(state_running_kernel)
1036+
1037+ return criterion_running_kernel
1038+
1039+ def _add_fixed_kernel_elements(self, cve: CVE, package: Package, package_rel_entry:CVEPkgRelEntry, root_element, running_kernel_id) -> etree.Element:
1040+ tests = root_element.find("tests")
1041+ objects = root_element.find("objects")
1042+ variables = root_element.find("variables")
1043+
1044+ comment_version = f'Kernel {package.name} version comparison ({package_rel_entry.fixed_version})'
1045+ comment_criterion = f'({cve.number}) {package.name} {package_rel_entry.note}'
1046+ criterion_version = self._generate_criterion_element(comment_criterion, self.definition_id)
1047+ test_kernel_version = self._generate_test_element(comment_version, self.definition_id, True, 'kernel', state_id=running_kernel_id)
1048+
1049+ obj_kernel_version = self._generate_kernel_version_object_element(self.definition_id, self.definition_id)
1050+ var_version_kernel = self._generate_variable_kernel_version(comment_version, self.definition_id, package_rel_entry.fixed_version)
1051+
1052+ tests.append(test_kernel_version)
1053+ objects.append(obj_kernel_version)
1054+ variables.append(var_version_kernel)
1055+
1056+ return criterion_version
1057+
1058+ # General functions
1059+ def _increase_id(self, is_definition):
1060+ if is_definition:
1061+ self.definition_id += self.definition_step
1062+ clean_value = self.definition_step / 10
1063+ self.definition_id = int(int(self.definition_id / clean_value) * clean_value)
1064+ else:
1065+ self.definition_id += self.criterion_step
1066+
1067+ def _add_to_criteria(self, definition, element, depth=2, operator='OR'):
1068+ criteria = definition
1069+ for _ in range(depth):
1070+ prev_criteria = criteria
1071+ criteria = criteria.find('criteria')
1072+ if criteria == None:
1073+ criteria = etree.SubElement(prev_criteria, "criteria")
1074+ criteria.set("operator", operator)
1075+
1076+ criteria.append(element)
1077+
1078+ def _add_criterion(self, id, package_entry, cve, definition, depth=2) -> None:
1079+ criterion_note = f'({cve.number}) {package_entry.pkg.name}{package_entry.note}'
1080+ criterion = self._generate_criterion_element(criterion_note, id)
1081+ self._add_to_criteria(definition, criterion, depth)
1082+
1083+ def _generate_vulnerable_elements(self, package, obj_id=None):
1084+ binary_keyword = 'binaries' if len(package.binaries) > 1 else 'binary'
1085+ test_note = f"Does the '{package.name}' package exist?"
1086+ object_note = f"The '{package.name}' package {binary_keyword}"
1087+
1088+ test = self._generate_test_element(test_note, self.definition_id, False, 'pkg', obj_id=obj_id)
1089+
1090+ if not obj_id:
1091+ object = self._generate_object_object(object_note, self.definition_id, self.definition_id)
1092+
1093+ binaries = package.binaries
1094+ if self.oval_format == 'oci':
1095+ if is_kernel_binaries(package.binaries):
1096+ regex = process_kernel_binaries(package.binaries, 'oci')
1097+ binaries = [f'^{regex}(?::\w+|)\s+(.*)$\s+(.*)']
1098+ else:
1099+ variable_values = '(?::\w+|)\s+(.*)$\s+(.*)'
1100+
1101+ binaries = []
1102+ for binary in package.binaries:
1103+ binaries.append(f'^{binary}{variable_values}')
1104+ var = self._generate_var_object(object_note, self.definition_id, binaries)
1105+ else:
1106+ object = None
1107+ var = None
1108+ return test, object, var
1109+
1110+ def _generate_fixed_elements(self, package, pkg_rel_entry, obj_id=None):
1111+ binary_keyword = 'binaries' if len(package.binaries) > 1 else 'binary'
1112+ test_note = f"Does the '{package.name}' package exist and is the version less than '{pkg_rel_entry.fixed_version}'?"
1113+ object_note = f"The '{package.name}' package {binary_keyword}"
1114+ state_note = f"The package version is less than '{pkg_rel_entry.fixed_version}'"
1115+
1116+ test = self._generate_test_element(test_note, self.definition_id, True, 'pkg', obj_id=obj_id)
1117+ if not obj_id:
1118+ object = self._generate_object_object(object_note, self.definition_id, self.definition_id)
1119+
1120+ binaries = package.binaries
1121+ if self.oval_format == 'oci':
1122+ if is_kernel_binaries(package.binaries):
1123+ regex = process_kernel_binaries(package.binaries, 'oci')
1124+ binaries = [f'^{regex}(?::\w+|)\s+(.*)$\s+(.*)']
1125+ else:
1126+ variable_values = '(?::\w+|)\s+(.*)$\s+(.*)'
1127+
1128+ binaries = []
1129+ for binary in package.binaries:
1130+ binaries.append(f'^{binary}{variable_values}')
1131+
1132+ var = self._generate_var_object(object_note, self.definition_id, binaries)
1133+ else:
1134+ object = None
1135+ var = None
1136+ state = self._generate_state_object(state_note, self.definition_id, pkg_rel_entry.fixed_version)
1137+
1138+ return test, object, var, state
1139+
1140+ def _populate_pkg(self, package, root_element):
1141+ pkg_id = Package.get_unique_id(package.name, self.release)
1142+ tests = root_element.find("tests")
1143+ objects = root_element.find("objects")
1144+ variables = root_element.find("variables")
1145+ states = root_element.find("states")
1146+
1147+ # Add package definition
1148+ definitions = root_element.find("definitions")
1149+ definition_element = self._generate_definition_object(package)
1150+
1151+ # Control/cache variables
1152+ one_time_added_id = None
1153+ fixed_versions = {}
1154+ binaries_id = None
1155+ cve_added = False
1156+
1157+ for cve in package.cves:
1158+ pkg_rel_entry = cve.pkg_rel_entries[pkg_id]
1159+ if pkg_rel_entry.status == 'vulnerable':
1160+ cve_added = True
1161+ if one_time_added_id:
1162+ self._add_criterion(one_time_added_id, pkg_rel_entry, cve, definition_element)
1163+ else:
1164+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element)
1165+
1166+ test, object, var = self._generate_vulnerable_elements(package, binaries_id)
1167+ tests.append(test)
1168+
1169+ if not binaries_id:
1170+ objects.append(object)
1171+ variables.append(var)
1172+ binaries_id = self.definition_id
1173+
1174+ one_time_added_id = self.definition_id
1175+ self._increase_id(is_definition=False)
1176+ elif pkg_rel_entry.status == 'fixed':
1177+ cve_added = True
1178+
1179+ if pkg_rel_entry.fixed_version in fixed_versions:
1180+ self._add_criterion(fixed_versions[pkg_rel_entry.fixed_version], pkg_rel_entry, cve, definition_element)
1181+ else:
1182+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element)
1183+
1184+ test, object, var, state = self._generate_fixed_elements(package, pkg_rel_entry, binaries_id)
1185+ tests.append(test)
1186+ states.append(state)
1187+
1188+ if not binaries_id:
1189+ objects.append(object)
1190+ variables.append(var)
1191+ binaries_id = self.definition_id
1192+
1193+ fixed_versions[pkg_rel_entry.fixed_version] = self.definition_id
1194+ self._increase_id(is_definition=False)
1195+
1196+ if cve_added:
1197+ definitions.append(definition_element)
1198+
1199+ self._increase_id(is_definition=True)
1200+
1201+ def _populate_kernel_pkg(self, package, root_element, running_kernel_id):
1202+ pkg_id = Package.get_unique_id(package.name, self.release)
1203+ tests = root_element.find("tests")
1204+ objects = root_element.find("objects")
1205+ variables = root_element.find("variables")
1206+
1207+ # Add package definition
1208+ definitions = root_element.find("definitions")
1209+ definition_element = self._generate_definition_object(package)
1210+
1211+ # Control/cache variables
1212+ one_time_added_id = None
1213+ fixed_versions = []
1214+ binaries_id = None
1215+ cve_added = False
1216+
1217+ # Generate one-time elements
1218+ kernel_criterion = self._generate_kernel_package_elements(package, root_element, running_kernel_id)
1219+ criteria = self._generate_criteria_kernel('OR')
1220+
1221+ self._add_to_criteria(definition_element, kernel_criterion, operator='AND')
1222+ self._add_to_criteria(definition_element, criteria, operator='AND')
1223+
1224+ for cve in package.cves:
1225+ pkg_rel_entry = cve.pkg_rel_entries[pkg_id]
1226+ if pkg_rel_entry.status == 'vulnerable':
1227+ cve_added = True
1228+ if one_time_added_id:
1229+ self._add_criterion(one_time_added_id, pkg_rel_entry, cve, definition_element, depth=3)
1230+ else:
1231+ self._add_criterion(self.definition_id, pkg_rel_entry, cve, definition_element, depth=3)
1232+
1233+ test, object, var = self._generate_vulnerable_elements(package, binaries_id)
1234+ tests.append(test)
1235+ objects.append(object)
1236+
1237+ if not binaries_id:
1238+ variables.append(var)
1239+ binaries_id = self.definition_id
1240+
1241+ one_time_added_id = self.definition_id
1242+ self._increase_id(is_definition=False)
1243+ elif pkg_rel_entry.status == 'fixed':
1244+ cve_added = True
1245+
1246+ if not pkg_rel_entry.fixed_version in fixed_versions:
1247+ kernel_version_criterion = self._add_fixed_kernel_elements(cve, package, pkg_rel_entry, root_element, running_kernel_id)
1248+ self._add_to_criteria(definition_element, kernel_version_criterion, depth=3)
1249+ fixed_versions.append(pkg_rel_entry.fixed_version)
1250+ self._increase_id(is_definition=False)
1251+
1252+ if cve_added:
1253+ definitions.append(definition_element)
1254+ self._increase_id(is_definition=True)
1255+
1256+ def _load_pkgs(self, cve_prefix_dir, packages_filter=None) -> None:
1257+ cve_lib.load_external_subprojects()
1258+
1259+ cves = []
1260+ for pathname in self.cve_paths:
1261+ cves = cves + glob.glob(os.path.join(cve_prefix_dir, pathname))
1262+ cves.sort()
1263+
1264+ packages = {}
1265+ sources[self.release] = load(releases=[self.release], skip_eol_releases=False)[self.release]
1266+ orig_name = cve_lib.get_orig_rel_name(self.release)
1267+ if '/' in orig_name:
1268+ orig_name = orig_name.split('/', maxsplit=1)[1]
1269+ source_map_binaries[self.release] = load(data_type='packages',releases=[orig_name], skip_eol_releases=False)[orig_name] \
1270+ if self.release not in cve_lib.external_releases else {}
1271+
1272+ i = 0
1273+ for cve_path in cves:
1274+ cve_number = cve_path.rsplit('/', 1)[1]
1275+ i += 1
1276+
1277+ if self.progress:
1278+ print(f'[{i:5}/{len(cves)}] Processing {cve_number:18}', end='\r')
1279+
1280+ if not cve_number in self.cve_cache:
1281+ self.cve_cache[cve_number] = cve_lib.load_cve(cve_path)
1282+
1283+ info = self.cve_cache[cve_number]
1284+ cve_obj = CVE(cve_number, info)
1285+
1286+ for pkg in info['pkgs']:
1287+ if packages_filter and pkg not in packages_filter:
1288+ continue
1289+
1290+ if self.release in info['pkgs'][pkg] and \
1291+ info['pkgs'][pkg][self.release][0] != 'DNE' and \
1292+ pkg in sources[self.release]:
1293+ pkg_id = Package.get_unique_id(pkg, self.release)
1294+ if pkg_id not in packages:
1295+ binaries = self.pkg_cache.get_binarypkgs(pkg, self.release)
1296+ version = ''
1297+ if binaries:
1298+ version = self.pkg_cache.pkgcache[pkg]['Releases'][self.release]['source_version']
1299+ pkg_obj = Package(pkg, self.release, binaries, version)
1300+ packages[pkg_id] = pkg_obj
1301+
1302+ pkg_obj = packages[pkg_id]
1303+ pkg_obj.cves.append(cve_obj)
1304+ # add_pkg (pkg, status, note)
1305+ cve_obj.add_pkg(pkg_obj, info['pkgs'][pkg][self.release][0],info['pkgs'][pkg][self.release][1])
1306+
1307+ packages = dict(sorted(packages.items()))
1308+ print(' ' * 40, end='\r')
1309+ return packages
1310+
1311+ def generate_oval(self) -> None:
1312+ xml_tree, root_element = self._get_root_element("Package")
1313+ self._add_structure(root_element)
1314+
1315+ if self.oval_format == 'dpkg':
1316+ # One time kernel check
1317+ self._add_release_checks(root_element)
1318+ self._add_running_kernel_checks(root_element)
1319+ running_kernel_id = self.definition_id
1320+ self._increase_id(is_definition=True)
1321+
1322+ for pkg in self.packages:
1323+ if len(self.packages[pkg].binaries) == 0:
1324+ continue
1325+
1326+ if is_kernel_binaries(self.packages[pkg].binaries) and self.oval_format != 'oci':
1327+ self._populate_kernel_pkg(self.packages[pkg], root_element, running_kernel_id)
1328+ else:
1329+ self._populate_pkg(self.packages[pkg], root_element)
1330+
1331+ etree.indent(xml_tree, level=0)
1332+ filename = f"com.ubuntu.{self.release_codename}.pkg.oval.xml"
1333+ if self.oval_format == 'oci':
1334+ filename = f'oci.{filename}'
1335+ xml_tree.write(os.path.join(self.output_dir, filename))
1336+ return
1337+
1338+class OvalGeneratorCVE:
1339 supported_oval_elements = ('definition', 'test', 'object', 'state',
1340 'variable')
1341 generator_version = '1.1'
1342@@ -179,9 +1193,9 @@ class OvalGenerator:
1343 # prepare update instructions if package is fixed
1344 if pkg['status'] == 'fixed':
1345 if 'parent' in release_status:
1346- product_description = get_subproject_description(release_status['parent'])
1347+ product_description = cve_lib.get_subproject_description(release_status['parent'])
1348 else:
1349- product_description = get_subproject_description(release)
1350+ product_description = cve_lib.get_subproject_description(release)
1351 instruction = prepare_instructions(instruction, header['Candidate'], product_description, pkg)
1352
1353 # if no packages for this release, then we're done
1354@@ -407,6 +1421,7 @@ class OvalGenerator:
1355 package['note'] = package['name'] + package['note']
1356 return {'id': self.id_unknown_test, 'comment': package['note']}
1357
1358+ # TODO: xml lib
1359 def add_release_applicability_definition(self):
1360 """ add platform/release applicability OVAL definition for codename """
1361
1362@@ -464,6 +1479,7 @@ class OvalGenerator:
1363 <ind-def:subexpression>{codename}</ind-def:subexpression>
1364 </ind-def:textfilecontent54_state>\n""".format(**mapping))
1365
1366+ # TODO: xml lib
1367 def get_package_object_id(self, name, bin_pkgs, id_base, version=1):
1368 """ create unique object for each package and return its OVAL id """
1369 if not hasattr(self, 'package_objects'):
1370@@ -532,6 +1548,7 @@ class OvalGenerator:
1371
1372 return self.package_objects[key]
1373
1374+ # TODO: xml lib
1375 def get_package_version_state_id(self, id_base, fix_version, version=1):
1376 """ create unique states for each version and return its OVAL id """
1377 if not hasattr(self, 'package_version_states'):
1378@@ -555,6 +1572,7 @@ class OvalGenerator:
1379
1380 return self.package_version_states[key]
1381
1382+ # TODO: xml lib
1383 def get_package_test_id(self, name, id_base, test_title, object_id, state_id=None, version=1, check_existence='at_least_one_exists'):
1384 """ create unique test for each parameter set and return its OVAL id """
1385 if not hasattr(self, 'package_tests'):
1386@@ -579,6 +1597,7 @@ class OvalGenerator:
1387
1388 return self.package_tests[key]
1389
1390+ # TODO: xml lib
1391 def get_running_kernel_object_id(self, id_base, var_id, version=1):
1392 """ creates a uname_object so we can use the value from uname -r for
1393 mainly two things:
1394@@ -611,6 +1630,7 @@ class OvalGenerator:
1395
1396 return (self.kernel_uname_obj_id, object_id_2)
1397
1398+ # TODO: xml lib
1399 def get_running_kernel_state_id(self, uname_regex, id_base, var_id, version=1):
1400 """ create uname_state to compare the system uname to the affected kernel
1401 uname regex, allowing us to verify we are running the same major version
1402@@ -643,6 +1663,7 @@ class OvalGenerator:
1403
1404 return (self.uname_states[uname_regex], self.kernel_state_id)
1405
1406+ # TODO: xml lib
1407 def get_running_kernel_variable_id(self, uname_regex, id_base, fixed_version, version=1):
1408 """ creates a local variable to store running kernel version in devian evr string"""
1409 if not hasattr(self, 'uname_variables'):
1410@@ -675,6 +1696,7 @@ class OvalGenerator:
1411
1412 return (self.uname_variables['local_variable'], var_id_2)
1413
1414+ # TODO: xml lib
1415 def get_running_kernel_test_id(self, uname_regex, id_base, name, object_id, state_id, object_id_2, state_id_2, version=1):
1416 """ create uname test and return its OVAL id """
1417 if not hasattr(self, 'uname_tests'):
1418@@ -724,6 +1746,7 @@ class OvalGenerator:
1419
1420 self.tmp[element].write(xml + '\n')
1421
1422+ # TODO: xml lib
1423 def write_to_file(self):
1424 """ dequeue all elements into one OVAL definitions file and clean up """
1425 if not hasattr(self, 'tmp'):
1426@@ -792,7 +1815,6 @@ class OvalGenerator:
1427 """ print a warning message """
1428 print('WARNING: {0}'.format(message))
1429
1430-
1431 class OvalGeneratorUSN():
1432 supported_oval_elements = ('definition', 'test', 'object', 'state',
1433 'variable')
1434@@ -851,6 +1873,7 @@ class OvalGeneratorUSN():
1435 self.oval_structure['object'].write(oval_rel_struct['object'])
1436 self.oval_structure['state'].write(oval_rel_struct['state'])
1437
1438+ # TODO: xml lib
1439 def create_release_definition(self):
1440 if self.oval_format == 'dpkg':
1441 mapping = {
1442@@ -876,6 +1899,7 @@ class OvalGeneratorUSN():
1443
1444 return definition
1445
1446+ # TODO: xml lib
1447 def create_release_test(self):
1448 if self.oval_format == 'dpkg':
1449 mapping = {
1450@@ -895,6 +1919,7 @@ class OvalGeneratorUSN():
1451
1452 return test
1453
1454+ # TODO: xml lib
1455 def create_release_object(self):
1456 if self.oval_format == 'dpkg':
1457 mapping = {
1458@@ -914,6 +1939,7 @@ class OvalGeneratorUSN():
1459
1460 return _object
1461
1462+ # TODO: xml lib
1463 def create_release_state(self):
1464 if self.oval_format == 'dpkg':
1465 mapping = {
1466@@ -980,6 +2006,7 @@ class OvalGeneratorUSN():
1467 if key[1] == max_severity][0][0]
1468 return usn_severity.capitalize()
1469
1470+ # TODO: xml lib
1471 def create_usn_definition(self, usn_object, usn_number, id_base, test_refs, cve_dir, instructions):
1472 urls, cves_info = self.format_cves_info(usn_object['cves'], cve_dir)
1473 cve_references = self.create_cves_references(cves_info)
1474@@ -1058,6 +2085,7 @@ class OvalGeneratorUSN():
1475
1476 return definition
1477
1478+ # TODO: xml lib
1479 def create_usn_test(self, test_ref):
1480 mapping = {
1481 'id': test_ref['testref_id'],
1482@@ -1113,6 +2141,7 @@ class OvalGeneratorUSN():
1483
1484 return test
1485
1486+ # TODO: xml lib
1487 def create_usn_object(self, test_ref):
1488 mapping = {
1489 'id': test_ref['testref_id'],
1490@@ -1168,7 +2197,8 @@ class OvalGeneratorUSN():
1491 </ind:textfilecontent54_object>""".format(**mapping)
1492
1493 return _object
1494-
1495+
1496+ # TODO: xml lib
1497 def create_usn_state(self, test_ref):
1498 mapping = {
1499 'id': test_ref['testref_id'],
1500@@ -1229,7 +2259,8 @@ class OvalGeneratorUSN():
1501 </ind:textfilecontent54_state>""".format(**mapping)
1502
1503 return state
1504-
1505+
1506+ # TODO: xml lib
1507 def create_usn_variable(self, test_ref):
1508 binaries_list = test_ref['pkgs']
1509
1510@@ -1300,7 +2331,7 @@ class OvalGeneratorUSN():
1511 else:
1512 return None
1513
1514- cve_object = load_cve(cve_file_path)
1515+ cve_object = cve_lib.load_cve(cve_file_path)
1516 if not cve_object:
1517 return None
1518
1519@@ -1407,23 +2438,23 @@ class OvalGeneratorUSN():
1520 break
1521 except KeyError:
1522 # trusty usns don't have pocket, so try to check on timestamp
1523- if self.release_codename == 'trusty' and stamp >= release_stamp('esm/trusty'):
1524+ if self.release_codename == 'trusty' and stamp >= cve_lib.release_stamp('esm/trusty'):
1525 self.pocket = 'esm'
1526 else:
1527 self.pocket = 'security'
1528 break
1529
1530 if self.pocket in ['security', 'updates', 'livepatch']:
1531- self.release_name = release_name(self.release_codename)
1532- self.product_description = get_subproject_description(self.release_codename)
1533+ self.release_name = cve_lib.release_name(self.release_codename)
1534+ self.product_description = cve_lib.get_subproject_description(self.release_codename)
1535 else:
1536 # deal with trusty's weirdness
1537 if self.release_codename == 'trusty':
1538- self.release_name = release_name('esm/' + self.release_codename)
1539- self.product_description = get_subproject_description('esm/' + self.release_codename)
1540+ self.release_name = cve_lib.release_name('esm/' + self.release_codename)
1541+ self.product_description = cve_lib.get_subproject_description('esm/' + self.release_codename)
1542 else:
1543- self.release_name = release_name(self.pocket + '/' + self.release_codename)
1544- self.product_description = get_subproject_description(self.pocket + '/' + self.release_codename)
1545+ self.release_name = cve_lib.release_name(self.pocket + '/' + self.release_codename)
1546+ self.product_description = cve_lib.get_subproject_description(self.pocket + '/' + self.release_codename)
1547
1548 def generate_usn_oval(self, usn_object, usn_number, cve_dir):
1549 if self.release_codename not in usn_object['releases'].keys():
1550@@ -1479,6 +2510,7 @@ class OvalGeneratorUSN():
1551 if usn_variable:
1552 self.oval_structure['variable'].write(usn_variable)
1553
1554+ # TODO: xml lib
1555 def write_oval_elements(self):
1556 """ write OVAL elements to .xml file w. OVAL header and footer """
1557 for key in self.oval_structure:
1558diff --git a/scripts/source_map.py b/scripts/source_map.py
1559index e4d221e..4bb69d1 100755
1560--- a/scripts/source_map.py
1561+++ b/scripts/source_map.py
1562@@ -304,6 +304,8 @@ def load_sources_collection(item, map):
1563 pkg = parser.section['Package']
1564 map.setdefault(release, dict()).setdefault(pkg, {'section': 'unset', 'version': '~', 'pocket': 'unset'})
1565 map[release][pkg]['section'] = section
1566+ if 'Description' in parser.section:
1567+ map[release][pkg]['description'] = parser.section['Description']
1568 if not pocket:
1569 map[release][pkg]['release_version'] = parser.section['Version']
1570 if apt_pkg.version_compare(parser.section['Version'], map[release][pkg]['version']) > 0:
1571@@ -322,6 +324,9 @@ def load_packages_collection(item, map):
1572 pkg = parser.section['Package']
1573 map.setdefault(release, dict()).setdefault(pkg, {'section': 'unset', 'version': '~', 'pocket': 'unset'})
1574 map[release][pkg]['section'] = section
1575+ if 'Description' in parser.section:
1576+ map[release][pkg]['description'] = parser.section['Description']
1577+
1578 if not pocket:
1579 map[release][pkg]['release_version'] = parser.section['Version']
1580 if apt_pkg.version_compare(parser.section['Version'], map[release][pkg]['version']) > 0:

Subscribers

People subscribed via source and target branches